diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 0fbbb59dc..7cf82b61c 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient; use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; @@ -138,6 +139,13 @@ return array( $container->get( 'api.repository.customer' ) ); }, + 'api.endpoint.payment-tokens' => static function( ContainerInterface $container ) : PaymentTokensEndpoint { + return new PaymentTokensEndpoint( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint { return new WebhookEndpoint( diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php new file mode 100644 index 000000000..bfe05d1b5 --- /dev/null +++ b/modules/ppcp-api-client/src/Endpoint/PaymentTokensEndpoint.php @@ -0,0 +1,95 @@ +host = $host; + $this->bearer = $bearer; + $this->logger = $logger; + } + + /** + * Deletes a payment token with the given id. + * + * @param string $id Payment token id. + * + * @return void + * + * @throws RuntimeException When something went wrong with the request. + * @throws PayPalApiException When something went wrong deleting the payment token. + */ + public function delete( string $id ): void { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens/' . $id; + $args = array( + 'method' => 'DELETE', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + ); + + $response = $this->request( $url, $args ); + if ( $response instanceof WP_Error ) { + throw new RuntimeException( $response->get_error_message() ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 204 !== $status_code ) { + throw new PayPalApiException( $json, $status_code ); + } + } +} diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index b898b73c3..b437e7285 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use WC_Order; use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; @@ -161,7 +162,7 @@ class SavePaymentMethodsModule implements ModuleInterface { add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' ); - add_filter('woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', '__return_false'); + add_filter( 'woocommerce_paypal_payments_subscription_renewal_return_before_create_order_without_token', '__return_false' ); add_action( 'wp_enqueue_scripts', @@ -253,5 +254,27 @@ class SavePaymentMethodsModule implements ModuleInterface { $endpoint->handle_request(); } ); + + add_action( + 'woocommerce_paypal_payments_before_delete_payment_token', + function( string $token_id ) use ( $c ) { + try { + $endpoint = $c->get( 'api.endpoint.payment-tokens' ); + assert( $endpoint instanceof PaymentTokensEndpoint ); + + $endpoint->delete( $token_id ); + } catch ( RuntimeException $exception ) { + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger->error( $error ); + } + } + ); } } diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index bed884f04..c9e7bd40a 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -137,6 +137,8 @@ class VaultingModule implements ModuleInterface { } try { + do_action( 'woocommerce_paypal_payments_before_delete_payment_token', $token->get_token() ); + $payment_token_endpoint = $container->get( 'api.endpoint.payment-token' ); $payment_token_endpoint->delete_token_by_id( $token->get_token() ); } catch ( RuntimeException $exception ) { diff --git a/tests/Playwright/tests/save-payment-methods.spec.js b/tests/Playwright/tests/save-payment-methods.spec.js new file mode 100644 index 000000000..4648eebca --- /dev/null +++ b/tests/Playwright/tests/save-payment-methods.spec.js @@ -0,0 +1,42 @@ +const {test, expect} = require('@playwright/test'); +const {loginAsCustomer} = require("./utils/user"); +const {openPaypalPopup, loginIntoPaypal, completePaypalPayment} = require("./utils/paypal-popup"); +const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout"); + +const { + PRODUCT_URL, +} = process.env; + +async function expectContinuation(page) { + await expect(page.locator('#payment_method_ppcp-gateway')).toBeChecked(); + + await expect(page.locator('.component-frame')).toHaveCount(0); +} + +async function completeContinuation(page) { + await expectContinuation(page); + + await Promise.all([ + page.waitForNavigation(), + page.locator('#place_order').click(), + ]); +} + +test('Save during purchase', async ({page}) => { + await loginAsCustomer(page) + + await page.goto(PRODUCT_URL); + const popup = await openPaypalPopup(page); + + await loginIntoPaypal(popup); + await completePaypalPayment(popup); + await fillCheckoutForm(page); + + await completeContinuation(page); + + await expectOrderReceivedPage(page); +}); + + + +