Fix merge conflicts

This commit is contained in:
dinamiko 2021-09-30 12:52:15 +02:00
commit 57ff2cc649
106 changed files with 10786 additions and 7645 deletions

View file

@ -1,23 +0,0 @@
language: php
os: linux
dist: xenial
notifications:
email: false
php:
- 7.0
branches:
only:
- master
- trunk
- compat/ppxo
script: |
CHANGED_FILES=`git diff --name-only --diff-filter=ACMR $TRAVIS_COMMIT_RANGE | grep \\\\.php | awk '{print}' ORS=' '`
if [ "$CHANGED_FILES" != "" ]; then
composer global require woocommerce/woocommerce-sniffs --update-with-all-dependencies
$HOME/.config/composer/vendor/bin/phpcs -p $CHANGED_FILES
fi

View file

@ -29,8 +29,8 @@ You can also use the Docker environment which includes WP, WC and all developmen
0. Install Docker and Docker Compose.
1. `$ cp .env.example .env` and edit the configuration in the `.env` file if needed.
2. `$ yarn run docker:build` (or copy the commands from [package.json](/package.json) if you do not have `yarn`).
3. `$ yarn docker:install`
4. `$ yarn docker:start`
3. `$ yarn run docker:install`
4. `$ yarn run docker:start`
5. Add `127.0.0.1 wc-pp.myhost` to your `hosts` file and open http://wc-pp.myhost (the default value of `WP_DOMAIN` in `.env`).
Tests and code style:
@ -44,30 +44,21 @@ After some changes in `.env` (such as PHP, WP versions) you may need to rebuild
See [package.json](/package.json) for other useful commands.
## Preparation for wordpress.org release
## Building a release package
If you want to deploy a new version, you need to do some preparation:
If you want to build a release package
(that can be used for deploying a new version on wordpress.org or manual installation on a WP website via ZIP uploading),
follow these steps:
### Clone
Clone the repository and `cd` into it
### Build
The following command should get you a ZIP file ready to be used on a WordPress site.
1. Clone the repository and `cd` into it.
2. Make sure you have the version in the plugin root file updated.
3. Update the PayPal JavaScript SDK integration date by using the current date for the `PAYPAL_INTEGRATION_DATE` constant.
4. The following command should get you a ZIP file ready to be used on a WordPress site:
```
npm run build
$ yarn run build
```
### Update version
Make sure you have the version in the plugin root file updated.
### Fixate integration date
Fix the PayPal JavaScript SDK integration date by using the current date for the `PAYPAL_INTEGRATION_DATE` constant.
## License
[GPL-2.0 License](LICENSE)

56
bootstrap.php Normal file
View file

@ -0,0 +1,56 @@
<?php
/**
* Bootstraps the modular app.
*
* @package WooCommerce\PayPalCommerce
*/
use Dhii\Container\CachingContainer;
use Dhii\Container\CompositeCachingServiceProvider;
use Dhii\Container\CompositeContainer;
use Dhii\Container\DelegatingContainer;
use Dhii\Container\ProxyContainer;
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
return function (
string $root_dir,
ContainerInterface ...$additional_containers
): ContainerInterface {
$modules = ( require "$root_dir/modules.php" )( $root_dir );
// Use this filter to add custom module or remove some of existing ones.
// Modules able to access container, add services and modify existing ones.
$modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );
$providers = array_map(
function ( ModuleInterface $module ): ServiceProviderInterface {
return $module->setup();
},
$modules
);
$provider = new CompositeCachingServiceProvider( $providers );
$proxy_container = new ProxyContainer();
// TODO: caching does not work currently,
// may want to consider fixing it later (pass proxy as parent to DelegatingContainer)
// for now not fixed since we were using this behavior for long time and fixing it now may break things.
$container = new DelegatingContainer( $provider );
$app_container = new CachingContainer(
new CompositeContainer(
array_merge(
$additional_containers,
array( $container )
)
)
);
$proxy_container->setInnerContainer( $app_container );
foreach ( $modules as $module ) {
/* @var $module ModuleInterface module */
$module->run( $app_container );
}
return $app_container;
};

View file

@ -1,5 +1,19 @@
*** Changelog ***
= 1.6.0 - 2021-09-29 =
* Add - Webhook status. #246 #273
* Add - Show CC gateway in admin payments list. #236
* Add - Add 3d secure contingency settings. #230
* Add - Improve logging. #252 #275
* Add - Do not send payee email. #231
* Add - Allow customers to see and delete their saved payments in My Account. #274
* Fix - PayPal Payments generates multiple orders. #244
* Fix - Saved credit card does not auto fill. #242
* Fix - Incorrect webhooks registration. #254
* Fix - Disable funding credit cards affecting hosted fields, unset for GB. #249
* Fix - REFUND_CAPTURE_CURRENCY_MISMATCH on multicurrency sites. #225
* Fix - Can't checkout to certain countries with optional postcode. #224
= 1.5.1 - 2021-08-19 =
* Fix - Set 3DS contingencies to "SCA_WHEN_REQUIRED". #178
* Fix - Plugin conflict blocking line item details. #221

30
modules.php Normal file
View file

@ -0,0 +1,30 @@
<?php
/**
* The list of modules.
*
* @package WooCommerce\PayPalCommerce
*/
use WooCommerce\PayPalCommerce\PluginModule;
return function ( string $root_dir ): iterable {
$modules_dir = "$root_dir/modules";
$modules = array(
new PluginModule(),
( require "$modules_dir/woocommerce-logging/module.php" )(),
( require "$modules_dir/ppcp-admin-notices/module.php" )(),
( require "$modules_dir/ppcp-api-client/module.php" )(),
( require "$modules_dir/ppcp-button/module.php" )(),
( require "$modules_dir/ppcp-compat/module.php" )(),
( require "$modules_dir/ppcp-onboarding/module.php" )(),
( require "$modules_dir/ppcp-session/module.php" )(),
( require "$modules_dir/ppcp-status-report/module.php" )(),
( require "$modules_dir/ppcp-subscription/module.php" )(),
( require "$modules_dir/ppcp-wc-gateway/module.php" )(),
( require "$modules_dir/ppcp-webhooks/module.php" )(),
( require "$modules_dir/ppcp-vaulting/module.php" )(),
);
return $modules;
};

View file

@ -36,6 +36,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
@ -72,19 +73,19 @@ return array(
return 'WC-';
},
'api.bearer' => static function ( ContainerInterface $container ): Bearer {
$cache = new Cache( 'ppcp-paypal-bearer' );
$key = $container->get( 'api.key' );
$secret = $container->get( 'api.secret' );
$host = $container->get( 'api.host' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$settings = $container->get( 'wcgateway.settings' );
return new PayPalBearer(
$cache,
$host,
$key,
$secret,
$logger
$logger,
$settings
);
},
'api.endpoint.partners' => static function ( ContainerInterface $container ) : PartnersEndpoint {
@ -115,6 +116,7 @@ return array(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.webhook' ),
$container->get( 'api.factory.webhook-event' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
@ -215,7 +217,10 @@ return array(
'api.factory.webhook' => static function ( ContainerInterface $container ): WebhookFactory {
return new WebhookFactory();
},
'api.factory.capture' => static function ( ContainerInterface $container ): CaptureFactory {
'api.factory.webhook-event' => static function ( $container ): WebhookEventFactory {
return new WebhookEventFactory();
},
'api.factory.capture' => static function ( $container ): CaptureFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new CaptureFactory( $amount_factory );

View file

@ -390,7 +390,7 @@ class OrderEndpoint {
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
if ( false !== strpos( $response['body'], ErrorResponse::ORDER_ALREADY_AUTHORIZED ) ) {
return $this->order( $order->id() );
}

View file

@ -139,24 +139,7 @@ class PaymentTokenEndpoint {
foreach ( $json->payment_tokens as $token_value ) {
$tokens[] = $this->factory->from_paypal_response( $token_value );
}
if ( empty( $tokens ) ) {
$error = new RuntimeException(
sprintf(
// translators: %d is the customer id.
__( 'No token stored for customer %d.', 'woocommerce-paypal-payments' ),
$id
)
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
return $tokens;
}

View file

@ -36,6 +36,48 @@ trait RequestTrait {
$args['headers']['PayPal-Partner-Attribution-Id'] = 'Woo_PPCP';
}
return wp_remote_get( $url, $args );
$response = wp_remote_get( $url, $args );
$this->logger->debug( $this->request_response_string( $url, $args, $response ) );
return $response;
}
/**
* Returns request and response information as string.
*
* @param string $url The request URL.
* @param array $args The request arguments.
* @param array $response The response.
* @return string
*/
private function request_response_string( string $url, array $args, array $response ): string {
$method = $args['method'] ?? '';
$output = $method . ' ' . $url . "\n";
if ( isset( $args['body'] ) ) {
if ( ! in_array(
$url,
array(
trailingslashit( $this->host ) . 'v1/oauth2/token/',
trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials',
),
true
) ) {
$output .= 'Request Body: ' . wc_print_r( $args['body'], true ) . "\n";
}
}
if ( isset( $response['headers']->getAll()['paypal-debug-id'] ) ) {
$output .= 'Response Debug ID: ' . $response['headers']->getAll()['paypal-debug-id'] . "\n";
}
if ( isset( $response['response'] ) ) {
$output .= 'Response: ' . wc_print_r( $response['response'], true ) . "\n";
if ( isset( $response['body'] )
&& isset( $response['response']['code'] )
&& ! in_array( $response['response']['code'], array( 200, 201, 202, 204 ), true ) ) {
$output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n";
}
}
return $output;
}
}

View file

@ -11,8 +11,10 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use Psr\Log\LoggerInterface;
@ -44,6 +46,13 @@ class WebhookEndpoint {
*/
private $webhook_factory;
/**
* The webhook event factory.
*
* @var WebhookEventFactory
*/
private $webhook_event_factory;
/**
* The logger.
*
@ -57,18 +66,21 @@ class WebhookEndpoint {
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param WebhookFactory $webhook_factory The webhook factory.
* @param WebhookEventFactory $webhook_event_factory The webhook event factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
WebhookFactory $webhook_factory,
WebhookEventFactory $webhook_event_factory,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->webhook_factory = $webhook_factory;
$this->webhook_event_factory = $webhook_event_factory;
$this->logger = $logger;
}
@ -79,14 +91,14 @@ class WebhookEndpoint {
*
* @return Webhook
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create( Webhook $hook ): Webhook {
/**
* An hook, which has an ID has already been created.
*/
// The hook was already created.
if ( $hook->id() ) {
return $hook;
}
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/notifications/webhooks';
$args = array(
@ -100,42 +112,64 @@ class WebhookEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
throw new RuntimeException(
__( 'Not able to create a webhook.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
$error = new PayPalApiException(
throw new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$hook = $this->webhook_factory->from_paypal_response( $json );
return $hook;
}
/**
* Loads the webhooks list for the current auth token.
*
* @return Webhook[]
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function list(): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/notifications/webhooks';
$args = array(
'method' => 'GET',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
throw new RuntimeException(
__( 'Not able to load webhooks list.', 'woocommerce-paypal-payments' )
);
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return array_map(
array( $this->webhook_factory, 'from_paypal_response' ),
$json->webhooks
);
}
/**
* Deletes a webhook.
*
@ -160,22 +194,61 @@ class WebhookEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
throw new RuntimeException(
__( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
return wp_remote_retrieve_response_code( $response ) === 204;
}
/**
* Request a simulated webhook to be sent.
*
* @param Webhook $hook The webhook subscription to use.
* @param string $event_type The event type, such as CHECKOUT.ORDER.APPROVED.
* @param string|null $resource_version The event resource version, such as 2.0.
*
* @return WebhookEvent
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function simulate( Webhook $hook, string $event_type, ?string $resource_version ): WebhookEvent {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/notifications/simulate-event';
$data = array(
'webhook_id' => $hook->id(),
'event_type' => $event_type,
);
if ( $resource_version ) {
$data['resource_version'] = $resource_version;
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
throw new RuntimeException(
__( 'Not able to simulate webhook.', 'woocommerce-paypal-payments' )
);
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 202 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->webhook_event_factory->from_paypal_response( $json );
}
/**
* Verifies if a webhook event is legitimate.
*

View file

@ -112,7 +112,7 @@ class CardAuthenticationResult {
$data['liability_shift'] = $this->liability_shift();
$data['three_d_secure'] = array(
'enrollment_status' => $this->enrollment_status(),
'authentication_result' => $this->authentication_result(),
'authentication_status' => $this->authentication_result(),
);
return $data;
}

View file

@ -68,11 +68,11 @@ class Payee {
* @return array
*/
public function to_array(): array {
$data = array(
'email_address' => $this->email(),
);
$data = array();
if ( $this->merchant_id ) {
$data['merchant_id'] = $this->merchant_id();
} else {
$data['email_address'] = $this->email();
}
return $data;
}

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use stdClass;
/**
* Class Webhook
*/
@ -71,13 +73,38 @@ class Webhook {
/**
* Returns the event types.
*
* @return array
* @return stdClass[]
*/
public function event_types(): array {
return $this->event_types;
}
/**
* Returns the human-friendly names of the event types.
*
* @return string[]
*/
public function humanfriendly_event_names(): array {
return array_map(
function ( $event ): string {
return Webhook::get_humanfriendly_event_name( $event->name );
},
$this->event_types
);
}
/**
* Converts event names to more human-friendly form.
*
* @param string $name The event name like 'CHECKOUT.ORDER.APPROVED'.
* @return string
*/
public static function get_humanfriendly_event_name( string $name ): string {
return strtolower( str_replace( '.', ' ', $name ) );
}
/**
* Returns the object as array.
*

View file

@ -0,0 +1,170 @@
<?php
/**
* The Webhook event notification object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use DateTime;
use stdClass;
/**
* Class WebhookEvent
*/
class WebhookEvent {
/**
* The ID of the event notification.
*
* @var string
*/
private $id;
/**
* The date and time when the event notification was created.
*
* @var DateTime|null
*/
private $create_time;
/**
* The name of the resource related to the webhook notification event, such as 'checkout-order'.
*
* @var string
*/
private $resource_type;
/**
* The event version in the webhook notification, such as '1.0'.
*
* @var string
*/
private $event_version;
/**
* The event that triggered the webhook event notification, such as 'CHECKOUT.ORDER.APPROVED'.
*
* @var string
*/
private $event_type;
/**
* A summary description for the event notification.
*
* @var string
*/
private $summary;
/**
* The resource version in the webhook notification, such as '1.0'.
*
* @var string
*/
private $resource_version;
/**
* The resource that triggered the webhook event notification.
*
* @var stdClass
*/
private $resource;
/**
* WebhookEvent constructor.
*
* @param string $id The ID of the event notification.
* @param DateTime|null $create_time The date and time when the event notification was created.
* @param string $resource_type The name of the resource related to the webhook notification event, such as 'checkout-order'.
* @param string $event_version The event version in the webhook notification, such as '1.0'.
* @param string $event_type The event that triggered the webhook event notification, such as 'CHECKOUT.ORDER.APPROVED'.
* @param string $summary A summary description for the event notification.
* @param string $resource_version The resource version in the webhook notification, such as '1.0'.
* @param stdClass $resource The resource that triggered the webhook event notification.
*/
public function __construct( string $id, ?DateTime $create_time, string $resource_type, string $event_version, string $event_type, string $summary, string $resource_version, stdClass $resource ) {
$this->id = $id;
$this->create_time = $create_time;
$this->resource_type = $resource_type;
$this->event_version = $event_version;
$this->event_type = $event_type;
$this->summary = $summary;
$this->resource_version = $resource_version;
$this->resource = $resource;
}
/**
* The ID of the event notification.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* The date and time when the event notification was created.
*
* @return DateTime|null
*/
public function create_time(): ?DateTime {
return $this->create_time;
}
/**
* The name of the resource related to the webhook notification event, such as 'checkout-order'.
*
* @return string
*/
public function resource_type(): string {
return $this->resource_type;
}
/**
* The event version in the webhook notification, such as '1.0'.
*
* @return string
*/
public function event_version(): string {
return $this->event_version;
}
/**
* The event that triggered the webhook event notification, such as 'CHECKOUT.ORDER.APPROVED'.
*
* @return string
*/
public function event_type(): string {
return $this->event_type;
}
/**
* A summary description for the event notification.
*
* @return string
*/
public function summary(): string {
return $this->summary;
}
/**
* The resource version in the webhook notification, such as '1.0'.
*
* @return string
*/
public function resource_version(): string {
return $this->resource_version;
}
/**
* The resource that triggered the webhook event notification.
*
* @return stdClass
*/
public function resource(): stdClass {
return $this->resource;
}
}

View file

@ -26,13 +26,8 @@ class PayeeFactory {
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $data ) {
if ( ! isset( $data->email_address ) ) {
throw new RuntimeException(
__( 'No email for payee given.', 'woocommerce-paypal-payments' )
);
}
$email = ( isset( $data->email_address ) ) ? $data->email_address : '';
$merchant_id = ( isset( $data->merchant_id ) ) ? $data->merchant_id : '';
return new Payee( $data->email_address, $merchant_id );
return new Payee( $email, $merchant_id );
}
}

View file

@ -37,8 +37,8 @@ class PaymentSourceFactory {
(string) $data->card->authentication_result->liability_shift : '',
isset( $data->card->authentication_result->three_d_secure->enrollment_status ) ?
(string) $data->card->authentication_result->three_d_secure->enrollment_status : '',
isset( $data->card->authentication_result->three_d_secure->authentication_result ) ?
(string) $data->card->authentication_result->three_d_secure->authentication_result : ''
isset( $data->card->authentication_result->three_d_secure->authentication_status ) ?
(string) $data->card->authentication_result->three_d_secure->authentication_status : ''
);
}
$card = new PaymentSourceCard(

View file

@ -158,6 +158,8 @@ class PurchaseUnitFactory {
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer );
if (
2 !== strlen( $shipping->address()->country_code() )
|| ( ! $shipping->address()->postal_code() )
|| $this->country_without_postal_code( $shipping->address()->country_code() )
) {
$shipping = null;
}
@ -264,4 +266,18 @@ class PurchaseUnitFactory {
}
return false;
}
/**
* Check if country does not have postal code.
*
* @param string $country_code The country code.
* @return bool Whether country has postal code or not.
*/
private function country_without_postal_code( string $country_code ): bool {
$countries = array( 'AE', 'AF', 'AG', 'AI', 'AL', 'AN', 'AO', 'AW', 'BB', 'BF', 'BH', 'BI', 'BJ', 'BM', 'BO', 'BS', 'BT', 'BW', 'BZ', 'CD', 'CF', 'CG', 'CI', 'CK', 'CL', 'CM', 'CO', 'CR', 'CV', 'DJ', 'DM', 'DO', 'EC', 'EG', 'ER', 'ET', 'FJ', 'FK', 'GA', 'GD', 'GH', 'GI', 'GM', 'GN', 'GQ', 'GT', 'GW', 'GY', 'HK', 'HN', 'HT', 'IE', 'IQ', 'IR', 'JM', 'JO', 'KE', 'KH', 'KI', 'KM', 'KN', 'KP', 'KW', 'KY', 'LA', 'LB', 'LC', 'LK', 'LR', 'LS', 'LY', 'ML', 'MM', 'MO', 'MR', 'MS', 'MT', 'MU', 'MW', 'MZ', 'NA', 'NE', 'NG', 'NI', 'NP', 'NR', 'NU', 'OM', 'PA', 'PE', 'PF', 'PY', 'QA', 'RW', 'SA', 'SB', 'SC', 'SD', 'SL', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SY', 'TC', 'TD', 'TG', 'TL', 'TO', 'TT', 'TV', 'TZ', 'UG', 'UY', 'VC', 'VE', 'VG', 'VN', 'VU', 'WS', 'XA', 'XB', 'XC', 'XE', 'XL', 'XM', 'XN', 'XS', 'YE', 'ZM', 'ZW' );
if ( in_array( $country_code, $countries, true ) ) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* Creates WebhookEvent.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use DateTime;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class WebhookEventFactory
*/
class WebhookEventFactory {
/**
* Returns a webhook from a given data array.
*
* @param array $data The data array.
*
* @return WebhookEvent
*/
public function from_array( array $data ): WebhookEvent {
return $this->from_paypal_response( (object) $data );
}
/**
* Returns a Webhook based of a PayPal JSON response.
*
* @param stdClass $data The JSON object.
*
* @return WebhookEvent
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): WebhookEvent {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'ID for webhook event not found.', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->event_type ) ) {
throw new RuntimeException(
__( 'Event type for webhook event not found.', 'woocommerce-paypal-payments' )
);
}
$create_time = ( isset( $data->create_time ) ) ?
DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $data->create_time )
: null;
// Sometimes the time may be in weird format 2018-12-19T22:20:32.000Z (at least in simulation),
// we do not care much about time, so just ignore on failure.
if ( false === $create_time ) {
$create_time = null;
}
return new WebhookEvent(
(string) $data->id,
$create_time,
(string) $data->resource_type ?? '',
(string) $data->event_version ?? '',
(string) $data->event_type,
(string) $data->summary ?? '',
(string) $data->resource_version ?? '',
(object) $data->resource ?? ( new stdClass() )
);
}
}

View file

@ -7,13 +7,13 @@
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"babel-loader": "^8.1.0",
"cross-env": "^5.0.1",
"file-loader": "^4.2.0",
"node-sass": "^4.13.0",
"sass-loader": "^8.0.0",
"webpack": "^4.42.1",
"webpack-cli": "^3.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0"
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.55.0",
"webpack-cli": "^4.8.0"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -7,3 +7,7 @@
.payments-sdk-contingency-handler {
z-index: 1000 !important;
}
.ppcp-credit-card-gateway-form-field-disabled {
opacity: .5 !important;
}

View file

@ -24,9 +24,11 @@ class CheckoutBootstap {
})
jQuery(document).on('hosted_fields_loaded', () => {
jQuery('#saved-credit-card').on('change', () => {
this.displayPlaceOrderButtonForSavedCreditCards()
})
});
this.switchBetweenPayPalandOrderButton()
this.displayPlaceOrderButtonForSavedCreditCards()
@ -100,13 +102,41 @@ class CheckoutBootstap {
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper)
jQuery('#place_order').show()
this.disableCreditCardFields()
} else {
jQuery('#place_order').hide()
this.renderer.hideButtons(this.gateway.button.wrapper)
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.showButtons(this.gateway.hosted_fields.wrapper)
this.enableCreditCardFields()
}
}
disableCreditCardFields() {
jQuery('label[for="ppcp-credit-card-gateway-card-number"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-number').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-expiry').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-cvc').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="vault"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').attr("disabled", true)
this.renderer.disableCreditCardFields()
}
enableCreditCardFields() {
jQuery('label[for="ppcp-credit-card-gateway-card-number"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-number').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-expiry').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-cvc').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="vault"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').attr("disabled", false)
this.renderer.enableCreditCardFields()
}
}
export default CheckoutBootstap

View file

@ -8,6 +8,8 @@ class CreditCardRenderer {
this.spinner = spinner;
this.cardValid = false;
this.formValid = false;
this.currentHostedFieldsInstance = null;
this.formSubmissionSubscribed = false;
}
render(wrapper, contextConfig) {
@ -31,6 +33,12 @@ class CreditCardRenderer {
return;
}
if (this.currentHostedFieldsInstance) {
this.currentHostedFieldsInstance.teardown()
.catch(err => console.error(`Hosted fields teardown error: ${err}`));
this.currentHostedFieldsInstance = null;
}
const gateWayBox = document.querySelector('.payment_box.payment_method_ppcp-credit-card-gateway');
const oldDisplayStyle = gateWayBox.style.display;
gateWayBox.style.display = 'block';
@ -92,36 +100,11 @@ class CreditCardRenderer {
}
}
}).then(hostedFields => {
const submitEvent = (event) => {
this.spinner.block();
if (event) {
event.preventDefault();
}
this.errorHandler.clear();
document.dispatchEvent(new CustomEvent("hosted_fields_loaded"));
this.currentHostedFieldsInstance = hostedFields;
if (this.formValid && this.cardValid) {
const save_card = this.defaultConfig.save_card ? true : false;
const vault = document.getElementById('ppcp-credit-card-vault') ?
document.getElementById('ppcp-credit-card-vault').checked : save_card;
hostedFields.submit({
contingencies: ['SCA_WHEN_REQUIRED'],
vault: vault
}).then((payload) => {
payload.orderID = payload.orderId;
this.spinner.unblock();
return contextConfig.onApprove(payload);
}).catch(() => {
this.errorHandler.genericError();
this.spinner.unblock();
});
} else {
this.spinner.unblock();
const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid;
this.errorHandler.message(message);
}
}
hostedFields.on('inputSubmitRequest', function () {
submitEvent(null);
hostedFields.on('inputSubmitRequest', () => {
this._submit(contextConfig);
});
hostedFields.on('cardTypeChange', (event) => {
if ( ! event.cards.length ) {
@ -137,11 +120,18 @@ class CreditCardRenderer {
});
this.formValid = formValid;
})
});
if (!this.formSubmissionSubscribed) {
document.querySelector(wrapper + ' button').addEventListener(
'click',
submitEvent
event => {
event.preventDefault();
this._submit(contextConfig);
}
);
this.formSubmissionSubscribed = true;
}
});
document.querySelector('#payment_method_ppcp-credit-card-gateway').addEventListener(
@ -151,5 +141,69 @@ class CreditCardRenderer {
}
)
}
disableFields() {
if( this.currentHostedFieldsInstance) {
this.currentHostedFieldsInstance.setAttribute({
field: 'number',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.setAttribute({
field: 'cvv',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.setAttribute({
field: 'expirationDate',
attribute: 'disabled'
})
}
}
enableFields() {
if( this.currentHostedFieldsInstance) {
this.currentHostedFieldsInstance.removeAttribute({
field: 'number',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.removeAttribute({
field: 'cvv',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.removeAttribute({
field: 'expirationDate',
attribute: 'disabled'
})
}
}
_submit(contextConfig) {
this.spinner.block();
this.errorHandler.clear();
if (this.formValid && this.cardValid) {
const save_card = this.defaultConfig.save_card ? true : false;
const vault = document.getElementById('ppcp-credit-card-vault') ?
document.getElementById('ppcp-credit-card-vault').checked : save_card;
const contingency = this.defaultConfig.hosted_fields.contingency;
const hostedFieldsData = {
vault: vault
};
if (contingency !== 'NO_3D_SECURE') {
hostedFieldsData.contingencies = [contingency];
}
this.currentHostedFieldsInstance.submit(hostedFieldsData).then((payload) => {
payload.orderID = payload.orderId;
this.spinner.unblock();
return contextConfig.onApprove(payload);
}).catch(err => {
console.error(err);
this.spinner.unblock();
});
} else {
this.spinner.unblock();
const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid;
this.errorHandler.message(message);
}
}
}
export default CreditCardRenderer;

View file

@ -43,6 +43,14 @@ class Renderer {
domElement.style.display = 'block';
return true;
}
disableCreditCardFields() {
this.creditCardRenderer.disableFields();
}
enableCreditCardFields() {
this.creditCardRenderer.enableFields();
}
}
export default Renderer;

View file

@ -68,7 +68,7 @@ return array(
$subscription_helper = $container->get( 'subscription.helper' );
$messages_apply = $container->get( 'button.helper.messages-apply' );
$environment = $container->get( 'onboarding.environment' );
$payment_token_repository = $container->get( 'subscription.repository.payment-token' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
$settings_status = $container->get( 'wcgateway.settings.status' );
return new SmartButton(
$container->get( 'button.url' ),
@ -103,7 +103,8 @@ return array(
$request_data = $container->get( 'button.request-data' );
$repository = $container->get( 'api.repository.cart' );
$data_store = \WC_Data_Store::load( 'product' );
return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store, $logger );
},
'button.endpoint.create-order' => static function ( ContainerInterface $container ): CreateOrderEndpoint {
$request_data = $container->get( 'button.request-data' );
@ -114,6 +115,7 @@ return array(
$session_handler = $container->get( 'session.handler' );
$settings = $container->get( 'wcgateway.settings' );
$early_order_handler = $container->get( 'button.helper.early-order-handler' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new CreateOrderEndpoint(
$request_data,
$cart_repository,
@ -122,7 +124,8 @@ return array(
$payer_factory,
$session_handler,
$settings,
$early_order_handler
$early_order_handler,
$logger
);
},
'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler {
@ -154,13 +157,16 @@ return array(
'button.endpoint.data-client-id' => static function( ContainerInterface $container ) : DataClientIdEndpoint {
$request_data = $container->get( 'button.request-data' );
$identity_token = $container->get( 'api.endpoint.identity-token' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new DataClientIdEndpoint(
$request_data,
$identity_token
$identity_token,
$logger
);
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
return new ThreeDSecure();
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger );
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply();

View file

@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -601,6 +601,19 @@ class SmartButton implements SmartButtonInterface {
return $this->subscription_helper->cart_contains_subscription();
}
/**
* Retrieves the 3D Secure contingency settings.
*
* @return string
*/
private function get_3ds_contingency(): string {
if ( $this->settings->has( '3d_secure_contingency' ) ) {
return $this->settings->get( '3d_secure_contingency' );
}
return 'SCA_WHEN_REQUIRED';
}
/**
* The localized data for the smart button.
*
@ -677,6 +690,7 @@ class SmartButton implements SmartButtonInterface {
),
),
'valid_cards' => $this->dcc_applies->valid_cards(),
'contingency' => $this->get_3ds_contingency(),
),
'messages' => $this->message_values(),
'labels' => array(
@ -729,8 +743,7 @@ class SmartButton implements SmartButtonInterface {
'currency' => get_woocommerce_currency(),
'integration-date' => PAYPAL_INTEGRATION_DATE,
'components' => implode( ',', $this->components() ),
'vault' => $this->can_save_vault_token() ?
'true' : 'false',
'vault' => $this->can_save_vault_token() ? 'true' : 'false',
'commit' => is_checkout() ? 'true' : 'false',
'intent' => ( $this->settings->has( 'intent' ) ) ?
$this->settings->get( 'intent' ) : 'capture',
@ -743,11 +756,20 @@ class SmartButton implements SmartButtonInterface {
) {
$params['buyer-country'] = WC()->customer->get_billing_country();
}
$disable_funding = $this->settings->has( 'disable_funding' ) ?
$this->settings->get( 'disable_funding' ) : array();
$disable_funding = $this->settings->has( 'disable_funding' )
? $this->settings->get( 'disable_funding' )
: array();
if ( ! is_checkout() ) {
$disable_funding[] = 'card';
}
if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) {
$key = array_search( 'card', $disable_funding, true );
if ( false !== $key ) {
unset( $disable_funding[ $key ] );
}
}
if ( count( $disable_funding ) > 0 ) {
$params['disable-funding'] = implode( ',', array_unique( $disable_funding ) );

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
@ -195,7 +196,9 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_order( $order );
wp_send_json_success( $order );
return true;
} catch ( \RuntimeException $error ) {
} catch ( Exception $error ) {
$this->logger->error( 'Order approve failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
@ -57,6 +59,13 @@ class ChangeCartEndpoint implements EndpointInterface {
*/
private $product_data_store;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* ChangeCartEndpoint constructor.
*
@ -65,13 +74,15 @@ class ChangeCartEndpoint implements EndpointInterface {
* @param RequestData $request_data The request data helper.
* @param CartRepository $repository The repository for the current purchase items.
* @param \WC_Data_Store $product_data_store The data store for products.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
\WC_Cart $cart,
\WC_Shipping $shipping,
RequestData $request_data,
CartRepository $repository,
\WC_Data_Store $product_data_store
\WC_Data_Store $product_data_store,
LoggerInterface $logger
) {
$this->cart = $cart;
@ -79,6 +90,7 @@ class ChangeCartEndpoint implements EndpointInterface {
$this->request_data = $request_data;
$this->repository = $repository;
$this->product_data_store = $product_data_store;
$this->logger = $logger;
}
/**
@ -94,12 +106,13 @@ class ChangeCartEndpoint implements EndpointInterface {
* Handles the request.
*
* @return bool
* @throws \Exception On error.
*/
public function handle_request(): bool {
try {
return $this->handle_data();
} catch ( RuntimeException $error ) {
} catch ( Exception $error ) {
$this->logger->error( 'Cart updating failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
@ -116,7 +129,7 @@ class ChangeCartEndpoint implements EndpointInterface {
* Handles the request data.
*
* @return bool
* @throws \Exception On error.
* @throws Exception On error.
*/
private function handle_data(): bool {
$data = $this->request_data->read_request( $this->nonce() );
@ -234,7 +247,7 @@ class ChangeCartEndpoint implements EndpointInterface {
* @param int $quantity The Quantity.
*
* @return bool
* @throws \Exception When product could not be added.
* @throws Exception When product could not be added.
*/
private function add_product( \WC_Product $product, int $quantity ): bool {
return false !== $this->cart->add_to_cart( $product->get_id(), $quantity );
@ -249,7 +262,7 @@ class ChangeCartEndpoint implements EndpointInterface {
* @param array $post_variations The variations.
*
* @return bool
* @throws \Exception When product could not be added.
* @throws Exception When product could not be added.
*/
private function add_variable_product(
\WC_Product $product,

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
@ -102,6 +104,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
private $purchase_units;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* CreateOrderEndpoint constructor.
*
@ -113,6 +122,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param SessionHandler $session_handler The SessionHandler object.
* @param Settings $settings The Settings object.
* @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
@ -122,7 +132,8 @@ class CreateOrderEndpoint implements EndpointInterface {
PayerFactory $payer_factory,
SessionHandler $session_handler,
Settings $settings,
EarlyOrderHandler $early_order_handler
EarlyOrderHandler $early_order_handler,
LoggerInterface $logger
) {
$this->request_data = $request_data;
@ -133,6 +144,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->early_order_handler = $early_order_handler;
$this->logger = $logger;
}
/**
@ -190,6 +202,8 @@ class CreateOrderEndpoint implements EndpointInterface {
wp_send_json_success( $order->to_array() );
return true;
} catch ( \RuntimeException $error ) {
$this->logger->error( 'Order creation failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
@ -198,7 +212,9 @@ class CreateOrderEndpoint implements EndpointInterface {
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
)
);
} catch ( \Exception $exception ) {
} catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
wc_add_notice( $exception->getMessage(), 'error' );
}
@ -212,11 +228,16 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param \WP_Error $errors The errors, which occurred.
*
* @return array
* @throws Exception On Error.
*/
public function after_checkout_validation( array $data, \WP_Error $errors ): array {
if ( ! $errors->errors ) {
try {
$order = $this->create_paypal_order();
} catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
throw $exception;
}
/**
* In case we are onboarded and everything is fine with the \WC_Order
@ -231,6 +252,8 @@ class CreateOrderEndpoint implements EndpointInterface {
return $data;
}
$this->logger->error( 'Checkout validation failed: ' . $errors->get_error_message() );
wp_send_json_error(
array(
'name' => '',
@ -336,7 +359,7 @@ class CreateOrderEndpoint implements EndpointInterface {
*
* @param string $form_values The values of the form.
*
* @throws \Exception On Error.
* @throws Exception On Error.
*/
private function process_checkout_form( string $form_values ) {
$form_values = explode( '&', $form_values );
@ -386,7 +409,7 @@ class CreateOrderEndpoint implements EndpointInterface {
*
* @param string $form_values The values of the form.
* @param \WC_Order|null $wc_order WC order to get data from.
* @throws \Exception On Error.
* @throws Exception On Error.
*/
private function process_checkout_form_when_creating_account( string $form_values, \WC_Order $wc_order = null ) {
$form_values = explode( '&', $form_values );
@ -406,7 +429,12 @@ class CreateOrderEndpoint implements EndpointInterface {
'woocommerce_after_checkout_validation',
function ( array $data, \WP_Error $errors ) use ( $wc_order ) {
if ( ! $errors->errors ) {
try {
$order = $this->create_paypal_order( $wc_order );
} catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
throw $exception;
}
wp_send_json_success( $order->to_array() );
return true;
}

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -35,19 +37,29 @@ class DataClientIdEndpoint implements EndpointInterface {
*/
private $identity_token;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* DataClientIdEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param IdentityToken $identity_token The Identity Token.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
IdentityToken $identity_token
IdentityToken $identity_token,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->identity_token = $identity_token;
$this->logger = $logger;
}
/**
@ -77,7 +89,9 @@ class DataClientIdEndpoint implements EndpointInterface {
)
);
return true;
} catch ( RuntimeException $error ) {
} catch ( Exception $error ) {
$this->logger->error( 'Client ID retrieval failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult as AuthResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
@ -17,12 +18,27 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
*/
class ThreeDSecure {
const NO_DECISION = 0;
const PROCCEED = 1;
const REJECT = 2;
const RETRY = 3;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* ThreeDSecure constructor.
*
* @param LoggerInterface $logger The logger.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* Determine, how we proceed with a given order.
*
@ -42,7 +58,10 @@ class ThreeDSecure {
if ( ! $order->payment_source()->card()->authentication_result() ) {
return self::NO_DECISION;
}
$result = $order->payment_source()->card()->authentication_result();
$this->logger->info( '3DS authentication result: ' . wc_print_r( $result->to_array(), true ) );
if ( $result->liability_shift() === AuthResult::LIABILITY_SHIFT_POSSIBLE ) {
return self::PROCCEED;
}

View file

@ -2,7 +2,7 @@ const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'sourcemap',
devtool: 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {

File diff suppressed because it is too large Load diff

View file

@ -170,6 +170,7 @@ return array(
$login_seller_sandbox = $container->get( 'api.endpoint.login-seller-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
$settings = $container->get( 'wcgateway.settings' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$cache = new Cache( 'ppcp-paypal-bearer' );
return new LoginSellerEndpoint(
@ -178,7 +179,8 @@ return array(
$login_seller_sandbox,
$partner_referrals_data,
$settings,
$cache
$cache,
$logger
);
},
'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals {

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
@ -67,6 +69,13 @@ class LoginSellerEndpoint implements EndpointInterface {
*/
private $cache;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* LoginSellerEndpoint constructor.
*
@ -76,6 +85,7 @@ class LoginSellerEndpoint implements EndpointInterface {
* @param PartnerReferralsData $partner_referrals_data The Partner Referrals Data.
* @param Settings $settings The Settings.
* @param Cache $cache The Cache.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
@ -83,7 +93,8 @@ class LoginSellerEndpoint implements EndpointInterface {
LoginSeller $login_seller_sandbox,
PartnerReferralsData $partner_referrals_data,
Settings $settings,
Cache $cache
Cache $cache,
LoggerInterface $logger
) {
$this->request_data = $request_data;
@ -92,6 +103,7 @@ class LoginSellerEndpoint implements EndpointInterface {
$this->partner_referrals_data = $partner_referrals_data;
$this->settings = $settings;
$this->cache = $cache;
$this->logger = $logger;
}
/**
@ -136,12 +148,13 @@ class LoginSellerEndpoint implements EndpointInterface {
$this->cache->delete( PayPalBearer::CACHE_KEY );
}
wp_schedule_single_event(
time() - 1,
time() + 5,
WebhookRegistrar::EVENT_HOOK
);
wp_send_json_success();
return true;
} catch ( \RuntimeException $error ) {
} catch ( Exception $error ) {
$this->logger->error( 'Onboarding completion handling error: ' . $error->getMessage() );
wp_send_json_error( $error->getMessage() );
return false;
}

View file

@ -10,8 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use Psr\Container\ContainerInterface;
return array(
'subscription.helper' => static function ( ContainerInterface $container ): SubscriptionHelper {
@ -19,7 +17,7 @@ return array(
},
'subscription.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$repository = $container->get( 'subscription.repository.payment-token' );
$repository = $container->get( 'vaulting.repository.payment-token' );
$endpoint = $container->get( 'api.endpoint.order' );
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' );

View file

@ -15,7 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Psr\Log\LoggerInterface;

View file

@ -1,148 +0,0 @@
<?php
/**
* The payment token repository returns or deletes payment tokens for users.
*
* @package WooCommerce\PayPalCommerce\Subscription\Repository
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
/**
* Class PaymentTokenRepository
*/
class PaymentTokenRepository {
const USER_META = 'ppcp-vault-token';
/**
* The payment token factory.
*
* @var PaymentTokenFactory
*/
private $factory;
/**
* The payment token endpoint.
*
* @var PaymentTokenEndpoint
*/
private $endpoint;
/**
* PaymentTokenRepository constructor.
*
* @param PaymentTokenFactory $factory The payment token factory.
* @param PaymentTokenEndpoint $endpoint The payment token endpoint.
*/
public function __construct(
PaymentTokenFactory $factory,
PaymentTokenEndpoint $endpoint
) {
$this->factory = $factory;
$this->endpoint = $endpoint;
}
/**
* Return a token for a user.
*
* @param int $id The user id.
*
* @return PaymentToken|null
*/
public function for_user_id( int $id ) {
try {
$token = (array) get_user_meta( $id, self::USER_META, true );
if ( ! $token || ! isset( $token['id'] ) ) {
return $this->fetch_for_user_id( $id );
}
$token = $this->factory->from_array( $token );
return $token;
} catch ( RuntimeException $error ) {
return null;
}
}
/**
* Return all tokens for a user.
*
* @param int $id The user id.
* @return PaymentToken[]
*/
public function all_for_user_id( int $id ) {
try {
$tokens = $this->endpoint->for_user( $id );
update_user_meta( $id, self::USER_META, $tokens );
return $tokens;
} catch ( RuntimeException $exception ) {
return array();
}
}
/**
* Delete a token for a user.
*
* @param int $user_id The user id.
* @param PaymentToken $token The token.
*
* @return bool
*/
public function delete_token( int $user_id, PaymentToken $token ): bool {
delete_user_meta( $user_id, self::USER_META );
return $this->endpoint->delete_token( $token );
}
/**
* Check if tokens has card source.
*
* @param PaymentToken[] $tokens The tokens.
* @return bool Whether tokens contains card or not.
*/
public function tokens_contains_card( $tokens ): bool {
foreach ( $tokens as $token ) {
if ( isset( $token->source()->card ) ) {
return true;
}
}
return false;
}
/**
* Check if tokens has PayPal source.
*
* @param PaymentToken[] $tokens The tokens.
* @return bool Whether tokens contains card or not.
*/
public function tokens_contains_paypal( $tokens ): bool {
foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) {
return true;
}
}
return false;
}
/**
* Fetch PaymentToken from PayPal for a user.
*
* @param int $id The user id.
* @return PaymentToken
*/
private function fetch_for_user_id( int $id ): PaymentToken {
$tokens = $this->endpoint->for_user( $id );
$token = current( $tokens );
$token_array = $token->to_array();
update_user_meta( $id, self::USER_META, $token_array );
return $token;
}
}

View file

@ -14,7 +14,7 @@ use Dhii\Modular\Module\ModuleInterface;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use Interop\Container\ServiceProviderInterface;
@ -62,7 +62,7 @@ class SubscriptionModule implements ModuleInterface {
add_action(
'woocommerce_subscription_payment_complete',
function ( $subscription ) use ( $c ) {
$payment_token_repository = $c->get( 'subscription.repository.payment-token' );
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$this->add_payment_token_id( $subscription, $payment_token_repository, $logger );
@ -72,7 +72,7 @@ class SubscriptionModule implements ModuleInterface {
add_filter(
'woocommerce_gateway_description',
function ( $description, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'subscription.repository.payment-token' );
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'subscription.helper' );
@ -85,7 +85,7 @@ class SubscriptionModule implements ModuleInterface {
add_filter(
'woocommerce_credit_card_form_fields',
function ( $default_fields, $id ) use ( $c ) {
$payment_token_repository = $c->get( 'subscription.repository.payment-token' );
$payment_token_repository = $c->get( 'vaulting.repository.payment-token' );
$settings = $c->get( 'wcgateway.settings' );
$subscription_helper = $c->get( 'subscription.helper' );

2
modules/ppcp-vaulting/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
/assets

View file

@ -0,0 +1,12 @@
<?php
/**
* The vaulting module extensions.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
return array();

View file

@ -0,0 +1,16 @@
<?php
/**
* The vaulting module.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new VaultingModule();
};

View file

@ -0,0 +1,23 @@
{
"name": "ppcp-vaulting",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"main": "resources/js/myaccount-payments.js",
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"babel-loader": "^8.1.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.55.1",
"webpack-cli": "^4.8.0"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,41 @@
document.addEventListener(
'DOMContentLoaded',
() => {
jQuery('.ppcp-delete-payment-button').click(async (event) => {
event.preventDefault();
jQuery(this).prop('disabled', true);
const token = event.target.id;
const response = await fetch(
PayPalCommerceGatewayVaulting.delete.endpoint,
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(
{
nonce: PayPalCommerceGatewayVaulting.delete.nonce,
token,
}
)
}
);
const reportError = error => {
alert(error);
}
if (!response.ok) {
try {
const result = await response.json();
reportError(result.data);
} catch (exc) {
console.error(exc);
reportError(response.status);
}
}
window.location.reload();
});
});

View file

@ -0,0 +1,42 @@
<?php
/**
* The vaulting module services.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use WooCommerce\PayPalCommerce\Vaulting\Assets\MyAccountPaymentsAssets;
use WooCommerce\PayPalCommerce\Vaulting\Endpoint\DeletePaymentTokenEndpoint;
return array(
'vaulting.module-url' => static function ( $container ): string {
return plugins_url(
'/modules/ppcp-vaulting/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'vaulting.assets.myaccount-payments' => function( $container ) : MyAccountPaymentsAssets {
return new MyAccountPaymentsAssets(
$container->get( 'vaulting.module-url' )
);
},
'vaulting.payment-tokens-renderer' => static function (): PaymentTokensRenderer {
return new PaymentTokensRenderer();
},
'vaulting.repository.payment-token' => static function ( $container ): PaymentTokenRepository {
$factory = $container->get( 'api.factory.payment-token' );
$endpoint = $container->get( 'api.endpoint.payment-token' );
return new PaymentTokenRepository( $factory, $endpoint );
},
'vaulting.endpoint.delete' => function( $container ) : DeletePaymentTokenEndpoint {
return new DeletePaymentTokenEndpoint(
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,67 @@
<?php
/**
* Register and configure assets for My account PayPal payments page.
*
* @package WooCommerce\PayPalCommerce\Vaulting\Assets
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting\Assets;
use WooCommerce\PayPalCommerce\Vaulting\Endpoint\DeletePaymentTokenEndpoint;
/**
* Class MyAccountPaymentsAssets
*/
class MyAccountPaymentsAssets {
/**
* The URL to the module.
*
* @var string
*/
private $module_url;
/**
* WebhooksStatusPageAssets constructor.
*
* @param string $module_url The URL to the module.
*/
public function __construct(
string $module_url
) {
$this->module_url = untrailingslashit( $module_url );
}
/**
* Enqueues the necessary scripts.
*
* @return void
*/
public function enqueue(): void {
wp_enqueue_script(
'ppcp-vaulting-myaccount-payments',
$this->module_url . '/assets/js/myaccount-payments.js',
array( 'jquery' ),
'1',
true
);
}
/**
* Localize script.
*/
public function localize() {
wp_localize_script(
'ppcp-vaulting-myaccount-payments',
'PayPalCommerceGatewayVaulting',
array(
'delete' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( DeletePaymentTokenEndpoint::ENDPOINT ) ),
'nonce' => wp_create_nonce( DeletePaymentTokenEndpoint::nonce() ),
),
)
);
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* The endpoint for deleting payment tokens.
*
* @package WooCommerce\PayPalCommerce\Vaulting\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
/**
* Class DeletePayment
*/
class DeletePaymentTokenEndpoint {
const ENDPOINT = 'ppc-vaulting-delete';
/**
* The repository.
*
* @var PaymentTokenRepository
*/
protected $repository;
/**
* The request data.
*
* @var RequestData
*/
protected $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* DeletePaymentTokenEndpoint constructor.
*
* @param PaymentTokenRepository $repository The repository.
* @param RequestData $request_data The request data.
* @param LoggerInterface $logger The logger.
*/
public function __construct( PaymentTokenRepository $repository, RequestData $request_data, LoggerInterface $logger ) {
$this->repository = $repository;
$this->request_data = $request_data;
$this->logger = $logger;
}
/**
* Returns the nonce for the endpoint.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request() {
try {
$data = $this->request_data->read_request( $this->nonce() );
$tokens = $this->repository->all_for_user_id( get_current_user_id() );
if ( $tokens ) {
foreach ( $tokens as $token ) {
if ( isset( $data['token'] ) && $token->id() === $data['token'] ) {
if ( $this->repository->delete_token( get_current_user_id(), $token ) ) {
wp_send_json_success();
return true;
}
wp_send_json_error( 'Could not delete payment token.' );
return false;
}
}
}
} catch ( Exception $error ) {
$this->logger->error( 'Failed to delete payment: ' . $error->getMessage() );
wp_send_json_error( $error->getMessage(), 403 );
return false;
}
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* The payment tokens renderer.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
/**
* Class PaymentTokensRendered
*/
class PaymentTokensRenderer {
/**
* Render payment tokens.
*
* @param PaymentToken[] $tokens The tokens.
* @return false|string
*/
public function render( array $tokens ) {
ob_start();
?>
<table class="shop_table shop_table_responsive">
<thead>
<tr>
<th><?php echo esc_html__( 'Payment sources', 'woocommerce-paypal-payments' ); ?></th>
<th></th>
</tr>
</thead>
<tbody>
<?php
foreach ( $tokens as $token ) {
$source = $token->source() ?? null;
if ( $source && isset( $source->card ) ) {
?>
<tr>
<td><?php echo esc_attr( $source->card->brand ) . ' ...' . esc_attr( $source->card->last_digits ); ?></td>
<td>
<a class="ppcp-delete-payment-button" id="<?php echo esc_attr( $token->id() ); ?>" href=""><?php echo esc_html__( 'Delete', 'woocommerce-paypal-payments' ); ?></a>
</td>
</tr>
<?php
}
if ( $source && isset( $source->paypal ) ) {
?>
<tr>
<td><?php echo esc_attr( $source->paypal->payer->email_address ); ?></td>
<td>
<a class="ppcp-delete-payment-button" id="<?php echo esc_attr( $token->id() ); ?>" href=""><?php echo esc_html__( 'Delete', 'woocommerce-paypal-payments' ); ?></a>
</td>
</tr>
<?php
}
?>
<?php
}
?>
</tbody>
</table>
<?php
return ob_get_clean();
}
/**
* Render no payments message.
*
* @return false|string
*/
public function render_no_tokens() {
ob_start();
?>
<div class="woocommerce-Message woocommerce-Message--info woocommerce-info">
<?php echo esc_html__( 'No payments available yet.', 'woocommerce-paypal-payments' ); ?>
</div>
<?php
return ob_get_clean();
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* The vaulting module.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Vaulting\Endpoint\DeletePaymentTokenEndpoint;
/**
* Class StatusReportModule
*/
class VaultingModule implements ModuleInterface {
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*
* @param ContainerInterface $container A services container instance.
*/
public function run( ContainerInterface $container ): void {
add_filter(
'woocommerce_account_menu_items',
function( $menu_links ) {
$menu_links = array_slice( $menu_links, 0, 5, true )
+ array( 'ppcp-paypal-payment-tokens' => 'PayPal payments' )
+ array_slice( $menu_links, 5, null, true );
return $menu_links;
},
40
);
add_action(
'init',
function () {
add_rewrite_endpoint( 'ppcp-paypal-payment-tokens', EP_PAGES );
}
);
add_action(
'woocommerce_account_ppcp-paypal-payment-tokens_endpoint',
function () use ( $container ) {
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
$renderer = $container->get( 'vaulting.payment-tokens-renderer' );
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
if ( $tokens ) {
echo wp_kses_post( $renderer->render( $tokens ) );
} else {
echo wp_kses_post( $renderer->render_no_tokens() );
}
}
);
$asset_loader = $container->get( 'vaulting.assets.myaccount-payments' );
add_action(
'wp_enqueue_scripts',
function () use ( $asset_loader ) {
if ( is_account_page() && $this->is_payments_page() ) {
$asset_loader->enqueue();
$asset_loader->localize();
}
}
);
add_action(
'wc_ajax_' . DeletePaymentTokenEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'vaulting.endpoint.delete' );
assert( $endpoint instanceof DeletePaymentTokenEndpoint );
$endpoint->handle_request();
}
);
}
/**
* {@inheritDoc}
*/
public function getKey() { }
/**
* Check if is payments page.
*
* @return bool Whethen page is payments or not.
*/
private function is_payments_page(): bool {
global $wp;
$request = explode( '/', wp_parse_url( $wp->request, PHP_URL_PATH ) );
if ( end( $request ) === 'ppcp-paypal-payment-tokens' ) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,22 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
'myaccount-payments': path.resolve('./resources/js/myaccount-payments.js'),
},
output: {
path: path.resolve(__dirname, 'assets/'),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
}]
}
};

File diff suppressed because it is too large Load diff

View file

@ -7,11 +7,11 @@
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"babel-loader": "^8.1.0",
"cross-env": "^5.0.1",
"file-loader": "^4.2.0",
"webpack": "^4.42.1",
"webpack-cli": "^3.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0"
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"webpack": "^5.55.0",
"webpack-cli": "^4.8.0"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -5,6 +5,8 @@
* @package WooCommerce\PayPalCommerce\WcGateway
*/
// phpcs:disable WordPress.Security.NonceVerification.Recommended
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway;
@ -29,6 +31,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice;
use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
@ -37,6 +40,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WpOop\TransientCache\CachePoolFactory;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage;
return array(
'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway {
@ -50,6 +54,7 @@ return array(
$state = $container->get( 'onboarding.state' );
$transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
$subscription_helper = $container->get( 'subscription.helper' );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
return new PayPalGateway(
$settings_renderer,
$order_processor,
@ -60,7 +65,8 @@ return array(
$refund_processor,
$state,
$transaction_url_provider,
$subscription_helper
$subscription_helper,
$page_id
);
},
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -74,11 +80,12 @@ return array(
$refund_processor = $container->get( 'wcgateway.processor.refunds' );
$state = $container->get( 'onboarding.state' );
$transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
$payment_token_repository = $container->get( 'subscription.repository.payment-token' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
$purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' );
$order_endpoint = $container->get( 'api.endpoint.order' );
$subscription_helper = $container->get( 'subscription.helper' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new CreditCardGateway(
$settings_renderer,
$order_processor,
@ -94,7 +101,8 @@ return array(
$purchase_unit_factory,
$payer_factory,
$order_endpoint,
$subscription_helper
$subscription_helper,
$logger
);
},
'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways {
@ -102,6 +110,32 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
return new DisableGateways( $session_handler, $settings );
},
'wcgateway.is-wc-payments-page' => static function ( $container ): bool {
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : '';
return 'wc-settings' === $page && 'checkout' === $tab;
},
'wcgateway.is-ppcp-settings-page' => static function ( $container ): bool {
if ( ! $container->get( 'wcgateway.is-wc-payments-page' ) ) {
return false;
}
$section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '';
return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID ), true );
},
'wcgateway.current-ppcp-settings-page-id' => static function ( $container ): string {
if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) {
return '';
}
$section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '';
$ppcp_tab = isset( $_GET[ SectionsRenderer::KEY ] ) ? sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) : '';
return $ppcp_tab ? $ppcp_tab : $section;
},
'wcgateway.settings' => static function ( ContainerInterface $container ): Settings {
return new Settings();
},
@ -110,14 +144,21 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
return new ConnectAdminNotice( $state, $settings );
},
'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): DccWithoutPayPalAdminNotice {
$state = $container->get( 'onboarding.state' );
$settings = $container->get( 'wcgateway.settings' );
$is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' );
$is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' );
return new DccWithoutPayPalAdminNotice( $state, $settings, $is_payments_page, $is_ppcp_settings_page );
},
'wcgateway.notice.authorize-order-action' =>
static function ( $container ): AuthorizeOrderActionNotice {
return new AuthorizeOrderActionNotice();
},
'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer {
return new SectionsRenderer();
return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ) );
},
'wcgateway.settings.status' => static function ( $container ): SettingsStatus {
'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus {
$settings = $container->get( 'wcgateway.settings' );
return new SettingsStatus( $settings );
},
@ -129,6 +170,7 @@ return array(
$messages_apply = $container->get( 'button.helper.messages-apply' );
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
$settings_status = $container->get( 'wcgateway.settings.status' );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
return new SettingsRenderer(
$settings,
$state,
@ -136,7 +178,8 @@ return array(
$dcc_applies,
$messages_apply,
$dcc_product_status,
$settings_status
$settings_status,
$page_id
);
},
'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener {
@ -146,7 +189,8 @@ return array(
$state = $container->get( 'onboarding.state' );
$cache = new Cache( 'ppcp-paypal-bearer' );
$bearer = $container->get( 'api.bearer' );
return new SettingsListener( $settings, $fields, $webhook_registrar, $cache, $state, $bearer );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
return new SettingsListener( $settings, $fields, $webhook_registrar, $cache, $state, $bearer, $page_id );
},
'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor {
@ -660,7 +704,7 @@ return array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'all',
'gateway' => array( 'paypal', 'dcc' ),
),
'logging_enabled' => array(
'title' => __( 'Logging', 'woocommerce-paypal-payments' ),
@ -1806,6 +1850,62 @@ return array(
),
'gateway' => 'dcc',
),
'3d_secure_heading' => array(
'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'description' => wp_kses_post(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'3D Secure benefits cardholders and merchants by providing
an additional layer of verification using Verified by Visa,
MasterCard SecureCode and American Express SafeKey.
%1$sLearn more about 3D Secure.%2$s',
'woocommerce-paypal-payments'
),
'<a
rel="noreferrer noopener"
href="https://woocommerce.com/posts/introducing-strong-customer-authentication-sca/"
>',
'</a>'
)
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(
'dcc',
),
'gateway' => 'dcc',
),
'3d_secure_contingency' => array(
'title' => __( 'Contingency for 3D Secure', 'woocommerce-paypal-payments' ),
'type' => 'select',
'description' => sprintf(
// translators: %1$s and %2$s opening and closing ul tag, %3$s and %4$s opening and closing li tag.
__( '%1$s%3$sNo 3D Secure will cause transactions to be denied if 3D Secure is required by the bank of the cardholder.%4$s%3$sSCA_WHEN_REQUIRED returns a 3D Secure contingency when it is a mandate in the region where you operate.%4$s%3$sSCA_ALWAYS triggers 3D Secure for every transaction, regardless of SCA requirements.%4$s%2$s', 'woocommerce-paypal-payments' ),
'<ul>',
'</ul>',
'<li>',
'</li>'
),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'SCA_WHEN_REQUIRED',
'desc_tip' => true,
'options' => array(
'NO_3D_SECURE' => __( 'No 3D Secure (transaction will be denied if 3D Secure is required)', 'woocommerce-paypal-payments' ),
'SCA_WHEN_REQUIRED' => __( '3D Secure when required', 'woocommerce-paypal-payments' ),
'3D_SECURE' => __( 'Always trigger 3D Secure', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(
'dcc',
),
'gateway' => 'dcc',
),
);
if ( ! defined( 'PPCP_FLAG_SUBSCRIPTION' ) || ! PPCP_FLAG_SUBSCRIPTION ) {
unset( $fields['vault_enabled'] );
@ -1822,15 +1922,6 @@ return array(
unset( $fields['ppcp_disconnect_sandbox'] );
}
/**
* Disable card for UK.
*/
$region = wc_get_base_location();
$country = $region['country'];
if ( 'GB' === $country ) {
unset( $fields['disable_funding']['options']['card'] );
}
/**
* Depending on your store location, some credit cards can't be used.
* Here, we filter them out.

View file

@ -9,13 +9,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
@ -46,6 +47,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
protected $subscription_helper;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* The URL to the module.
*
@ -106,6 +114,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param PayerFactory $payer_factory The payer factory.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -122,7 +131,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
PurchaseUnitFactory $purchase_unit_factory,
PayerFactory $payer_factory,
OrderEndpoint $order_endpoint,
SubscriptionHelper $subscription_helper
SubscriptionHelper $subscription_helper,
LoggerInterface $logger
) {
$this->id = self::ID;
@ -190,6 +200,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->order_endpoint = $order_endpoint;
$this->transaction_url_provider = $transaction_url_provider;
$this->subscription_helper = $subscription_helper;
$this->logger = $logger;
}
/**
@ -197,12 +208,6 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable Credit Card Payments', 'woocommerce-paypal-payments' ),
'default' => 'no',
),
'ppcp' => array(
'type' => 'ppcp',
),
@ -218,6 +223,20 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
remove_action( 'gettext', 'replace_credit_card_cvv_label' );
}
/**
* Renders the settings.
*
* @return string
*/
public function generate_ppcp_html(): string {
ob_start();
$this->settings_renderer->render();
$content = ob_get_contents();
ob_end_clean();
return $content;
}
/**
* Replace WooCommerce credit card field label.
*
@ -314,7 +333,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @return bool
*/
public function is_available() : bool {
return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' );
return $this->is_enabled();
}
@ -349,4 +368,60 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
return parent::get_transaction_url( $order );
}
/**
* Initialize settings for WC.
*
* @return void
*/
public function init_settings() {
parent::init_settings();
// looks like in some cases WC uses this field instead of get_option.
$this->enabled = $this->is_enabled();
}
/**
* Get the option value for WC.
*
* @param string $key The option key.
* @param mixed $empty_value Value when empty.
* @return mixed
*/
public function get_option( $key, $empty_value = null ) {
if ( 'enabled' === $key ) {
return $this->is_enabled();
}
return parent::get_option( $key, $empty_value );
}
/**
* Handle update of WC settings.
*
* @param string $key The option key.
* @param string $value The option value.
* @return bool was anything saved?
*/
public function update_option( $key, $value = '' ) {
$ret = parent::update_option( $key, $value );
if ( 'enabled' === $key ) {
$this->config->set( 'dcc_enabled', 'yes' === $value );
$this->config->persist();
return true;
}
return $ret;
}
/**
* Returns if the gateway is enabled.
*
* @return bool
*/
private function is_enabled(): bool {
return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' );
}
}

View file

@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage;
/**
* Class PayPalGateway
@ -103,6 +104,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/
private $onboarded;
/**
* ID of the current PPCP gateway settings page, or empty if it is not such page.
*
* @var string
*/
protected $page_id;
/**
* PayPalGateway constructor.
*
@ -116,6 +124,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @param State $state The state.
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -127,7 +136,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
RefundProcessor $refund_processor,
State $state,
TransactionUrlProvider $transaction_url_provider,
SubscriptionHelper $subscription_helper
SubscriptionHelper $subscription_helper,
string $page_id
) {
$this->id = self::ID;
@ -139,6 +149,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->page_id = $page_id;
$this->onboarded = $state->current_state() === State::STATE_ONBOARDED;
if ( $this->onboarded ) {
@ -214,7 +225,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
),
);
$should_show_enabled_checkbox = ! $this->is_credit_card_tab() && ( $this->config->has( 'merchant_email' ) && $this->config->get( 'merchant_email' ) );
$should_show_enabled_checkbox = $this->is_paypal_tab() && ( $this->config->has( 'merchant_email' ) && $this->config->get( 'merchant_email' ) );
if ( ! $should_show_enabled_checkbox ) {
unset( $this->form_fields['enabled'] );
}
@ -298,6 +309,9 @@ class PayPalGateway extends \WC_Payment_Gateway {
if ( $this->is_credit_card_tab() ) {
return __( 'PayPal Card Processing', 'woocommerce-paypal-payments' );
}
if ( $this->is_webhooks_tab() ) {
return __( 'Webhooks Status', 'woocommerce-paypal-payments' );
}
if ( $this->is_paypal_tab() ) {
return __( 'PayPal Checkout', 'woocommerce-paypal-payments' );
}
@ -316,6 +330,12 @@ class PayPalGateway extends \WC_Payment_Gateway {
'woocommerce-paypal-payments'
);
}
if ( $this->is_webhooks_tab() ) {
return __(
'Status of the webhooks subscription.',
'woocommerce-paypal-payments'
);
}
return __(
'Accept PayPal, Pay Later and alternative payment types.',
@ -332,11 +352,20 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/
private function is_credit_card_tab() : bool {
return is_admin()
&& isset( $_GET[ SectionsRenderer::KEY ] )
&& CreditCardGateway::ID === sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) );
&& CreditCardGateway::ID === $this->page_id;
}
/**
* Whether we are on the Webhooks Status tab.
*
* @return bool
*/
private function is_webhooks_tab() : bool {
return is_admin()
&& WebhooksStatusPage::ID === $this->page_id;
}
/**
* Whether we are on the PayPal settings tab.
*
@ -345,8 +374,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
private function is_paypal_tab() : bool {
return ! $this->is_credit_card_tab()
&& is_admin()
&& isset( $_GET['section'] )
&& self::ID === sanitize_text_field( wp_unslash( $_GET['section'] ) );
&& self::ID === $this->page_id;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
@ -387,14 +415,18 @@ class PayPalGateway extends \WC_Payment_Gateway {
*
* @param string $key The option key.
* @param string $value The option value.
* @return bool|void
* @return bool was anything saved?
*/
public function update_option( $key, $value = '' ) {
parent::update_option( $key, $value );
$ret = parent::update_option( $key, $value );
if ( 'enabled' === $key ) {
$this->config->set( 'enabled', 'yes' === $value );
$this->config->persist();
return true;
}
return $ret;
}
}

View file

@ -85,7 +85,27 @@ trait ProcessPaymentTrait {
'redirect' => $this->get_return_url( $wc_order ),
);
}
if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'AUTHORIZE' ) {
$this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_status(
'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' )
);
$this->session_handler->destroy_session_data();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
}
$this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() );
} catch ( RuntimeException $error ) {
$this->logger->error( $error->getMessage() );
$this->session_handler->destroy_session_data();
wc_add_notice( $error->getMessage(), 'error' );
return null;

View file

@ -0,0 +1,101 @@
<?php
/**
* Creates the admin message about the DCC gateway being enabled without the PayPal gateway.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Notice
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Notice;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\Onboarding\State;
use Psr\Container\ContainerInterface;
/**
* Creates the admin message about the DCC gateway being enabled without the PayPal gateway.
*/
class DccWithoutPayPalAdminNotice {
/**
* The state.
*
* @var State
*/
private $state;
/**
* The settings.
*
* @var ContainerInterface
*/
private $settings;
/**
* Whether the current page is the WC payment page.
*
* @var bool
*/
private $is_payments_page;
/**
* Whether the current page is the PPCP settings page.
*
* @var bool
*/
private $is_ppcp_settings_page;
/**
* ConnectAdminNotice constructor.
*
* @param State $state The state.
* @param ContainerInterface $settings The settings.
* @param bool $is_payments_page Whether the current page is the WC payment page.
* @param bool $is_ppcp_settings_page Whether the current page is the PPCP settings page.
*/
public function __construct(
State $state,
ContainerInterface $settings,
bool $is_payments_page,
bool $is_ppcp_settings_page
) {
$this->state = $state;
$this->settings = $settings;
$this->is_payments_page = $is_payments_page;
$this->is_ppcp_settings_page = $is_ppcp_settings_page;
}
/**
* Returns the message.
*
* @return Message|null
*/
public function message(): ?Message {
if ( ! $this->should_display() ) {
return null;
}
$message = sprintf(
/* translators: %1$s the gateway name. */
__(
'PayPal Card Processing cannot be used without the PayPal gateway. <a href="%1$s">Enable the PayPal Gateway</a>.',
'woocommerce-paypal-payments'
),
admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' )
);
return new Message( $message, 'warning' );
}
/**
* Whether the message should be displayed.
*
* @return bool
*/
protected function should_display(): bool {
return State::STATE_ONBOARDED === $this->state->current_state()
&& ( $this->is_payments_page || $this->is_ppcp_settings_page )
&& ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) )
&& ( ! $this->settings->has( 'enabled' ) || ! $this->settings->get( 'enabled' ) );
}
}

View file

@ -16,7 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage;
/**
* Class SectionsRenderer
@ -19,15 +20,29 @@ class SectionsRenderer {
const KEY = 'ppcp-tab';
/**
* ID of the current PPCP gateway settings page, or empty if it is not such page.
*
* @var string
*/
protected $page_id;
/**
* SectionsRenderer constructor.
*
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
*/
public function __construct( string $page_id ) {
$this->page_id = $page_id;
}
/**
* Whether the sections tab should be rendered.
*
* @return bool
*/
public function should_render() : bool {
global $current_section;
return PayPalGateway::ID === $current_section;
return ! empty( $this->page_id );
}
/**
@ -38,11 +53,10 @@ class SectionsRenderer {
return;
}
//phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current = ! isset( $_GET[ self::KEY ] ) ? PayPalGateway::ID : sanitize_text_field( wp_unslash( $_GET[ self::KEY ] ) );
$sections = array(
PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ),
CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ),
WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ),
);
echo '<ul class="subsubsub">';
@ -51,7 +65,7 @@ class SectionsRenderer {
foreach ( $sections as $id => $label ) {
$url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&' . self::KEY . '=' . $id );
echo '<li><a href="' . esc_url( $url ) . '" class="' . ( $current === $id ? 'current' : '' ) . '">' . esc_html( $label ) . '</a> ' . ( end( $array_keys ) === $id ? '' : '|' ) . ' </li>';
echo '<li><a href="' . esc_url( $url ) . '" class="' . ( $this->page_id === $id ? 'current' : '' ) . '">' . esc_html( $label ) . '</a> ' . ( end( $array_keys ) === $id ? '' : '|' ) . ' </li>';
}
echo '</ul><br class="clear" />';

View file

@ -23,8 +23,15 @@ use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
*/
class SettingsListener {
use PageMatcherTrait;
const NONCE = 'ppcp-settings';
private const CREDENTIALS_ADDED = 'credentials_added';
private const CREDENTIALS_REMOVED = 'credentials_removed';
private const CREDENTIALS_CHANGED = 'credentials_changed';
private const CREDENTIALS_UNCHANGED = 'credentials_unchanged';
/**
* The Settings.
*
@ -67,6 +74,13 @@ class SettingsListener {
*/
private $bearer;
/**
* ID of the current PPCP gateway settings page, or empty if it is not such page.
*
* @var string
*/
protected $page_id;
/**
* SettingsListener constructor.
*
@ -76,6 +90,7 @@ class SettingsListener {
* @param Cache $cache The Cache.
* @param State $state The state.
* @param Bearer $bearer The bearer.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
*/
public function __construct(
Settings $settings,
@ -83,7 +98,8 @@ class SettingsListener {
WebhookRegistrar $webhook_registrar,
Cache $cache,
State $state,
Bearer $bearer
Bearer $bearer,
string $page_id
) {
$this->settings = $settings;
@ -92,6 +108,7 @@ class SettingsListener {
$this->cache = $cache;
$this->state = $state;
$this->bearer = $bearer;
$this->page_id = $page_id;
}
/**
@ -218,18 +235,46 @@ class SettingsListener {
$settings = $this->read_active_credentials_from_settings( $settings );
if ( ! isset( $_GET[ SectionsRenderer::KEY ] ) || PayPalGateway::ID === $_GET[ SectionsRenderer::KEY ] ) {
$credentials_change_status = null; // Cannot detect on Card Processing page.
if ( PayPalGateway::ID === $this->page_id ) {
$settings['enabled'] = isset( $_POST['woocommerce_ppcp-gateway_enabled'] )
&& 1 === absint( $_POST['woocommerce_ppcp-gateway_enabled'] );
$this->maybe_register_webhooks( $settings );
$credentials_change_status = $this->determine_credentials_change_status( $settings );
}
// phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:enable phpcs:disable WordPress.Security.NonceVerification.Missing
if ( $credentials_change_status ) {
if ( self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) {
$this->settings->set( 'products_dcc_enabled', null );
}
if ( in_array(
$credentials_change_status,
array( self::CREDENTIALS_REMOVED, self::CREDENTIALS_CHANGED ),
true
) ) {
$this->webhook_registrar->unregister();
}
}
foreach ( $settings as $id => $value ) {
$this->settings->set( $id, $value );
}
$this->settings->persist();
if ( $credentials_change_status ) {
if ( in_array(
$credentials_change_status,
array( self::CREDENTIALS_ADDED, self::CREDENTIALS_CHANGED ),
true
) ) {
$this->webhook_registrar->register();
}
}
if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) {
$this->cache->delete( PayPalBearer::CACHE_KEY );
}
@ -265,30 +310,36 @@ class SettingsListener {
}
/**
* Depending on the settings change, we might need to register or unregister the Webhooks at PayPal.
* Checks whether on the credentials changed.
*
* @param array $settings The settings.
*
* @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException If a setting hasn't been found.
* @param array $new_settings New settings.
* @return string One of the CREDENTIALS_ constants.
*/
private function maybe_register_webhooks( array $settings ) {
private function determine_credentials_change_status( array $new_settings ): string {
$current_id = $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : '';
$current_secret = $this->settings->has( 'client_secret' ) ? $this->settings->get( 'client_secret' ) : '';
$new_id = $new_settings['client_id'] ?? '';
$new_secret = $new_settings['client_secret'] ?? '';
if ( ! $this->settings->has( 'client_id' ) && $settings['client_id'] ) {
$this->settings->set( 'products_dcc_enabled', null );
$this->webhook_registrar->register();
$had_credentials = $current_id && $current_secret;
$submitted_credentials = $new_id && $new_secret;
if ( ! $had_credentials && $submitted_credentials ) {
return self::CREDENTIALS_ADDED;
}
if ( $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) ) {
$current_secret = $this->settings->has( 'client_secret' ) ?
$this->settings->get( 'client_secret' ) : '';
if ( $had_credentials ) {
if ( ! $submitted_credentials ) {
return self::CREDENTIALS_REMOVED;
}
if (
$settings['client_id'] !== $this->settings->get( 'client_id' )
|| $settings['client_secret'] !== $current_secret
$current_id !== $new_id
|| $current_secret !== $new_secret
) {
$this->settings->set( 'products_dcc_enabled', null );
$this->webhook_registrar->unregister();
$this->webhook_registrar->register();
return self::CREDENTIALS_CHANGED;
}
}
return self::CREDENTIALS_UNCHANGED;
}
/**
@ -311,20 +362,7 @@ class SettingsListener {
if ( ! in_array( $this->state->current_state(), $config['screens'], true ) ) {
continue;
}
if (
'dcc' === $config['gateway']
&& (
! isset( $_GET[ SectionsRenderer::KEY ] )
|| sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) !== CreditCardGateway::ID
)
) {
continue;
}
if (
'paypal' === $config['gateway']
&& isset( $_GET[ SectionsRenderer::KEY ] )
&& sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) ) !== PayPalGateway::ID
) {
if ( ! $this->field_matches_page( $config, $this->page_id ) ) {
continue;
}
switch ( $config['type'] ) {
@ -406,14 +444,7 @@ class SettingsListener {
* phpcs:disable WordPress.Security.NonceVerification.Missing
* phpcs:disable WordPress.Security.NonceVerification.Recommended
*/
if (
! isset( $_REQUEST['section'] )
|| ! in_array(
sanitize_text_field( wp_unslash( $_REQUEST['section'] ) ),
array( 'ppcp-gateway', 'ppcp-credit-card-gateway' ),
true
)
) {
if ( empty( $this->page_id ) ) {
return false;
}

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use Psr\Container\ContainerInterface;
use Woocommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
/**
@ -23,6 +24,8 @@ use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
*/
class SettingsRenderer {
use PageMatcherTrait;
/**
* The Settings status helper.
*
@ -72,6 +75,13 @@ class SettingsRenderer {
*/
private $dcc_product_status;
/**
* ID of the current PPCP gateway settings page, or empty if it is not such page.
*
* @var string
*/
protected $page_id;
/**
* SettingsRenderer constructor.
*
@ -82,6 +92,7 @@ class SettingsRenderer {
* @param MessagesApply $messages_apply Whether messages can be shown.
* @param DCCProductStatus $dcc_product_status The product status.
* @param SettingsStatus $settings_status The Settings status helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
*/
public function __construct(
ContainerInterface $settings,
@ -89,8 +100,9 @@ class SettingsRenderer {
array $fields,
DccApplies $dcc_applies,
MessagesApply $messages_apply,
DCCProductStatus $dcc_product_status,
SettingsStatus $settings_status
DccProductStatus $dcc_product_status,
SettingsStatus $settings_status,
string $page_id
) {
$this->settings = $settings;
@ -100,6 +112,7 @@ class SettingsRenderer {
$this->messages_apply = $messages_apply;
$this->dcc_product_status = $dcc_product_status;
$this->settings_status = $settings_status;
$this->page_id = $page_id;
}
/**
@ -166,21 +179,7 @@ class SettingsRenderer {
* @return bool Whether is PayPal checkout screen or not.
*/
private function is_paypal_checkout_screen(): bool {
$current_screen = get_current_screen();
//phpcs:disable WordPress.Security.NonceVerification.Recommended
//phpcs:disable WordPress.Security.NonceVerification.Missing
if ( isset( $current_screen->id ) && 'woocommerce_page_wc-settings' === $current_screen->id
&& isset( $_GET['section'] ) && 'ppcp-gateway' === $_GET['section'] ) {
if ( isset( $_GET['ppcp-tab'] ) && 'ppcp-gateway' !== $_GET['ppcp-tab'] ) {
return false;
}
return true;
}
//phpcs:enable
return false;
return PayPalGateway::ID === $this->page_id;
}
/**
@ -312,14 +311,66 @@ class SettingsRenderer {
return $html;
}
/**
* Renders the table row.
*
* @param array $data Values of the row cells.
* @param string $tag HTML tag ('td', 'th').
* @return string
*/
public function render_table_row( array $data, string $tag = 'td' ): string {
$cells = array_map(
function ( $value ) use ( $tag ): string {
return "<$tag>" . (string) $value . "</$tag>";
},
$data
);
return '<tr>' . implode( $cells ) . '</tr>';
}
/**
* Renders the table field.
*
* @param string $field The current field HTML.
* @param string $key The key.
* @param array $config The configuration of the field.
* @param array $value The current value.
*
* @return string HTML.
*/
public function render_table( $field, $key, $config, $value ): string {
if ( 'ppcp-table' !== $config['type'] ) {
return $field;
}
$data = $value['data'];
if ( empty( $data ) ) {
$empty_placeholder = $value['empty_placeholder'] ?? ( $config['empty_placeholder'] ?? null );
if ( $empty_placeholder ) {
return $empty_placeholder;
}
}
$header_row_html = $this->render_table_row( $value['headers'], 'th' );
$data_rows_html = implode(
array_map(
array( $this, 'render_table_row' ),
$data
)
);
return "<table>
$header_row_html
$data_rows_html
</table>";
}
/**
* Renders the settings.
*/
public function render() {
//phpcs:disable WordPress.Security.NonceVerification.Recommended
//phpcs:disable WordPress.Security.NonceVerification.Missing
$is_dcc = isset( $_GET[ SectionsRenderer::KEY ] ) && CreditCardGateway::ID === sanitize_text_field( wp_unslash( $_GET[ SectionsRenderer::KEY ] ) );
$is_dcc = CreditCardGateway::ID === $this->page_id;
//phpcs:enable WordPress.Security.NonceVerification.Recommended
//phpcs:enable WordPress.Security.NonceVerification.Missing
$nonce = wp_create_nonce( SettingsListener::NONCE );
@ -330,10 +381,7 @@ class SettingsRenderer {
if ( ! in_array( $this->state->current_state(), $config['screens'], true ) ) {
continue;
}
if ( $is_dcc && ! in_array( $config['gateway'], array( 'all', 'dcc' ), true ) ) {
continue;
}
if ( ! $is_dcc && ! in_array( $config['gateway'], array( 'all', 'paypal' ), true ) ) {
if ( ! $this->field_matches_page( $config, $this->page_id ) ) {
continue;
}
if (
@ -354,7 +402,7 @@ class SettingsRenderer {
) {
continue;
}
$value = $this->settings->has( $field ) ? $this->settings->get( $field ) : null;
$value = $this->settings->has( $field ) ? $this->settings->get( $field ) : ( isset( $config['value'] ) ? $config['value']() : null );
$key = 'ppcp[' . $field . ']';
$id = 'ppcp-' . $field;
$config['id'] = $id;
@ -399,8 +447,6 @@ class SettingsRenderer {
if ( $this->dcc_applies->for_country_currency() ) {
if ( State::STATE_ONBOARDED > $this->state->current_state() ) {
$this->render_dcc_onboarding_info();
} elseif ( State::STATE_ONBOARDED === $this->state->current_state() && $this->dcc_product_status->dcc_is_active() ) {
$this->render_3d_secure_info();
} elseif ( ! $this->dcc_product_status->dcc_is_active() ) {
$this->render_dcc_not_active_yet();
}
@ -450,45 +496,6 @@ class SettingsRenderer {
<?php
}
/**
* Renders the 3d secure info text.
*/
private function render_3d_secure_info() {
?>
<tr>
<th><?php esc_html_e( '3D Secure', 'woocommerce-paypal-payments' ); ?></th>
<td>
<p>
<?php
/**
* We still need to provide a docs link.
*
* @todo: Provide link to documentation.
*/
echo wp_kses_post(
sprintf(
// translators: %1$s and %2$s is a link tag.
__(
'3D Secure benefits cardholders and merchants by providing
an additional layer of verification using Verified by Visa,
MasterCard SecureCode and American Express SafeKey.
%1$sLearn more about 3D Secure.%2$s',
'woocommerce-paypal-payments'
),
'<a
rel="noreferrer noopener"
href="https://woocommerce.com/posts/introducing-strong-customer-authentication-sca/"
>',
'</a>'
)
);
?>
</p>
</td>
</tr>
<?php
}
/**
* Renders the DCC onboarding info.
*/
@ -547,8 +554,8 @@ class SettingsRenderer {
return false;
}
return $this->is_paypal_checkout_screen() && $this->paypal_vaulting_is_enabled()
|| $this->is_paypal_checkout_screen() && $this->settings_status->pay_later_messaging_is_enabled();
return $this->is_paypal_checkout_screen()
&& ( $this->paypal_vaulting_is_enabled() || $this->settings_status->pay_later_messaging_is_enabled() );
}
}

View file

@ -0,0 +1,49 @@
<?php
/**
* PageMatcherTrait.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Settings
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Woocommerce\PayPalCommerce\WcGateway\Helper\DccProductStatus;
use Woocommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage;
/**
* Class PageMatcherTrait.
*/
trait PageMatcherTrait {
/**
* Checks whether the field config matches the current page (can be rendered here).
*
* @param array $field_config The field config (from wcgateway.settings.fields).
* @param string $current_page_id ID of the current PPCP gateway settings page.
* @return bool
*/
protected function field_matches_page( array $field_config, string $current_page_id ): bool {
$allowed_gateways = $field_config['gateway'];
if ( ! is_array( $allowed_gateways ) ) {
$allowed_gateways = array( $allowed_gateways );
}
$gateway_page_id_map = array(
PayPalGateway::ID => 'paypal',
CreditCardGateway::ID => 'dcc', // TODO: consider using just the gateway ID for PayPal and DCC too.
WebhooksStatusPage::ID => WebhooksStatusPage::ID,
);
return array_key_exists( $current_page_id, $gateway_page_id_map )
&& in_array( $gateway_page_id_map[ $current_page_id ], $allowed_gateways, true );
}
}

View file

@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice;
use WooCommerce\PayPalCommerce\WcGateway\Notice\DccWithoutPayPalAdminNotice;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
@ -54,7 +55,6 @@ class WCGatewayModule implements ModuleInterface {
$this->register_order_functionality( $c );
$this->register_columns( $c );
$this->register_checkout_paypal_address_preset( $c );
$this->ajax_gateway_enabler( $c );
add_action(
'woocommerce_sections_checkout',
@ -82,15 +82,19 @@ class WCGatewayModule implements ModuleInterface {
Repository::NOTICES_FILTER,
static function ( $notices ) use ( $c ): array {
$notice = $c->get( 'wcgateway.notice.connect' );
/**
* The Connect Admin Notice object.
*
* @var ConnectAdminNotice $notice
*/
assert( $notice instanceof ConnectAdminNotice );
$connect_message = $notice->connect_message();
if ( $connect_message ) {
$notices[] = $connect_message;
}
$dcc_without_paypal_notice = $c->get( 'wcgateway.notice.dcc-without-paypal' );
assert( $dcc_without_paypal_notice instanceof DccWithoutPayPalAdminNotice );
$dcc_without_paypal_message = $dcc_without_paypal_notice->message();
if ( $dcc_without_paypal_message ) {
$notices[] = $dcc_without_paypal_message;
}
$authorize_order_action = $c->get( 'wcgateway.notice.authorize-order-action' );
$authorized_message = $authorize_order_action->message();
if ( $authorized_message ) {
@ -98,11 +102,7 @@ class WCGatewayModule implements ModuleInterface {
}
$settings_renderer = $c->get( 'wcgateway.settings.render' );
/**
* The settings renderer.
*
* @var SettingsRenderer $settings_renderer
*/
assert( $settings_renderer instanceof SettingsRenderer );
$messages = $settings_renderer->messages();
$notices = array_merge( $notices, $messages );
@ -145,50 +145,6 @@ class WCGatewayModule implements ModuleInterface {
);
}
/**
* Adds the functionality to listen to the ajax enable gateway switch.
*
* @param ContainerInterface $container The container.
*/
private function ajax_gateway_enabler( ContainerInterface $container ) {
add_action(
'wp_ajax_woocommerce_toggle_gateway_enabled',
static function () use ( $container ) {
if (
! current_user_can( 'manage_woocommerce' )
|| ! check_ajax_referer(
'woocommerce-toggle-payment-gateway-enabled',
'security'
)
|| ! isset( $_POST['gateway_id'] )
) {
return;
}
/**
* The settings.
*
* @var Settings $settings
*/
$settings = $container->get( 'wcgateway.settings' );
$key = PayPalGateway::ID === $_POST['gateway_id'] ? 'enabled' : '';
if ( CreditCardGateway::ID === $_POST['gateway_id'] ) {
$key = 'dcc_enabled';
}
if ( ! $key ) {
return;
}
$enabled = $settings->has( $key ) ? $settings->get( $key ) : false;
if ( ! $enabled ) {
return;
}
$settings->set( $key, false );
$settings->persist();
},
9
);
}
/**
* Registers the payment gateways.
*
@ -202,16 +158,12 @@ class WCGatewayModule implements ModuleInterface {
$methods[] = $container->get( 'wcgateway.paypal-gateway' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
$screen = ! function_exists( 'get_current_screen' ) ? (object) array( 'id' => 'front' ) : get_current_screen();
if ( ! $screen ) {
$screen = (object) array( 'id' => 'front' );
}
/**
* The DCC Applies object.
*
* @var DccApplies $dcc_applies
*/
if ( 'woocommerce_page_wc-settings' !== $screen->id && $dcc_applies->for_country_currency() ) {
if ( $dcc_applies->for_country_currency() ) {
$methods[] = $container->get( 'wcgateway.credit-card-gateway' );
}
return (array) $methods;
@ -258,6 +210,7 @@ class WCGatewayModule implements ModuleInterface {
$field = $renderer->render_password( $field, $key, $args, $value );
$field = $renderer->render_text_input( $field, $key, $args, $value );
$field = $renderer->render_heading( $field, $key, $args, $value );
$field = $renderer->render_table( $field, $key, $args, $value );
return $field;
},
10,

View file

@ -2,7 +2,7 @@ const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'sourcemap',
devtool: 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {

File diff suppressed because it is too large Load diff

2
modules/ppcp-webhooks/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
assets

View file

@ -9,4 +9,61 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
return array();
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage;
return array(
'wcgateway.settings.fields' => static function ( $container, array $fields ): array {
$status_page_fields = array(
'webhooks_list' => array(
'title' => __( 'Subscribed webhooks', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-table',
'screens' => array(
State::STATE_PROGRESSIVE,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => WebhooksStatusPage::ID,
'classes' => array( 'ppcp-webhooks-table' ),
'value' => function () use ( $container ) : array {
return $container->get( 'webhook.status.registered-webhooks-data' );
},
),
'webhooks_resubscribe' => array(
'title' => __( 'Resubscribe webhooks', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => '<button type="button" class="button ppcp-webhooks-resubscribe">' . esc_html__( 'Resubscribe', 'woocommerce-paypal-payments' ) . '</button>',
'screens' => array(
State::STATE_PROGRESSIVE,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => WebhooksStatusPage::ID,
'description' => __( 'Click to remove the current webhook subscription and subscribe again, for example, if the website domain or URL structure changed.', 'woocommerce-paypal-payments' ),
),
);
$is_registered = $container->get( 'webhook.is-registered' );
if ( $is_registered ) {
$status_page_fields = array_merge(
$status_page_fields,
array(
'webhooks_simulate' => array(
'title' => __( 'Webhook simulation', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => '<button type="button" class="button ppcp-webhooks-simulate">' . esc_html__( 'Simulate', 'woocommerce-paypal-payments' ) . '</button>',
'screens' => array(
State::STATE_PROGRESSIVE,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => WebhooksStatusPage::ID,
'description' => __( 'Click to request a sample webhook payload from PayPal, allowing to check that your server can successfully receive webhooks.', 'woocommerce-paypal-payments' ),
),
)
);
}
return array_merge( $fields, $status_page_fields );
},
);

View file

@ -0,0 +1,23 @@
{
"name": "ppcp-webhooks",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"main": "resources/js/status-page.js",
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"babel-loader": "^8.1.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.55.0",
"webpack-cli": "^4.8.0"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,34 @@
.ppcp-webhooks-table, .ppcp-webhooks-status-text {
.error {
color: red;
font-weight: bold;
}
.success {
color: green;
font-weight: bold;
}
}
.ppcp-webhooks-table {
table {
border-collapse: collapse;
th {
border-bottom: 1px solid black;
}
td, th {
padding: 10px 15px;
}
td:first-child {
vertical-align: top;
width: 450px;
}
}
}
.ppcp-webhooks-status-text {
padding-top: 4px;
}

View file

@ -0,0 +1,148 @@
document.addEventListener(
'DOMContentLoaded',
() => {
const resubscribeBtn = jQuery(PayPalCommerceGatewayWebhooksStatus.resubscribe.button);
resubscribeBtn.click(async () => {
resubscribeBtn.prop('disabled', true);
const response = await fetch(
PayPalCommerceGatewayWebhooksStatus.resubscribe.endpoint,
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(
{
nonce: PayPalCommerceGatewayWebhooksStatus.resubscribe.nonce,
}
)
}
);
const reportError = error => {
const msg = PayPalCommerceGatewayWebhooksStatus.resubscribe.failureMessage + ' ' + error;
alert(msg);
}
if (!response.ok) {
try {
const result = await response.json();
reportError(result.data);
} catch (exc) {
console.error(exc);
reportError(response.status);
}
}
window.location.reload();
});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const simulateBtn = jQuery(PayPalCommerceGatewayWebhooksStatus.simulation.start.button);
simulateBtn.click(async () => {
simulateBtn.prop('disabled', true);
try {
const response = await fetch(
PayPalCommerceGatewayWebhooksStatus.simulation.start.endpoint,
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify(
{
nonce: PayPalCommerceGatewayWebhooksStatus.simulation.start.nonce,
}
)
}
);
const reportError = error => {
const msg = PayPalCommerceGatewayWebhooksStatus.simulation.start.failureMessage + ' ' + error;
alert(msg);
};
if (!response.ok) {
try {
const result = await response.json();
reportError(result.data);
} catch (exc) {
console.error(exc);
reportError(response.status);
}
return;
}
const showStatus = html => {
let statusBlock = simulateBtn.siblings('.ppcp-webhooks-status-text');
if (!statusBlock.length) {
statusBlock = jQuery('<div class="ppcp-webhooks-status-text"></div>').insertAfter(simulateBtn);
}
statusBlock.html(html);
};
simulateBtn.siblings('.description').hide();
showStatus(
PayPalCommerceGatewayWebhooksStatus.simulation.state.waitingMessage +
'<span class="spinner is-active" style="float: none;"></span>'
);
const delay = 2000;
const retriesBeforeErrorMessage = 15;
const maxRetries = 30;
for (let i = 0; i < maxRetries; i++) {
await sleep(delay);
const stateResponse = await fetch(
PayPalCommerceGatewayWebhooksStatus.simulation.state.endpoint,
{
method: 'GET',
}
);
try {
const result = await stateResponse.json();
if (!stateResponse.ok || !result.success) {
console.error('Simulation state query failed: ' + result.data);
continue;
}
const state = result.data.state;
if (state === PayPalCommerceGatewayWebhooksStatus.simulation.state.successState) {
showStatus(
'<span class="success">' +
'✔️ ' +
PayPalCommerceGatewayWebhooksStatus.simulation.state.successMessage +
'</span>'
);
return;
}
} catch (exc) {
console.error(exc);
}
if (i === retriesBeforeErrorMessage) {
showStatus(
'<span class="error">' +
PayPalCommerceGatewayWebhooksStatus.simulation.state.tooLongDelayMessage +
'</span>'
);
}
}
} finally {
simulateBtn.prop('disabled', false);
}
});
}
);

View file

@ -9,12 +9,22 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\WcGateway\Assets\WebhooksStatusPageAssets;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderApproved;
use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderCompleted;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureCompleted;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureRefunded;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureReversed;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
return array(
@ -22,24 +32,30 @@ return array(
$factory = $container->get( 'api.factory.webhook' );
$endpoint = $container->get( 'api.endpoint.webhook' );
$rest_endpoint = $container->get( 'webhook.endpoint.controller' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new WebhookRegistrar(
$factory,
$endpoint,
$rest_endpoint
$rest_endpoint,
$logger
);
},
'webhook.endpoint.controller' => function( ContainerInterface $container ) : IncomingWebhookEndpoint {
$webhook_endpoint = $container->get( 'api.endpoint.webhook' );
$webhook_factory = $container->get( 'api.factory.webhook' );
$webhook = $container->get( 'webhook.current' );
$handler = $container->get( 'webhook.endpoint.handler' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$verify_request = ! defined( 'PAYPAL_WEBHOOK_REQUEST_VERIFICATION' ) || PAYPAL_WEBHOOK_REQUEST_VERIFICATION;
$webhook_event_factory = $container->get( 'api.factory.webhook-event' );
$simulation = $container->get( 'webhook.status.simulation' );
return new IncomingWebhookEndpoint(
$webhook_endpoint,
$webhook_factory,
$webhook,
$logger,
$verify_request,
$webhook_event_factory,
$simulation,
... $handler
);
},
@ -55,4 +71,122 @@ return array(
new PaymentCaptureCompleted( $logger, $prefix ),
);
},
'webhook.current' => function( $container ) : ?Webhook {
$data = (array) get_option( WebhookRegistrar::KEY, array() );
if ( empty( $data ) ) {
return null;
}
$factory = $container->get( 'api.factory.webhook' );
assert( $factory instanceof WebhookFactory );
try {
return $factory->from_array( $data );
} catch ( Exception $exception ) {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$logger->error( 'Failed to parse the stored webhook data: ' . $exception->getMessage() );
return null;
}
},
'webhook.is-registered' => function( $container ) : bool {
return $container->get( 'webhook.current' ) !== null;
},
'webhook.status.registered-webhooks' => function( $container ) : array {
$endpoint = $container->get( 'api.endpoint.webhook' );
assert( $endpoint instanceof WebhookEndpoint );
return $endpoint->list();
},
'webhook.status.registered-webhooks-data' => function( $container ) : array {
$empty_placeholder = __( 'No webhooks found.', 'woocommerce-paypal-payments' );
$webhooks = array();
try {
$webhooks = $container->get( 'webhook.status.registered-webhooks' );
} catch ( Exception $exception ) {
$empty_placeholder = sprintf(
'<span class="error">%s</span>',
__( 'Failed to load webhooks.', 'woocommerce-paypal-payments' )
);
}
return array(
'headers' => array(
__( 'URL', 'woocommerce-paypal-payments' ),
__( 'Tracked events', 'woocommerce-paypal-payments' ),
),
'data' => array_map(
function ( Webhook $webhook ): array {
return array(
esc_html( $webhook->url() ),
implode(
',<br/>',
array_map(
'esc_html',
$webhook->humanfriendly_event_names()
)
),
);
},
$webhooks
),
'empty_placeholder' => $empty_placeholder,
);
},
'webhook.status.simulation' => function( $container ) : WebhookSimulation {
$webhook_endpoint = $container->get( 'api.endpoint.webhook' );
$webhook = $container->get( 'webhook.current' );
return new WebhookSimulation(
$webhook_endpoint,
$webhook,
'CHECKOUT.ORDER.APPROVED',
'2.0'
);
},
'webhook.status.assets' => function( $container ) : WebhooksStatusPageAssets {
return new WebhooksStatusPageAssets(
$container->get( 'webhook.module-url' )
);
},
'webhook.endpoint.resubscribe' => static function ( $container ) : ResubscribeEndpoint {
$registrar = $container->get( 'webhook.registrar' );
$request_data = $container->get( 'button.request-data' );
return new ResubscribeEndpoint(
$registrar,
$request_data
);
},
'webhook.endpoint.simulate' => static function ( $container ) : SimulateEndpoint {
$simulation = $container->get( 'webhook.status.simulation' );
$request_data = $container->get( 'button.request-data' );
return new SimulateEndpoint(
$simulation,
$request_data
);
},
'webhook.endpoint.simulation-state' => static function ( $container ) : SimulationStateEndpoint {
$simulation = $container->get( 'webhook.status.simulation' );
return new SimulationStateEndpoint(
$simulation
);
},
'webhook.module-url' => static function ( $container ): string {
return plugins_url(
'/modules/ppcp-webhooks/',
dirname( __FILE__, 3 ) . '/woocommerce-paypal-payments.php'
);
},
);

View file

@ -0,0 +1,79 @@
<?php
/**
* The endpoint for resubscribing webhooks.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
/**
* Class ResubscribeEndpoint
*/
class ResubscribeEndpoint {
const ENDPOINT = 'ppc-webhooks-resubscribe';
/**
* The webhooks registrar.
*
* @var WebhookRegistrar
*/
private $registrar;
/**
* The Request Data helper object.
*
* @var RequestData
*/
private $request_data;
/**
* ResubscribeEndpoint constructor.
*
* @param WebhookRegistrar $registrar The webhooks registrar.
* @param RequestData $request_data The Request Data helper object.
*/
public function __construct( WebhookRegistrar $registrar, RequestData $request_data ) {
$this->registrar = $registrar;
$this->request_data = $request_data;
}
/**
* Returns the nonce for the endpoint.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request() {
try {
// Validate nonce.
$this->request_data->read_request( $this->nonce() );
$this->registrar->unregister();
if ( ! $this->registrar->register() ) {
wp_send_json_error( 'Webhook subscription failed.', 500 );
return false;
}
wp_send_json_success();
return true;
} catch ( Exception $error ) {
wp_send_json_error( $error->getMessage(), 403 );
return false;
}
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* The endpoint for starting webhooks simulation.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
/**
* Class SimulateEndpoint
*/
class SimulateEndpoint {
const ENDPOINT = 'ppc-webhooks-simulate';
/**
* The simulation handler.
*
* @var WebhookSimulation
*/
private $simulation;
/**
* The Request Data helper object.
*
* @var RequestData
*/
private $request_data;
/**
* SimulateEndpoint constructor.
*
* @param WebhookSimulation $simulation The simulation handler.
* @param RequestData $request_data The Request Data helper object.
*/
public function __construct(
WebhookSimulation $simulation,
RequestData $request_data
) {
$this->simulation = $simulation;
$this->request_data = $request_data;
}
/**
* Returns the nonce for the endpoint.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request() {
try {
// Validate nonce.
$this->request_data->read_request( $this->nonce() );
$this->simulation->start();
wp_send_json_success();
return true;
} catch ( Exception $error ) {
wp_send_json_error( $error->getMessage(), 500 );
return false;
}
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* The endpoint for getting the current webhooks simulation state.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Endpoint;
use Exception;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
/**
* Class SimulationStateEndpoint
*/
class SimulationStateEndpoint {
const ENDPOINT = 'ppc-webhooks-simulation-state';
/**
* The simulation handler.
*
* @var WebhookSimulation
*/
private $simulation;
/**
* SimulationStateEndpoint constructor.
*
* @param WebhookSimulation $simulation The simulation handler.
*/
public function __construct(
WebhookSimulation $simulation
) {
$this->simulation = $simulation;
}
/**
* Returns the nonce for the endpoint.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request() {
try {
$state = $this->simulation->get_state();
wp_send_json_success(
array(
'state' => $state,
)
);
return true;
} catch ( Exception $error ) {
wp_send_json_error( $error->getMessage(), 500 );
return false;
}
}
}

View file

@ -9,11 +9,15 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
use phpDocumentor\Reflection\Types\This;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandler;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
/**
* Class IncomingWebhookEndpoint
@ -31,11 +35,11 @@ class IncomingWebhookEndpoint {
private $webhook_endpoint;
/**
* The Webhook Factory.
* Our registered webhook.
*
* @var WebhookFactory
* @var Webhook|null
*/
private $webhook_factory;
private $webhook;
/**
* The Request handlers.
@ -58,28 +62,48 @@ class IncomingWebhookEndpoint {
*/
private $verify_request;
/**
* The webhook event factory.
*
* @var WebhookEventFactory
*/
private $webhook_event_factory;
/**
* The simulation handler.
*
* @var WebhookSimulation
*/
private $simulation;
/**
* IncomingWebhookEndpoint constructor.
*
* @param WebhookEndpoint $webhook_endpoint The webhook endpoint.
* @param WebhookFactory $webhook_factory The webhook factory.
* @param Webhook|null $webhook Our registered webhook.
* @param LoggerInterface $logger The logger.
* @param bool $verify_request Whether requests need to be verified or not.
* @param WebhookEventFactory $webhook_event_factory The webhook event factory.
* @param WebhookSimulation $simulation The simulation handler.
* @param RequestHandler ...$handlers The handlers, which process a request in the end.
*/
public function __construct(
WebhookEndpoint $webhook_endpoint,
WebhookFactory $webhook_factory,
?Webhook $webhook,
LoggerInterface $logger,
bool $verify_request,
WebhookEventFactory $webhook_event_factory,
WebhookSimulation $simulation,
RequestHandler ...$handlers
) {
$this->webhook_endpoint = $webhook_endpoint;
$this->webhook_factory = $webhook_factory;
$this->webhook = $webhook;
$this->handlers = $handlers;
$this->logger = $logger;
$this->verify_request = $verify_request;
$this->webhook_event_factory = $webhook_event_factory;
$this->simulation = $simulation;
}
/**
@ -110,35 +134,34 @@ class IncomingWebhookEndpoint {
/**
* Verifies the current request.
*
* @param \WP_REST_Request $request The request.
*
* @return bool
*/
public function verify_request(): bool {
public function verify_request( \WP_REST_Request $request ): bool {
if ( ! $this->verify_request ) {
return true;
}
if ( ! $this->webhook ) {
$this->logger->error( 'Failed to retrieve stored webhook data.' );
return false;
}
try {
$data = (array) get_option( WebhookRegistrar::KEY, array() );
$webhook = $this->webhook_factory->from_array( $data );
$result = $this->webhook_endpoint->verify_current_request_for_webhook( $webhook );
$event = $this->event_from_request( $request );
if ( $this->simulation->is_simulation_event( $event ) ) {
return true;
}
$result = $this->webhook_endpoint->verify_current_request_for_webhook( $this->webhook );
if ( ! $result ) {
$this->logger->log(
'error',
__( 'Illegit Webhook request detected.', 'woocommerce-paypal-payments' )
);
$this->logger->error( 'Webhook verification failed.' );
}
return $result;
} catch ( RuntimeException $exception ) {
$this->logger->log(
'error',
sprintf(
// translators: %s is the error message.
__(
'Illegit Webhook request detected: %s',
'woocommerce-paypal-payments'
),
$exception->getMessage()
)
);
$this->logger->error( 'Webhook verification failed: ' . $exception->getMessage() );
return false;
}
}
@ -151,6 +174,17 @@ class IncomingWebhookEndpoint {
* @return \WP_REST_Response
*/
public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
$event = $this->event_from_request( $request );
if ( $this->simulation->is_simulation_event( $event ) ) {
$this->logger->info( 'Received simulated webhook.' );
$this->simulation->receive( $event );
return rest_ensure_response(
array(
'success' => true,
)
);
}
foreach ( $this->handlers as $handler ) {
if ( $handler->responsible_for_request( $request ) ) {
@ -211,4 +245,16 @@ class IncomingWebhookEndpoint {
}
return array_unique( $event_types );
}
/**
* Creates WebhookEvent from request data.
*
* @param \WP_REST_Request $request The request with event data.
*
* @return WebhookEvent
* @throws RuntimeException When failed to create.
*/
private function event_from_request( \WP_REST_Request $request ): WebhookEvent {
return $this->webhook_event_factory->from_array( $request->get_params() );
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* Register and configure assets for webhooks status page.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Status\Assets
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
/**
* Class WebhooksStatusPageAssets
*/
class WebhooksStatusPageAssets {
/**
* The URL to the module.
*
* @var string
*/
private $module_url;
/**
* WebhooksStatusPageAssets constructor.
*
* @param string $module_url The URL to the module.
*/
public function __construct(
string $module_url
) {
$this->module_url = untrailingslashit( $module_url );
}
/**
* Registers the scripts and styles.
*
* @return void
*/
public function register(): void {
wp_register_style(
'ppcp-webhooks-status-page-style',
$this->module_url . '/assets/css/status-page.css',
array(),
1
);
wp_register_script(
'ppcp-webhooks-status-page',
$this->module_url . '/assets/js/status-page.js',
array(),
1,
true
);
wp_localize_script(
'ppcp-webhooks-status-page',
'PayPalCommerceGatewayWebhooksStatus',
$this->get_script_data()
);
}
/**
* Returns the data for the script.
*
* @return array
*/
public function get_script_data() {
return array(
'resubscribe' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( ResubscribeEndpoint::ENDPOINT ) ),
'nonce' => wp_create_nonce( ResubscribeEndpoint::nonce() ),
'button' => '.ppcp-webhooks-resubscribe',
'failureMessage' => __( 'Operation failed. Check WooCommerce logs for more details.', 'woocommerce-paypal-payments' ),
),
'simulation' => array(
'start' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( SimulateEndpoint::ENDPOINT ) ),
'nonce' => wp_create_nonce( SimulateEndpoint::nonce() ),
'button' => '.ppcp-webhooks-simulate',
'failureMessage' => __( 'Operation failed. Check WooCommerce logs for more details.', 'woocommerce-paypal-payments' ),
),
'state' => array(
'endpoint' => home_url( \WC_AJAX::get_endpoint( SimulationStateEndpoint::ENDPOINT ) ),
'successState' => WebhookSimulation::STATE_RECEIVED,
'waitingMessage' => __( 'Waiting for the webhook to arrive...', 'woocommerce-paypal-payments' ),
'successMessage' => __( 'The webhook was received successfully.', 'woocommerce-paypal-payments' ),
'tooLongDelayMessage' => __( 'Looks like the webhook cannot be received. Check that your website is accessible from the internet.', 'woocommerce-paypal-payments' ),
),
),
);
}
/**
* Enqueues the necessary scripts.
*
* @return void
*/
public function enqueue(): void {
wp_enqueue_style( 'ppcp-webhooks-status-page-style' );
wp_enqueue_script( 'ppcp-webhooks-status-page' );
}
}

View file

@ -0,0 +1,180 @@
<?php
/**
* Handles the webhook simulation.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Status
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Status;
use Exception;
use UnexpectedValueException;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
/**
* Class WebhookSimulation
*/
class WebhookSimulation {
public const STATE_WAITING = 'waiting';
public const STATE_RECEIVED = 'received';
private const OPTION_ID = 'ppcp-webhook-simulation';
/**
* The webhooks endpoint.
*
* @var WebhookEndpoint
*/
private $webhook_endpoint;
/**
* Our registered webhook.
*
* @var Webhook|null
*/
private $webhook;
/**
* The event type that will be simulated, such as CHECKOUT.ORDER.APPROVED.
*
* @var string
*/
private $event_type;
/**
* The event resource version, such as 2.0.
*
* @var string|null
*/
private $resource_version;
/**
* WebhookSimulation constructor.
*
* @param WebhookEndpoint $webhook_endpoint The webhooks endpoint.
* @param Webhook|null $webhook Our registered webhook.
* @param string $event_type The event type that will be simulated, such as CHECKOUT.ORDER.APPROVED.
* @param string|null $resource_version The event resource version, such as 2.0.
*/
public function __construct(
WebhookEndpoint $webhook_endpoint,
?Webhook $webhook,
string $event_type,
?string $resource_version
) {
$this->webhook_endpoint = $webhook_endpoint;
$this->webhook = $webhook;
$this->event_type = $event_type;
$this->resource_version = $resource_version;
}
/**
* Starts the simulation by sending request to PayPal and saving the simulation data with STATE_WAITING.
*
* @throws Exception If failed to start simulation.
*/
public function start() {
if ( ! $this->webhook ) {
throw new Exception( 'Webhooks not registered' );
}
$event = $this->webhook_endpoint->simulate( $this->webhook, $this->event_type, $this->resource_version );
$this->save(
array(
'id' => $event->id(),
'state' => self::STATE_WAITING,
)
);
}
/**
* Returns true if the given event matches the expected simulation event.
*
* @param WebhookEvent $event The webhook event.
* @return bool
*/
public function is_simulation_event( WebhookEvent $event ): bool {
try {
$data = $this->load();
return isset( $data['id'] ) && $event->id() === $data['id'];
} catch ( Exception $exception ) {
return false;
}
}
/**
* Sets the simulation state to STATE_RECEIVED if the given event matches the expected simulation event.
*
* @param WebhookEvent $event The webhook event.
*
* @return bool
* @throws Exception If failed to save new state.
*/
public function receive( WebhookEvent $event ): bool {
if ( ! $this->is_simulation_event( $event ) ) {
return false;
}
$this->set_state( self::STATE_RECEIVED );
return true;
}
/**
* Returns the current simulation state, one of the STATE_ constants.
*
* @return string
* @throws Exception If failed to load state.
*/
public function get_state(): string {
$data = $this->load();
return $data['state'];
}
/**
* Saves the new state.
*
* @param string $state One of the STATE_ constants.
*
* @throws Exception If failed to load state.
*/
private function set_state( string $state ): void {
$data = $this->load();
$data['state'] = $state;
$this->save( $data );
}
/**
* Saves the simulation data.
*
* @param array $data The simulation data.
*/
private function save( array $data ): void {
update_option( self::OPTION_ID, $data );
}
/**
* Returns the current simulation data.
*
* @return array
* @throws UnexpectedValueException If failed to load.
*/
private function load(): array {
$data = get_option( self::OPTION_ID );
if ( ! $data ) {
throw new UnexpectedValueException( 'Webhook simulation data not found.' );
}
return $data;
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* Status page.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Status
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Status;
/**
* Class WebhooksStatusPage
*/
class WebhooksStatusPage {
const ID = 'ppcp-webhooks-status-page';
}

View file

@ -11,8 +11,15 @@ namespace WooCommerce\PayPalCommerce\Webhooks;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use Exception;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Assets\WebhooksStatusPageAssets;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage;
/**
* Class WebhookModule
@ -32,11 +39,14 @@ class WebhookModule implements ModuleInterface {
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function run( ContainerInterface $container ): void {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
add_action(
'rest_api_init',
static function () use ( $c ) {
$endpoint = $c->get( 'webhook.endpoint.controller' );
static function () use ( $container ) {
$endpoint = $container->get( 'webhook.endpoint.controller' );
/**
* The Incoming Webhook Endpoint.
*
@ -48,8 +58,8 @@ class WebhookModule implements ModuleInterface {
add_action(
WebhookRegistrar::EVENT_HOOK,
static function () use ( $c ) {
$registrar = $c->get( 'webhook.registrar' );
static function () use ( $container ) {
$registrar = $container->get( 'webhook.registrar' );
/**
* The Webhook Registrar.
*
@ -61,8 +71,8 @@ class WebhookModule implements ModuleInterface {
add_action(
'woocommerce_paypal_payments_gateway_deactivate',
static function () use ( $c ) {
$registrar = $c->get( 'webhook.registrar' );
static function () use ( $container ) {
$registrar = $container->get( 'webhook.registrar' );
/**
* The Webhook Registrar.
*
@ -71,6 +81,69 @@ class WebhookModule implements ModuleInterface {
$registrar->unregister();
}
);
add_action(
'wc_ajax_' . ResubscribeEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'webhook.endpoint.resubscribe' );
assert( $endpoint instanceof ResubscribeEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . SimulateEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'webhook.endpoint.simulate' );
assert( $endpoint instanceof SimulateEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . SimulationStateEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'webhook.endpoint.simulation-state' );
assert( $endpoint instanceof SimulationStateEndpoint );
$endpoint->handle_request();
}
);
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
if ( WebhooksStatusPage::ID === $page_id ) {
$GLOBALS['hide_save_button'] = true;
$asset_loader = $container->get( 'webhook.status.assets' );
assert( $asset_loader instanceof WebhooksStatusPageAssets );
add_action(
'init',
array( $asset_loader, 'register' )
);
add_action(
'admin_enqueue_scripts',
array( $asset_loader, 'enqueue' )
);
try {
$webhooks = $container->get( 'webhook.status.registered-webhooks' );
if ( empty( $webhooks ) ) {
$registrar = $container->get( 'webhook.registrar' );
assert( $registrar instanceof WebhookRegistrar );
// Looks like we cannot call rest_url too early.
add_action(
'init',
function () use ( $registrar ) {
$registrar->register();
}
);
}
} catch ( Exception $exception ) {
$logger->error( 'Failed to load webhooks list: ' . $exception->getMessage() );
}
}
}
/**

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
@ -43,22 +44,32 @@ class WebhookRegistrar {
*/
private $rest_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* WebhookRegistrar constructor.
*
* @param WebhookFactory $webhook_factory The Webhook factory.
* @param WebhookEndpoint $endpoint The Webhook endpoint.
* @param IncomingWebhookEndpoint $rest_endpoint The WordPress Rest API endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
WebhookFactory $webhook_factory,
WebhookEndpoint $endpoint,
IncomingWebhookEndpoint $rest_endpoint
IncomingWebhookEndpoint $rest_endpoint,
LoggerInterface $logger
) {
$this->webhook_factory = $webhook_factory;
$this->endpoint = $endpoint;
$this->rest_endpoint = $rest_endpoint;
$this->logger = $logger;
}
/**
@ -81,8 +92,10 @@ class WebhookRegistrar {
self::KEY,
$created->to_array()
);
$this->logger->info( 'Webhooks subscribed.' );
return true;
} catch ( RuntimeException $error ) {
$this->logger->error( 'Failed to subscribe webhooks: ' . $error->getMessage() );
return false;
}
}
@ -101,11 +114,13 @@ class WebhookRegistrar {
$webhook = $this->webhook_factory->from_array( $data );
$success = $this->endpoint->delete( $webhook );
} catch ( RuntimeException $error ) {
$this->logger->error( 'Failed to delete webhooks: ' . $error->getMessage() );
return false;
}
if ( $success ) {
delete_option( self::KEY );
$this->logger->info( 'Webhooks deleted.' );
}
return $success;
}

View file

@ -0,0 +1,36 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
devtool: 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
'status-page': path.resolve('./resources/js/status-page.js'),
'status-page-style': path.resolve('./resources/css/status-page.scss'),
},
output: {
path: path.resolve(__dirname, 'assets/'),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
}
},
{loader:'sass-loader'}
]
}]
}
};

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
"version": "1.5.1",
"version": "1.6.0",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",
@ -8,10 +8,14 @@
"scripts": {
"install:modules:ppcp-button": "cd modules/ppcp-button && yarn install && cd -",
"install:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn install && cd -",
"install:modules": "yarn run install:modules:ppcp-button && yarn run install:modules:ppcp-wc-gateway",
"install:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn install && cd -",
"install:modules:ppcp-vaulting": "cd modules/ppcp-vaulting && yarn install && cd -",
"install:modules": "yarn run install:modules:ppcp-button && yarn run install:modules:ppcp-wc-gateway && yarn run install:modules:ppcp-webhooks && yarn run install:modules:ppcp-vaulting",
"build:modules:ppcp-button": "cd modules/ppcp-button && yarn run build && cd -",
"build:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run build && cd -",
"build:modules": "yarn run build:modules:ppcp-button && yarn build:modules:ppcp-wc-gateway",
"build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build && cd -",
"build:modules:ppcp-vaulting": "cd modules/ppcp-vaulting && yarn run build && cd -",
"build:modules": "yarn run build:modules:ppcp-button && yarn build:modules:ppcp-wc-gateway && yarn build:modules:ppcp-webhooks && yarn build:modules:ppcp-vaulting",
"build:dev": "yarn run install:modules && yarn run build:modules",
"docker:build": "docker-compose build",
@ -24,14 +28,15 @@
"docker:build-js": "docker-compose run --rm build yarn run build:dev",
"docker:composer-update": "docker-compose run --rm composer composer update",
"docker:test": "docker-compose run --rm test vendor/bin/phpunit",
"docker:lint": "docker-compose run --rm test vendor/bin/phpcs --parallel=8 -n -s",
"docker:lint": "docker-compose run --rm test vendor/bin/phpcs --parallel=8 -s",
"docker:fix-lint": "docker-compose run --rm test vendor/bin/phpcbf",
"prebuild": "rm -rf ./vendor",
"build": "composer install --no-dev && npm run build:dev && npm run archive",
"prebuild": "rm -rf ./vendor && find . -name 'node_modules' -type d -maxdepth 3 -exec rm -rf {} +",
"build": "composer install --no-dev && yarn run build:dev && yarn run archive",
"prearchive": "rm -rf $npm_package_name.zip",
"archive": "zip -r $npm_package_name.zip . -x **.git/\\* **node_modules/\\*",
"postarchive": "npm run archive:cleanup && rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name",
"postarchive": "yarn run archive:cleanup && rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name",
"archive:cleanup": "zip -d $npm_package_name.zip .env* docker/\\* docker-compose.yml .editorconfig tests/\\* .github/\\* wordpress_org_assets/\\* \\*.DS_Store README.md .gitattributes .gitignore .travis.yml composer.json composer.lock package.json package-lock.json patchwork.json yarn.lock phpunit.xml.dist .phpunit.result.cache phpcs.xml.dist modules/ppcp-button/.babelrc modules/ppcp-button/package.json modules/ppcp-button/webpack.config.js modules/ppcp-button/yarn.lock vendor/\\*/.idea/\\* vendor/\\*/.gitignore vendor/\\*/.gitattributes vendor/\\*/.travis.yml"
},
"config": {

View file

@ -35,6 +35,8 @@
<file>src</file>
<file>modules</file>
<file>woocommerce-paypal-payments.php</file>
<file>modules.php</file>
<file>bootstrap.php</file>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>

View file

@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell,
Requires at least: 5.3
Tested up to: 5.8
Requires PHP: 7.1
Stable tag: 1.5.1
Stable tag: 1.6.0
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -81,6 +81,20 @@ Follow the steps below to connect the plugin to your PayPal account:
== Changelog ==
= 1.6.0 =
* Add - Webhook status. #246 #273
* Add - Show CC gateway in admin payments list. #236
* Add - Add 3d secure contingency settings. #230
* Add - Improve logging. #252 #275
* Add - Do not send payee email. #231
* Add - Allow customers to see and delete their saved payments in My Account. #274
* Fix - PayPal Payments generates multiple orders. #244
* Fix - Saved credit card does not auto fill. #242
* Fix - Incorrect webhooks registration. #254
* Fix - Disable funding credit cards affecting hosted fields, unset for GB. #249
* Fix - REFUND_CAPTURE_CURRENCY_MISMATCH on multicurrency sites. #225
* Fix - Can't checkout to certain countries with optional postcode. #224
= 1.5.1 =
* Fix - Set 3DS contingencies to "SCA_WHEN_REQUIRED". #178
* Fix - Plugin conflict blocking line item details. #221

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
@ -28,10 +29,12 @@ class PayPalBearerTest extends TestCase
$key = 'key';
$secret = 'secret';
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$settings = Mockery::mock(Settings::class);
$settings->shouldReceive('has')->andReturn(true);
$settings->shouldReceive('get')->andReturn('');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings);
@ -40,7 +43,7 @@ class PayPalBearerTest extends TestCase
->andReturn($host . '/');
expect('wp_remote_get')
->andReturnUsing(
function ($url, $args) use ($json, $key, $secret, $host) {
function ($url, $args) use ($json, $key, $secret, $host, $headers) {
if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') {
return false;
}
@ -53,6 +56,7 @@ class PayPalBearerTest extends TestCase
return [
'body' => $json,
'headers' => $headers
];
}
);
@ -80,10 +84,12 @@ class PayPalBearerTest extends TestCase
$key = 'key';
$secret = 'secret';
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$settings = Mockery::mock(Settings::class);
$settings->shouldReceive('has')->andReturn(true);
$settings->shouldReceive('get')->andReturn('');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings);
@ -92,7 +98,7 @@ class PayPalBearerTest extends TestCase
->andReturn($host . '/');
expect('wp_remote_get')
->andReturnUsing(
function ($url, $args) use ($json, $key, $secret, $host) {
function ($url, $args) use ($json, $key, $secret, $host, $headers) {
if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') {
return false;
}
@ -105,6 +111,7 @@ class PayPalBearerTest extends TestCase
return [
'body' => $json,
'headers' => $headers,
];
}
);
@ -153,9 +160,12 @@ class PayPalBearerTest extends TestCase
$secret = 'secret';
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$settings = Mockery::mock(Settings::class);
$settings->shouldReceive('has')->andReturn(true);
$settings->shouldReceive('get')->andReturn('');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings);
@ -164,7 +174,7 @@ class PayPalBearerTest extends TestCase
->andReturn($host . '/');
expect('wp_remote_get')
->andReturnUsing(
function ($url, $args) use ($json, $key, $secret, $host) {
function ($url, $args) use ($json, $key, $secret, $host, $headers) {
if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') {
return false;
}
@ -177,6 +187,7 @@ class PayPalBearerTest extends TestCase
return [
'body' => $json,
'headers' => $headers,
];
}
);
@ -199,9 +210,12 @@ class PayPalBearerTest extends TestCase
$secret = 'secret';
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$settings = Mockery::mock(Settings::class);
$settings->shouldReceive('has')->andReturn(true);
$settings->shouldReceive('get')->andReturn('');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$bearer = new PayPalBearer($cache, $host, $key, $secret, $logger, $settings);
@ -210,7 +224,7 @@ class PayPalBearerTest extends TestCase
->andReturn($host . '/');
expect('wp_remote_get')
->andReturnUsing(
function ($url, $args) use ($json, $key, $secret, $host) {
function ($url, $args) use ($json, $key, $secret, $host, $headers) {
if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') {
return false;
}
@ -223,6 +237,7 @@ class PayPalBearerTest extends TestCase
return [
'body' => $json,
'headers' => $headers,
];
}
);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -11,6 +12,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
class IdentityTokenTest extends TestCase
{
@ -40,10 +42,18 @@ class IdentityTokenTest extends TestCase
$this->bearer
->expects('bearer')->andReturn($token);
$rawResponse = ['body' => '{"client_token":"abc123", "expires_in":3600}'];
$host = $this->host;
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$this->logger->shouldReceive('debug');
$rawResponse = [
'body' => '{"client_token":"abc123", "expires_in":3600}',
'headers' => $headers,
];
expect('wp_remote_get')
->andReturnUsing(function ($url, $args) use ($rawResponse, $host) {
->andReturnUsing(function ($url, $args) use ($rawResponse, $host, $headers) {
if ($url !== $host . 'v1/identity/generate-token') {
return false;
}
@ -65,6 +75,7 @@ class IdentityTokenTest extends TestCase
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200);
when('wc_print_r')->returnArg();
$result = $this->sut->generate_for_customer(1);
$this->assertInstanceOf(Token::class, $result);
@ -78,9 +89,13 @@ class IdentityTokenTest extends TestCase
$this->bearer
->expects('bearer')->andReturn($token);
expect('wp_remote_get')->andReturn();
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
expect('wp_remote_get')->andReturn(['headers' => $headers,]);
expect('is_wp_error')->andReturn(true);
when('wc_print_r')->returnArg();
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->expectException(RuntimeException::class);
$this->sut->generate_for_customer(1);
@ -94,10 +109,17 @@ class IdentityTokenTest extends TestCase
$this->bearer
->expects('bearer')->andReturn($token);
expect('wp_remote_get')->andReturn(['body' => '',]);
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
expect('wp_remote_get')->andReturn([
'body' => '',
'headers' => $headers,
]);
expect('is_wp_error')->andReturn(false);
expect('wp_remote_retrieve_response_code')->andReturn(500);
when('wc_print_r')->returnArg();
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->expectException(PayPalApiException::class);
$this->sut->generate_for_customer(1);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Hamcrest\Matchers;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
@ -22,13 +23,19 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use Psr\Log\LoggerInterface;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
class OrderEndpointTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
when('wc_print_r')->returnArg();
}
public function testOrderDefault()
{
expect('wp_json_encode')->andReturnUsing('json_encode');
@ -51,10 +58,14 @@ class OrderEndpointTest extends TestCase
$intent = 'CAPTURE';
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
->expects('get_for_order_id')->with($orderId)->andReturn('uniqueRequestId');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$testee = new OrderEndpoint(
$host,
$bearer,
@ -66,7 +77,10 @@ class OrderEndpointTest extends TestCase
$paypalRequestIdRepository
);
$rawResponse = ['body' => '{"is_correct":true}'];
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
expect('wp_remote_get')
->andReturnUsing(function ($url, $args) use ($rawResponse, $host, $orderId) {
if ($url !== $host . 'v2/checkout/orders/' . $orderId) {
@ -103,10 +117,14 @@ class OrderEndpointTest extends TestCase
$intent = 'CAPTURE';
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
->expects('get_for_order_id')->with($orderId)->andReturn('uniqueRequestId');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$testee = new OrderEndpoint(
$host,
$bearer,
@ -118,7 +136,10 @@ class OrderEndpointTest extends TestCase
$paypalRequestIdRepository
);
$rawResponse = ['body' => '{"is_correct":true}'];
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(true);
@ -140,9 +161,15 @@ class OrderEndpointTest extends TestCase
$orderFactory = Mockery::mock(OrderFactory::class);
$patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class);
$intent = 'CAPTURE';
$rawResponse = ['body' => '{"some_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"some_error":true}',
'headers' => $headers,
];
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -176,8 +203,12 @@ class OrderEndpointTest extends TestCase
$orderToCapture = Mockery::mock(Order::class);
$orderToCapture->expects('status')->andReturn($orderToCaptureStatus);
$orderToCapture->expects('id')->andReturn($orderId);
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$expectedOrder = Mockery::mock(Order::class);
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
@ -202,6 +233,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -307,6 +339,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -321,8 +354,12 @@ class OrderEndpointTest extends TestCase
$applicationContextRepository,
$paypalRequestIdRepository
);
$rawResponse = ['body' => '{"is_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_error":true}',
'headers' => $headers,
];
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(true);
$this->expectException(RuntimeException::class);
@ -351,6 +388,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -366,7 +404,12 @@ class OrderEndpointTest extends TestCase
$paypalRequestIdRepository
);
$rawResponse = ['body' => '{"some_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"some_error":true}',
'headers' => $headers
];
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500);
@ -396,6 +439,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -415,8 +459,12 @@ class OrderEndpointTest extends TestCase
)->makePartial();
$orderToExpect = Mockery::mock(Order::class);
$testee->expects('order')->with($orderId)->andReturn($orderToExpect);
$rawResponse = ['body' => '{"some_error": "' . ErrorResponse::ORDER_ALREADY_CAPTURED . '"}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"some_error": "' . ErrorResponse::ORDER_ALREADY_CAPTURED . '"}',
'headers' => $headers,
];
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500);
@ -436,8 +484,12 @@ class OrderEndpointTest extends TestCase
->shouldReceive('purchase_units')
->andReturn([]);
$orderToCompare = Mockery::mock(Order::class);
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$expectedOrder = Mockery::mock(Order::class);
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
@ -464,6 +516,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -533,8 +586,12 @@ class OrderEndpointTest extends TestCase
->shouldReceive('purchase_units')
->andReturn([]);
$orderToCompare = Mockery::mock(Order::class);
$rawResponse = ['body' => '{"has_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"has_error":true}',
'headers' => $headers,
];
$expectedOrder = Mockery::mock(Order::class);
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
@ -561,6 +618,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
$paypalRequestIdRepository
@ -624,8 +682,12 @@ class OrderEndpointTest extends TestCase
->shouldReceive('purchase_units')
->andReturn([]);
$orderToCompare = Mockery::mock(Order::class);
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$expectedOrder = Mockery::mock(Order::class);
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
@ -652,6 +714,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class);
@ -748,7 +811,12 @@ class OrderEndpointTest extends TestCase
public function testCreateForPurchaseUnitsDefault()
{
expect('wp_json_encode')->andReturnUsing('json_encode');
$rawResponse = ['body' => '{"success":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"success":true}',
'headers' => $headers,
];
$host = 'https://example.com/';
$bearer = Mockery::mock(Bearer::class);
$token = Mockery::mock(Token::class);
@ -772,6 +840,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$applicationContext = Mockery::mock(ApplicationContext::class);
$applicationContext
->expects('to_array')
@ -844,7 +913,12 @@ class OrderEndpointTest extends TestCase
public function testCreateForPurchaseUnitsWithPayer()
{
expect('wp_json_encode')->andReturnUsing('json_encode');
$rawResponse = ['body' => '{"success":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"success":true}',
'headers' => $headers,
];
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
$token
@ -868,6 +942,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$applicationContext = Mockery::mock(ApplicationContext::class);
$applicationContext
->expects('to_array')
@ -928,7 +1003,12 @@ class OrderEndpointTest extends TestCase
public function testCreateForPurchaseUnitsIsWpError()
{
expect('wp_json_encode')->andReturnUsing('json_encode');
$rawResponse = ['body' => '{"success":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"success":true}',
'headers' => $headers,
];
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
$token
@ -943,6 +1023,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContext = Mockery::mock(ApplicationContext::class);
$applicationContext
->expects('to_array')
@ -1006,7 +1087,12 @@ class OrderEndpointTest extends TestCase
public function testCreateForPurchaseUnitsIsNot201()
{
expect('wp_json_encode')->andReturnUsing('json_encode');
$rawResponse = ['body' => '{"has_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"has_error":true}',
'headers' => $headers,
];
$host = 'https://example.com/';
$token = Mockery::mock(Token::class);
$token
@ -1021,6 +1107,7 @@ class OrderEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$applicationContext = Mockery::mock(ApplicationContext::class);
$applicationContext
->expects('to_array')

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
@ -48,7 +49,12 @@ class PaymentTokenEndpointTest extends TestCase
{
$id = 1;
$token = Mockery::mock(Token::class);
$rawResponse = ['body' => '{"payment_tokens":[{"id": "123abc"}]}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"payment_tokens":[{"id": "123abc"}]}',
'headers' => $headers,
];
$paymentToken = Mockery::mock(PaymentToken::class);
$paymentToken->shouldReceive('id')
->andReturn('foo');
@ -65,6 +71,8 @@ class PaymentTokenEndpointTest extends TestCase
$this->factory->shouldReceive('from_paypal_response')
->andReturn($paymentToken);
$this->logger->shouldReceive('debug');
$result = $this->sut->for_user($id);
$this->assertInstanceOf(PaymentToken::class, $result[0]);
@ -74,7 +82,9 @@ class PaymentTokenEndpointTest extends TestCase
{
$id = 1;
$token = Mockery::mock(Token::class);
$rawResponse = [];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = ['headers' => $headers,];
$this->bearer->shouldReceive('bearer')
->andReturn($token);
$token->shouldReceive('token')
@ -84,6 +94,7 @@ class PaymentTokenEndpointTest extends TestCase
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(true);
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->expectException(RuntimeException::class);
$this->sut->for_user($id);
@ -93,7 +104,12 @@ class PaymentTokenEndpointTest extends TestCase
{
$id = 1;
$token = Mockery::mock(Token::class);
$rawResponse = ['body' => '{"some_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"some_error":true}',
'headers' => $headers,
];
$this->bearer->shouldReceive('bearer')
->andReturn($token);
$token->shouldReceive('token')
@ -105,35 +121,15 @@ class PaymentTokenEndpointTest extends TestCase
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500);
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->expectException(PayPalApiException::class);
$this->sut->for_user($id);
}
public function testForUserFailBecauseEmptyTokens()
{
$id = 1;
$token = Mockery::mock(Token::class);
$rawResponse = ['body' => '{"payment_tokens":[]}'];
$this->bearer->shouldReceive('bearer')
->andReturn($token);
$token->shouldReceive('token')
->andReturn('bearer');
$this->ensureRequestForUser($rawResponse, $id);
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200);
$this->logger->shouldReceive('log');
$this->expectException(RuntimeException::class);
$this->sut->for_user($id);
}
public function testDeleteToken()
{
$paymentToken = $paymentToken = Mockery::mock(PaymentToken::class);
$paymentToken = Mockery::mock(PaymentToken::class);
$paymentToken->shouldReceive('id')
->andReturn('foo');
$token = Mockery::mock(Token::class);
@ -142,9 +138,14 @@ class PaymentTokenEndpointTest extends TestCase
$token->shouldReceive('token')
->andReturn('bearer');
expect('wp_remote_get')->andReturn();
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
expect('wp_remote_get')->andReturn([
'headers' => $headers,
]);
expect('is_wp_error')->andReturn(false);
expect('wp_remote_retrieve_response_code')->andReturn(204);
$this->logger->shouldReceive('debug');
$this->sut->delete_token($paymentToken);
}
@ -160,9 +161,14 @@ class PaymentTokenEndpointTest extends TestCase
$token->shouldReceive('token')
->andReturn('bearer');
expect('wp_remote_get')->andReturn();
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
expect('wp_remote_get')->andReturn([
'headers' => $headers,
]);
expect('is_wp_error')->andReturn(true);
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->expectException(RuntimeException::class);
$this->sut->delete_token($paymentToken);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ErrorResponseCollection;
@ -40,8 +41,14 @@ class PaymentsEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$testee = new PaymentsEndpoint(
$host,
@ -88,8 +95,14 @@ class PaymentsEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$testee = new PaymentsEndpoint(
$host,
@ -119,10 +132,16 @@ class PaymentsEndpointTest extends TestCase
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$rawResponse = ['body' => '{"some_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"some_error":true}',
'headers' => $headers,
];
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$testee = new PaymentsEndpoint(
$host,
@ -161,8 +180,14 @@ class PaymentsEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$testee = new PaymentsEndpoint(
$host,
@ -212,8 +237,14 @@ class PaymentsEndpointTest extends TestCase
$logger = Mockery::mock(LoggerInterface::class);
$logger->expects('log');
$logger->expects('debug');
$rawResponse = ['body' => '{"is_correct":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
$testee = new PaymentsEndpoint(
$host,
@ -243,10 +274,16 @@ class PaymentsEndpointTest extends TestCase
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$rawResponse = ['body' => '{"some_error":true}'];
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"some_error":true}',
'headers' => $headers,
];
$logger = Mockery::mock(LoggerInterface::class);
$logger->expects('log');
$logger->expects('debug');
$testee = new PaymentsEndpoint(
$host,

View file

@ -8,6 +8,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use function Brain\Monkey\Functions\expect;
class ChangeCartEndpointTest extends TestCase
@ -68,7 +69,8 @@ class ChangeCartEndpointTest extends TestCase
$shipping,
$requestData,
$cartRepository,
$dataStore
$dataStore,
new NullLogger()
);
expect('wp_send_json_success')

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use function Brain\Monkey\Functions\expect;
class CreateOrderEndpointTest extends TestCase
@ -32,7 +33,7 @@ class CreateOrderEndpointTest extends TestCase
{
list($payer_factory, $testee) = $this->mockTestee();
$method = $this->getProtectedMethodAccessibleReflection(CreateOrderEndpoint::class, 'payer');
$method = $this->makePrivateMethod(CreateOrderEndpoint::class, 'payer');
$dataString = wp_json_encode($expectedResult['payer']);
$dataObj = json_decode(wp_json_encode($expectedResult['payer']));
@ -160,7 +161,8 @@ class CreateOrderEndpointTest extends TestCase
$payer_factory,
$session_handler,
$settings,
$early_order_handler
$early_order_handler,
new NullLogger()
);
return array($payer_factory, $testee);
}
@ -173,7 +175,7 @@ class CreateOrderEndpointTest extends TestCase
* @return \ReflectionMethod
* @throws \ReflectionException
*/
protected function getProtectedMethodAccessibleReflection($class, $method)
protected function makePrivateMethod($class, $method)
{
$reflector = new ReflectionClass($class);
$method = $reflector->getMethod($method);

View file

@ -8,6 +8,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use function Brain\Monkey\Functions\when;
use function Brain\Monkey\Functions\expect;
@ -22,7 +23,7 @@ class DataClientIdEndpointTest extends TestCase
parent::setUp();
$this->requestData = Mockery::mock(RequestData::class);
$this->identityToken = Mockery::mock(IdentityToken::class);
$this->sut = new DataClientIdEndpoint($this->requestData, $this->identityToken);
$this->sut = new DataClientIdEndpoint($this->requestData, $this->identityToken, new NullLogger());
}
public function testHandleRequestSuccess()

View file

@ -4,12 +4,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSourceCard;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery\Mock;
use function Brain\Monkey\Functions\when;
class ThreeDSecureTest extends TestCase
{
@ -25,13 +27,18 @@ class ThreeDSecureTest extends TestCase
$result->shouldReceive('liability_shift')->andReturn($liabilityShift);
$result->shouldReceive('authentication_result')->andReturn($authenticationResult);
$result->shouldReceive('enrollment_status')->andReturn($enrollment);
$result->shouldReceive('to_array')->andReturn(['foo' => 'bar',]);
$card = \Mockery::mock(PaymentSourceCard::class);
$card->shouldReceive('authentication_result')->andReturn($result);
$source = \Mockery::mock(PaymentSource::class);
$source->shouldReceive('card')->andReturn($card);
$order = \Mockery::mock(Order::class);
$order->shouldReceive('payment_source')->andReturn($source);
$testee = new ThreeDSecure();
$logger = \Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('info');
when('wc_print_r')->justReturn();
$testee = new ThreeDSecure($logger);
$result = $testee->proceed_with_order($order);
$this->assertEquals($expected, $result);
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce;
use Dhii\Container\CompositeCachingServiceProvider;
use Dhii\Container\DelegatingContainer;
use Dhii\Container\ServiceProvider;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Psr\Container\ContainerInterface;
use function Brain\Monkey\Functions\when;
class ModularTestCase extends TestCase
{
use MockeryPHPUnitIntegration;
public function setUp(): void
{
parent::setUp();
when('get_option')->justReturn(null);
when('plugins_url')->returnArg();
when('plugin_dir_path')->alias(function ($file) { return trailingslashit(dirname($file)); });
when('get_current_blog_id')->justReturn(42);
when('get_site_url')->justReturn('example.com');
when('get_bloginfo')->justReturn('My Shop');
when('wc_get_base_location')->justReturn(['country' => 'US']);
when('get_woocommerce_currency')->justReturn('USD');
when('WC')->justReturn((object) [
'session' => null,
]);
global $wpdb;
$wpdb = \Mockery::mock(\stdClass::class);
$wpdb->shouldReceive('get_var')->andReturn(null);
$wpdb->shouldReceive('prepare')->andReturn(null);
$wpdb->posts = '';
$wpdb->postmeta = '';
!defined('PAYPAL_API_URL') && define('PAYPAL_API_URL', 'https://api.paypal.com');
!defined('PAYPAL_SANDBOX_API_URL') && define('PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com');
!defined('PAYPAL_INTEGRATION_DATE') && define('PAYPAL_INTEGRATION_DATE', '2020-10-15');
!defined('PPCP_FLAG_SUBSCRIPTION') && define('PPCP_FLAG_SUBSCRIPTION', true);
!defined('CONNECT_WOO_CLIENT_ID') && define('CONNECT_WOO_CLIENT_ID', 'woo-id');
!defined('CONNECT_WOO_SANDBOX_CLIENT_ID') && define('CONNECT_WOO_SANDBOX_CLIENT_ID', 'woo-id2');
!defined('CONNECT_WOO_MERCHANT_ID') && define('CONNECT_WOO_MERCHANT_ID', 'merchant-id');
!defined('CONNECT_WOO_SANDBOX_MERCHANT_ID') && define('CONNECT_WOO_SANDBOX_MERCHANT_ID', 'merchant-id2');
!defined('CONNECT_WOO_URL') && define('CONNECT_WOO_URL', 'https://connect.woocommerce.com/ppc');
!defined('CONNECT_WOO_SANDBOX_URL') && define('CONNECT_WOO_SANDBOX_URL', 'https://connect.woocommerce.com/ppcsandbox');
}
/**
* @param array<string, callable> $overriddenServices
* @return ContainerInterface
*/
protected function bootstrapModule(array $overriddenServices = []): ContainerInterface
{
$overridingContainer = new DelegatingContainer(new CompositeCachingServiceProvider([
new ServiceProvider($overriddenServices, []),
]));
$rootDir = ROOT_DIR;
$bootstrap = require ("$rootDir/bootstrap.php");
$appContainer = $bootstrap($rootDir, $overridingContainer);
return $appContainer;
}
}

View file

@ -11,10 +11,10 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
class RenewalHandlerTest extends TestCase
{

View file

@ -10,6 +10,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
@ -90,4 +91,72 @@ class PaymentTokenRepositoryTest extends TestCase
$this->sut->delete_token($id, $paymentToken);
}
public function testAllForUserId()
{
$id = 1;
$tokens = [];
$this->endpoint->shouldReceive('for_user')
->with($id)
->andReturn($tokens);
expect('update_user_meta')->with($id, $this->sut::USER_META, $tokens);
$result = $this->sut->all_for_user_id($id);
$this->assertSame($tokens, $result);
}
public function test_AllForUserIdReturnsEmptyArrayIfGettingTokenFails()
{
$id = 1;
$tokens = [];
$this->endpoint
->expects('for_user')
->with($id)
->andThrow(RuntimeException::class);
$result = $this->sut->all_for_user_id($id);
$this->assertSame($tokens, $result);
}
public function testTokensContainCardReturnsTrue()
{
$source = new \stdClass();
$card = new \stdClass();
$source->card = $card;
$token = Mockery::mock(PaymentToken::class);
$tokens = [$token];
$token->shouldReceive('source')->andReturn($source);
$this->assertTrue($this->sut->tokens_contains_card($tokens));
}
public function testTokensContainCardReturnsFalse()
{
$tokens = [];
$this->assertFalse($this->sut->tokens_contains_card($tokens));
}
public function testTokensContainPayPalReturnsTrue()
{
$source = new \stdClass();
$paypal = new \stdClass();
$source->paypal = $paypal;
$token = Mockery::mock(PaymentToken::class);
$tokens = [$token];
$token->shouldReceive('source')->andReturn($source);
$this->assertTrue($this->sut->tokens_contains_paypal($tokens));
}
public function testTokensContainPayPalReturnsFalse()
{
$tokens = [];
$this->assertFalse($this->sut->tokens_contains_paypal($tokens));
}
}

View file

@ -3,9 +3,9 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce;
use function Brain\Monkey\Functions\when;
use function Brain\Monkey\setUp;
use function Brain\Monkey\tearDown;
use function Brain\Monkey\Functions\expect;
use Mockery;
class TestCase extends \PHPUnit\Framework\TestCase
@ -13,9 +13,19 @@ class TestCase extends \PHPUnit\Framework\TestCase
public function setUp(): void
{
parent::setUp();
expect('__')->andReturnUsing(function (string $text) {
return $text;
});
when('__')->returnArg();
when('_x')->returnArg();
when('esc_url')->returnArg();
when('esc_attr')->returnArg();
when('esc_attr__')->returnArg();
when('esc_html')->returnArg();
when('esc_html__')->returnArg();
when('esc_textarea')->returnArg();
when('sanitize_text_field')->returnArg();
when('wp_kses_post')->returnArg();
when('wp_unslash')->returnArg();
setUp();
}

View file

@ -68,7 +68,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$state,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
expect('wc_get_order')
@ -116,7 +117,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$state,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
expect('wc_get_order')
@ -181,7 +183,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$state,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
expect('wc_get_order')
@ -254,7 +257,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$state,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
$this->assertTrue($testee->capture_authorized_payment($wcOrder));
@ -311,7 +315,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$state,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
$this->assertTrue($testee->capture_authorized_payment($wcOrder));
@ -362,7 +367,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$state,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
$this->assertFalse($testee->capture_authorized_payment($wcOrder));
@ -401,7 +407,8 @@ class WcGatewayTest extends TestCase
$refundProcessor,
$onboardingState,
$transactionUrlProvider,
$subscriptionHelper
$subscriptionHelper,
PayPalGateway::ID
);
$this->assertSame($needSetup, $testee->needs_setup());

View file

@ -4,24 +4,39 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ModularTestCase;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use function Brain\Monkey\Functions\when;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
class SettingsListenerTest extends TestCase
class SettingsListenerTest extends ModularTestCase
{
use MockeryPHPUnitIntegration;
private $appContainer;
public function setUp(): void
{
parent::setUp();
$this->appContainer = $this->bootstrapModule();
}
public function testListen()
{
$settings = Mockery::mock(Settings::class);
$setting_fields = [];
$settings->shouldReceive('set');
$setting_fields = $this->appContainer->get('wcgateway.settings.fields');
$webhook_registrar = Mockery::mock(WebhookRegistrar::class);
$webhook_registrar->shouldReceive('unregister')->andReturnTrue();
$webhook_registrar->shouldReceive('register')->andReturnTrue();
$cache = Mockery::mock(Cache::class);
$state = Mockery::mock(State::class);
$state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$bearer = Mockery::mock(Bearer::class);
$testee = new SettingsListener(
@ -30,18 +45,17 @@ class SettingsListenerTest extends TestCase
$webhook_registrar,
$cache,
$state,
$bearer
$bearer,
PayPalGateway::ID
);
$_REQUEST['section'] = 'ppcp-gateway';
$_GET['section'] = PayPalGateway::ID;
$_POST['ppcp-nonce'] = 'foo';
$_POST['ppcp'] = [
'client_id' => 'client_id',
];
$_GET['ppcp-tab'] = 'just-a-tab';
$_GET['ppcp-tab'] = PayPalGateway::ID;
when('sanitize_text_field')->justReturn('ppcp-gateway');
when('wp_unslash')->justReturn('ppcp-gateway');
when('current_user_can')->justReturn(true);
when('wp_verify_nonce')->justReturn(true);

View file

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Status;
use Exception;
use Mockery;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\when;
class WebhookSimulationTest extends TestCase
{
private $webhook_endpoint;
private $webhook;
private $event_type = 'CHECKOUT.ORDER.APPROVED';
private $event_version = '2.0';
private $sut;
private $storage;
private $event_id = '123';
private $event;
public function setUp(): void
{
parent::setUp();
$this->webhook_endpoint = Mockery::mock(WebhookEndpoint::class);
$this->webhook = new Webhook('https://example.com', []);
$this->sut = new WebhookSimulation($this->webhook_endpoint, $this->webhook, $this->event_type, $this->event_version);
when('update_option')->alias(function ($key, $value) {
$this->storage[$key] = $value;
});
when('get_option')->alias(function ($key, $default = false) {
return $this->storage[$key] ?? $default;
});
$this->event = $this->createEvent($this->event_id);
}
public function testSimulation()
{
$this->webhook_endpoint
->expects('simulate')
->with($this->webhook, $this->event_type, $this->event_version)
->andReturn($this->event);
$this->sut->start();
self::assertTrue($this->sut->is_simulation_event($this->createEvent($this->event_id)));
self::assertFalse($this->sut->is_simulation_event($this->createEvent('456')));
self::assertFalse($this->sut->receive($this->createEvent('456')));
self::assertEquals(WebhookSimulation::STATE_WAITING, $this->sut->get_state());
self::assertTrue($this->sut->receive($this->createEvent($this->event_id)));
self::assertEquals(WebhookSimulation::STATE_RECEIVED, $this->sut->get_state());
}
public function testIsSimulationNeverThrows()
{
self::assertFalse($this->sut->is_simulation_event($this->createEvent($this->event_id)));
}
public function testSimulationWhenNoWebhook()
{
$this->sut = new WebhookSimulation($this->webhook_endpoint, null, $this->event_type, $this->event_version);
self::assertFalse($this->sut->is_simulation_event($this->createEvent($this->event_id)));
$this->expectException(Exception::class);
$this->sut->start();
}
private function createEvent(string $id): WebhookEvent
{
return new WebhookEvent($id, null, '', '', $this->event_type, '', '', (object) []);
}
}

Some files were not shown because too many files have changed in this diff Show more