From 9d2cbdbe51732b7f06d8faf836bd43fec006032b Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Wed, 16 Apr 2025 12:01:23 +0200 Subject: [PATCH 01/65] =?UTF-8?q?=E2=9C=A8=20Add=20UK=20and=20GBP=20to=20t?= =?UTF-8?q?he=20Fastlane=20country/currency=20supported=20matrix=20(behind?= =?UTF-8?q?=20a=20feature=20flag)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/services.php | 87 +++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index bd9882487..96bc85649 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -156,46 +156,85 @@ return array( * The matrix which countries and currency combinations can be used for AXO. */ 'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { + $matrix = array( + 'US' => array( + 'AUD', + 'CAD', + 'EUR', + 'GBP', + 'JPY', + 'USD', + ), + ); + + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + /** + * Filter to determine if Fastlane UK with 3D Secure should be enabled. + * + * @param bool $enabled Whether Fastlane UK is enabled. + */ + if ( apply_filters( + 'woocommerce.feature-flags.woocommerce_paypal_payments.axo_uk_enabled', + getenv( 'PCP_AXO_UK_ENABLED' ) !== '0' + ) ) { + $matrix['GB'] = array( + 'GBP', + ); + } + // phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores + /** * Returns which countries and currency combinations can be used for AXO. */ return apply_filters( 'woocommerce_paypal_payments_axo_supported_country_currency_matrix', - array( - 'US' => array( - 'AUD', - 'CAD', - 'EUR', - 'GBP', - 'JPY', - 'USD', - ), - ) + $matrix ); }, /** * The matrix which countries and card type combinations can be used for AXO. */ 'axo.supported-country-card-type-matrix' => static function ( ContainerInterface $container ) : array { + $matrix = array( + 'US' => array( + 'VISA', + 'MASTERCARD', + 'AMEX', + 'DISCOVER', + ), + 'CA' => array( + 'VISA', + 'MASTERCARD', + 'AMEX', + 'DISCOVER', + ), + ); + + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + /** + * Filter to determine if Fastlane UK with 3D Secure should be enabled. + * + * @param bool $enabled Whether Fastlane UK is enabled. + */ + if ( apply_filters( + 'woocommerce.feature-flags.woocommerce_paypal_payments.axo_uk_enabled', + getenv( 'PCP_AXO_UK_ENABLED' ) !== '0' + ) ) { + $matrix['GB'] = array( + 'VISA', + 'MASTERCARD', + 'AMEX', + 'DISCOVER', + ); + } + // phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores + /** * Returns which countries and card type combinations can be used for AXO. */ return apply_filters( 'woocommerce_paypal_payments_axo_supported_country_card_type_matrix', - array( - 'US' => array( - 'VISA', - 'MASTERCARD', - 'AMEX', - 'DISCOVER', - ), - 'CA' => array( - 'VISA', - 'MASTERCARD', - 'AMEX', - 'DISCOVER', - ), - ) + $matrix ); }, 'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string { From e5cd2b30ec777d7b90f646635a88fa543c60b20a Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 17 Apr 2025 14:00:51 +0200 Subject: [PATCH 02/65] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20the=20Fas?= =?UTF-8?q?tlane=20UK=20feature=20flag=20to=20a=20separate=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/services.php | 41 ++++++++++++++--------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index 96bc85649..a31587f15 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -167,21 +167,9 @@ return array( ), ); - // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores - /** - * Filter to determine if Fastlane UK with 3D Secure should be enabled. - * - * @param bool $enabled Whether Fastlane UK is enabled. - */ - if ( apply_filters( - 'woocommerce.feature-flags.woocommerce_paypal_payments.axo_uk_enabled', - getenv( 'PCP_AXO_UK_ENABLED' ) !== '0' - ) ) { - $matrix['GB'] = array( - 'GBP', - ); + if ( $container->get( 'axo.uk.enabled' ) ) { + $matrix['GB'] = array( 'GBP' ); } - // phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores /** * Returns which countries and currency combinations can be used for AXO. @@ -210,16 +198,7 @@ return array( ), ); - // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores - /** - * Filter to determine if Fastlane UK with 3D Secure should be enabled. - * - * @param bool $enabled Whether Fastlane UK is enabled. - */ - if ( apply_filters( - 'woocommerce.feature-flags.woocommerce_paypal_payments.axo_uk_enabled', - getenv( 'PCP_AXO_UK_ENABLED' ) !== '0' - ) ) { + if ( $container->get( 'axo.uk.enabled' ) ) { $matrix['GB'] = array( 'VISA', 'MASTERCARD', @@ -227,7 +206,6 @@ return array( 'DISCOVER', ); } - // phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores /** * Returns which countries and card type combinations can be used for AXO. @@ -418,4 +396,17 @@ return array( ) ); }, + 'axo.uk.enabled' => static function ( ContainerInterface $container ): bool { + // phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores + /** + * Filter to determine if Fastlane UK with 3D Secure should be enabled. + * + * @param bool $enabled Whether Fastlane UK is enabled. + */ + return apply_filters( + 'woocommerce.feature-flags.woocommerce_paypal_payments.axo_uk_enabled', + getenv( 'PCP_AXO_UK_ENABLED' ) !== '0' + ); + // phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores + }, ); From 7e7d94eb01f5a523978acde97555ee4d5b45d1f5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 14 May 2025 16:02:53 +0200 Subject: [PATCH 03/65] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Change=20REST=20hand?= =?UTF-8?q?ler=20to=20only=20store=20data=20in=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DTO/OAuthConnectionDTO.php | 66 +++++++++++++++++++ .../Endpoint/AuthenticationRestEndpoint.php | 12 +--- .../src/Service/AuthenticationManager.php | 36 ++++++++++ 3 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 modules/ppcp-settings/src/DTO/OAuthConnectionDTO.php diff --git a/modules/ppcp-settings/src/DTO/OAuthConnectionDTO.php b/modules/ppcp-settings/src/DTO/OAuthConnectionDTO.php new file mode 100644 index 000000000..e6b8d94e5 --- /dev/null +++ b/modules/ppcp-settings/src/DTO/OAuthConnectionDTO.php @@ -0,0 +1,66 @@ +is_sandbox = $is_sandbox; + $this->shared_id = $shared_id; + $this->auth_token = $auth_token; + $this->timestamp = 0 === $timestamp ? time() : $timestamp; + } +} diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 1915d998f..d7747c28a 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -204,17 +204,9 @@ class AuthenticationRestEndpoint extends RestEndpoint { $auth_code = $request->get_param( 'authCode' ); $use_sandbox = $request->get_param( 'useSandbox' ); - try { - $this->authentication_manager->validate_id_and_auth_code( $shared_id, $auth_code ); - $this->authentication_manager->authenticate_via_oauth( $use_sandbox, $shared_id, $auth_code ); - } catch ( Exception $exception ) { - return $this->return_error( $exception->getMessage() ); - } + $this->authentication_manager->remember_oauth_connection_details( $shared_id, $auth_code, $use_sandbox ); - $account = $this->authentication_manager->get_account_details(); - $response = $this->sanitize_for_javascript( $this->response_map, $account ); - - return $this->return_success( $response ); + return $this->return_success( true ); } /** diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 3f6cd4909..057182e52 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; +use WooCommerce\PayPalCommerce\Settings\DTO\OAuthConnectionDTO; use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; @@ -33,6 +34,12 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; * Class that manages the connection to PayPal. */ class AuthenticationManager { + + /** + * Option name used to store the OAuth connection details. + */ + private const OAUTH_OPTION_NAME = 'ppcp_oauth_connection_details'; + /** * Data model that stores the connection details. * @@ -221,6 +228,35 @@ class AuthenticationManager { $this->update_connection_details( $connection ); } + /** + * Stores the OAuth details in the DB. + * + * Those details are used in another request to complete the OAuth login process. + * + * @param string $shared_id The shared onboarding ID. + * @param string $auth_code The authorization code. + * @param bool $use_sandbox Whether it's a sandbox or production account. + * @return void + */ + public function remember_oauth_connection_details( string $shared_id, string $auth_code, bool $use_sandbox ) : void { + $this->logger->info( + 'Storing one-time OAuth credentials...', + array( + 'shared_id' => $shared_id, + 'auth_code' => $auth_code, + 'use_sandbox' => $use_sandbox, + ) + ); + + $oauth_connection = new OAuthConnectionDTO( + $use_sandbox, + $shared_id, + $auth_code + ); + + update_option( self::OAUTH_OPTION_NAME, $oauth_connection, false ); + } + /** * Checks if the provided ID and auth-code have a valid format. From 5cd587c51c1550c9ed0ba3af1a5a6dbe904e2818 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 14 May 2025 16:04:56 +0200 Subject: [PATCH 04/65] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Handle=20the=20full?= =?UTF-8?q?=20OAuth=20logic=20during=20page=20reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change addresses the timing issue which often leads to failed login attempts --- .../src/Handler/ConnectionListener.php | 2 +- .../src/Service/AuthenticationManager.php | 58 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php index a1ed6dd29..768474ef9 100644 --- a/modules/ppcp-settings/src/Handler/ConnectionListener.php +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -156,7 +156,7 @@ class ConnectionListener { $this->logger->info( 'Found OAuth merchant data in request', $data ); try { - $this->authentication_manager->finish_oauth_authentication( $data ); + $this->authentication_manager->handle_oauth_authentication( $data ); $this->mark_token_as_processed( $token ); } catch ( \Exception $e ) { $this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() ); diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 057182e52..f5da6aa96 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings\Service; use JsonException; use Throwable; +use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; @@ -257,6 +258,36 @@ class AuthenticationManager { update_option( self::OAUTH_OPTION_NAME, $oauth_connection, false ); } + /** + * Remove the temporary oauth credentials from the DB. + * + * @return void + */ + private function remove_oauth_connection_details() : void { + delete_option( self::OAUTH_OPTION_NAME ); + } + + /** + * Returns the previously remembered oauth details. + * + * @return OAuthConnectionDTO The stored oauth credentials. + * @throws RuntimeException When no stored credentials are found, or they have expired. + */ + private function retrieve_oauth_connection_details() : OAuthConnectionDTO { + $oauth_data = get_option( self::OAUTH_OPTION_NAME ); + + if ( ! $oauth_data instanceof OAuthConnectionDTO ) { + throw new RuntimeException( 'No stored OAuth credentials found' ); + } + + // Check for expiration (credentials expire after 1 hour). + if ( time() - $oauth_data->timestamp > 3600 ) { + $this->remove_oauth_connection_details(); + throw new RuntimeException( 'Stored OAuth credentials have expired' ); + } + + return $oauth_data; + } /** * Checks if the provided ID and auth-code have a valid format. @@ -319,29 +350,44 @@ class AuthenticationManager { $connection->client_secret = $credentials['client_secret']; $connection->merchant_id = $credentials['merchant_id']; - $this->update_connection_details( $connection ); + return $connection; } /** - * Verifies the merchant details in the final OAuth redirect and extracts - * missing credentials from the URL. + * Handles the full OAuth authentication flow. + * + * 1. Verify that the provided request contains required details. + * 2. Retrieve the oauth-connection details and validate them. + * 3. Convert the oauth-connection details into a permanent id/secret. + * 4. Complete the authentication by storing all details in the DB. * * @param array $request_data Array of request parameters to process. * @return void * * @throws RuntimeException Missing or invalid credentials. */ - public function finish_oauth_authentication( array $request_data ) : void { + public function handle_oauth_authentication( array $request_data ) : void { $merchant_id = $request_data['merchant_id'] ?? ''; $merchant_email = $request_data['merchant_email'] ?? ''; $seller_type = $request_data['seller_type'] ?? ''; + // 1. Verify the request details. if ( empty( $merchant_id ) || empty( $merchant_email ) ) { throw new RuntimeException( 'Missing merchant ID or email in request' ); } - $connection = $this->common_settings->get_merchant_data(); + // 2. Retrieve and validate the oauth connection. + $oauth_connection = $this->retrieve_oauth_connection_details(); + $this->validate_id_and_auth_code( $oauth_connection->shared_id, $oauth_connection->auth_token ); + // 3. Trade oauth connection details to permanent client credentials. + $connection = $this->authenticate_via_oauth( + $oauth_connection->is_sandbox, + $oauth_connection->shared_id, + $oauth_connection->auth_token + ); + + // 4. Complete the authentication checks and persist details. if ( $connection->merchant_id && $connection->merchant_id !== $merchant_id ) { throw new RuntimeException( 'Unexpected merchant ID in request' ); } @@ -353,6 +399,8 @@ class AuthenticationManager { $connection->seller_type = $seller_type; } + $this->remove_oauth_connection_details(); + $this->update_connection_details( $connection ); } From 4f51161765ae45b91efe2ee3cf2a745be4b0bc3b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 14 May 2025 16:06:13 +0200 Subject: [PATCH 05/65] =?UTF-8?q?=F0=9F=8E=A8=20Minor=20clean=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/AuthenticationManager.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index f5da6aa96..75845ebd9 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -9,12 +9,9 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Service; -use JsonException; use Throwable; -use Exception; +use JsonException; use Psr\Log\LoggerInterface; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; @@ -303,7 +300,7 @@ class AuthenticationManager { * @return void * @throws RuntimeException When invalid shared ID or auth provided. */ - public function validate_id_and_auth_code( string $shared_id, string $auth_code ) : void { + private function validate_id_and_auth_code( string $shared_id, string $auth_code ) : void { if ( empty( $shared_id ) ) { throw new RuntimeException( 'No onboarding ID provided.' ); } @@ -322,10 +319,10 @@ class AuthenticationManager { * @param bool $use_sandbox Whether to use the sandbox mode. * @param string $shared_id The OAuth client ID. * @param string $auth_code The OAuth authorization code. - * @return void + * @return MerchantConnectionDTO A DTO containing the connection details. * @throws RuntimeException When failed to retrieve payee. */ - public function authenticate_via_oauth( bool $use_sandbox, string $shared_id, string $auth_code ) : void { + private function authenticate_via_oauth( bool $use_sandbox, string $shared_id, string $auth_code ) : MerchantConnectionDTO { $this->logger->info( 'Attempting OAuth login to PayPal...', array( From caff6b563e1cb1d5ce2d5c7296ee76ed994e250b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 14 May 2025 17:53:03 +0200 Subject: [PATCH 06/65] =?UTF-8?q?=F0=9F=94=A5=20Remove=20incorrect/unused?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This condition returns true, even though the token is not valid anymore. --- modules/ppcp-settings/src/Service/OnboardingUrlManager.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/modules/ppcp-settings/src/Service/OnboardingUrlManager.php b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php index f2463af46..93befa47c 100644 --- a/modules/ppcp-settings/src/Service/OnboardingUrlManager.php +++ b/modules/ppcp-settings/src/Service/OnboardingUrlManager.php @@ -87,13 +87,6 @@ class OnboardingUrlManager { return true; } - if ( OnboardingUrl::validate_previous_token( $this->cache, $token, $user_id ) ) { - // TODO: Do we need this here? Previous logic was to reload the page without doing anything in this case. - $this->logger->info( 'Validated previous token, silently redirecting: ' . $log_token ); - - return true; - } - $this->logger->error( 'Failed to validate onboarding ppcpToken: ' . $log_token ); return false; From 342a30a1c435a1ffdc4c3b0981f63340df45877d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 14 May 2025 18:01:19 +0200 Subject: [PATCH 07/65] =?UTF-8?q?=E2=9C=A8=20Handle=20overlapping=20authen?= =?UTF-8?q?tication=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PayPal triggers multiple page redirects after completing the login popup. This commit introduces a new state handler that will halt duplicate requests until the first request is finished --- .../src/Handler/ConnectionListener.php | 108 +++++++++++++++--- 1 file changed, 89 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php index 768474ef9..3a9927734 100644 --- a/modules/ppcp-settings/src/Handler/ConnectionListener.php +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -28,6 +28,17 @@ use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum; * Clicking that button triggers the merchant-connection request. */ class ConnectionListener { + /** + * Token processing states + */ + private const TOKEN_STATE_PROCESSING = 'processing'; + private const TOKEN_STATE_PROCESSED = 'processed'; + + /** + * Transient key for storing token state. + */ + private const TOKEN_STATE_TRANSIENT = 'ppcp_auth_token_state'; + /** * ID of the current settings page; empty if not on a PayPal settings page. * @@ -133,34 +144,65 @@ class ConnectionListener { * @return void */ private function process_oauth_token( string $token ) : void { - // The request contains OAuth details: To avoid abuse we'll slow down the processing. - sleep( 2 ); - if ( ! $token ) { return; } + $log_token = ( (string) substr( $token, 0, 2 ) ) . '...' . ( (string) substr( $token, - 6 ) ); + if ( $this->was_token_processed( $token ) ) { + /* + * Token already processed: + * Do nothing as the DB already contains the full connection details. + */ + $this->logger->info( 'Token already processed, continuing silently', array( 'token' => $log_token ) ); + return; } + if ( $this->is_token_processing( $token ) ) { + /* + * Authentication token is currently processed (in another request): + * Briefly wait and then retry this request, basically waiting for + * the above "was_processed" condition to become true. + */ + $this->logger->info( 'Token is currently being processed, waiting and retrying', array( 'token' => $log_token ) ); + sleep( 1 ); + + // Get the current URL. + $current_url = add_query_arg( null, null ); + + // Add time to the query parameter to prevent browser cache issues. + $current_url = add_query_arg( 'retry', microtime( true ), $current_url ); + + wp_safe_redirect( $current_url ); + exit; + } + if ( ! $this->url_manager->validate_token_and_delete( $token, $this->user_id ) ) { + $this->logger->error( 'Token validation failed', array( 'token' => $log_token ) ); + return; } $data = $this->extract_data(); if ( ! $data ) { + $this->logger->error( 'Failed to extract merchant data from request' ); + return; } $this->logger->info( 'Found OAuth merchant data in request', $data ); try { + $this->set_token_state( $token, self::TOKEN_STATE_PROCESSING ); + $this->authentication_manager->handle_oauth_authentication( $data ); - $this->mark_token_as_processed( $token ); } catch ( \Exception $e ) { $this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() ); } + + $this->set_token_state( $token, self::TOKEN_STATE_PROCESSED ); } /** @@ -194,29 +236,57 @@ class ConnectionListener { } /** - * Checks if the provided authentication token is new or has been used before. + * Sets the state for a token. * - * This check catches an issue where we receive the same authentication token twice, - * which does not impact the login flow but creates noise in the logs. - * - * @param string $token The authentication token to check. - * @return bool True if the token was already processed. + * @param string $token The token to set state for. + * @param string $state The state to set. + * @return void */ - private function was_token_processed( string $token ) : bool { - $prev_token = get_transient( 'ppcp_previous_auth_token' ); + private function set_token_state( string $token, string $state ) : void { + $data = array( + 'token' => $token, + 'state' => $state, + ); - return $prev_token && $prev_token === $token; + // 10 second expiration will block the page for max 10 seconds. + set_transient( self::TOKEN_STATE_TRANSIENT, $data, 10 ); } /** - * Stores the processed authentication token so we can prevent double-processing - * of already verified token. + * Gets the current state of a token. * - * @param string $token The processed authentication token. - * @return void + * @param string $token The token to check. + * @return string The current state of the token, or empty string if the token doesn't match. */ - private function mark_token_as_processed( string $token ) : void { - set_transient( 'ppcp_previous_auth_token', $token, 60 ); + private function get_token_state( string $token ) : string { + $data = get_transient( self::TOKEN_STATE_TRANSIENT ); + + if ( empty( $data ) || ! is_array( $data ) || empty( $data['token'] ) || empty( $data['state'] ) ) { + return ''; + } + + // Only return the state if the token matches. + return ( $data['token'] === $token ) ? $data['state'] : ''; + } + + /** + * Checks if the token is currently being processed. + * + * @param string $token The token to check. + * @return bool True if the token is currently being processed, false otherwise. + */ + private function is_token_processing( string $token ) : bool { + return $this->get_token_state( $token ) === self::TOKEN_STATE_PROCESSING; + } + + /** + * Checks if the token has been fully processed. + * + * @param string $token The token to check. + * @return bool True if the token has been processed, false otherwise. + */ + private function was_token_processed( string $token ) : bool { + return $this->get_token_state( $token ) === self::TOKEN_STATE_PROCESSED; } /** From 30a933f2dfce80f29fa4e97444962f5a1353a887 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Fri, 20 Jun 2025 14:13:00 +0200 Subject: [PATCH 08/65] =?UTF-8?q?=E2=9C=A8=20Add=203ds=20redirect=20handli?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/services.php | 4 +- .../src/Endpoint/OrderEndpoint.php | 44 +++++++- modules/ppcp-api-client/src/Entity/Order.php | 32 ++++-- .../src/Factory/OrderFactory.php | 101 +++++++++++++++++- modules/ppcp-axo/src/AxoModule.php | 39 +++++++ modules/ppcp-axo/src/Gateway/AxoGateway.php | 55 +++++++++- .../ppcp-settings/src/Data/SettingsModel.php | 21 ++++ .../CreditCardOrderInfoHandlingTrait.php | 4 +- 8 files changed, 280 insertions(+), 20 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index addaafbb5..23bbd7515 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -428,9 +428,11 @@ return array( 'api.factory.order' => static function ( ContainerInterface $container ): OrderFactory { $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $payer_factory = $container->get( 'api.factory.payer' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new OrderFactory( $purchase_unit_factory, - $payer_factory + $payer_factory, + $logger ); }, 'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory { diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 69b49e9e3..e9b19344b 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -169,6 +169,7 @@ class OrderEndpoint { * @param string $payment_method WC payment method. * @param array $request_data Request data. * @param PaymentSource|null $payment_source The payment source. + * @param \WC_Order|null $wc_order The WooCommerce order, if available. * * @return Order * @throws RuntimeException If the request fails. @@ -179,7 +180,8 @@ class OrderEndpoint { Payer $payer = null, string $payment_method = '', array $request_data = array(), - PaymentSource $payment_source = null + PaymentSource $payment_source = null, + $wc_order = null ): Order { $bearer = $this->bearer->bearer(); $data = array( @@ -265,7 +267,45 @@ class OrderEndpoint { throw $error; } - $order = $this->order_factory->from_paypal_response( $json ); + // Check if this is a PAYER_ACTION_REQUIRED response (3DS) + $is_3ds_response = isset( $json->status ) && $json->status === 'PAYER_ACTION_REQUIRED'; + + // Extract payer-action URL if present + $payer_action_url = ''; + if ( isset( $json->links ) && is_array( $json->links ) ) { + foreach ( $json->links as $link ) { + if ( isset( $link->rel ) && $link->rel === 'payer-action' && isset( $link->href ) ) { + $payer_action_url = $link->href; + break; + } + } + } + + // Store payer-action URL in WC order meta if available + if ( $wc_order && $payer_action_url ) { + $wc_order->add_meta_data( 'ppcp_axo_payer_action', $payer_action_url ); + $wc_order->save_meta_data(); + + $this->logger->info( + sprintf( + 'Saved payer-action URL to order meta: %s', + $payer_action_url + ) + ); + } + + // Log the entire $json response for debugging + $this->logger->info( + sprintf( + 'Order created successfully. PayPal API response: %s', + print_r( $json, true ) + ) + ); + + // Use appropriate factory method based on response type + $order = $is_3ds_response + ? $this->order_factory->from_paypal_response_with_3ds( $json ) + : $this->order_factory->from_paypal_response( $json ); do_action( 'woocommerce_paypal_payments_paypal_order_created', $order ); diff --git a/modules/ppcp-api-client/src/Entity/Order.php b/modules/ppcp-api-client/src/Entity/Order.php index a9cca343f..feeb7008d 100644 --- a/modules/ppcp-api-client/src/Entity/Order.php +++ b/modules/ppcp-api-client/src/Entity/Order.php @@ -70,6 +70,10 @@ class Order { * @var PaymentSource|null */ private $payment_source; + /** + * @var mixed|null + */ + private $links; /** * Order constructor. @@ -93,17 +97,19 @@ class Order { Payer $payer = null, string $intent = 'CAPTURE', \DateTime $create_time = null, - \DateTime $update_time = null + \DateTime $update_time = null, + $links = null ) { - $this->id = $id; - $this->payer = $payer; - $this->order_status = $order_status; - $this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE'; - $this->purchase_units = $purchase_units; - $this->create_time = $create_time; - $this->update_time = $update_time; - $this->payment_source = $payment_source; + $this->id = $id; + $this->payer = $payer; + $this->order_status = $order_status; + $this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE'; + $this->purchase_units = $purchase_units; + $this->create_time = $create_time; + $this->update_time = $update_time; + $this->payment_source = $payment_source; + $this->links = $links; } /** @@ -179,6 +185,10 @@ class Order { return $this->payment_source; } + public function links() { + return $this->links; + } + /** * Returns the object as array. * @@ -206,6 +216,10 @@ class Order { $order['update_time'] = $this->update_time()->format( 'Y-m-d\TH:i:sO' ); } + if ( $this->links ) { + $this->links()->to_array(); + } + return $order; } } diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index ece13a36a..72e77954e 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; +use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; @@ -34,19 +35,29 @@ class OrderFactory { */ private $payer_factory; + /** + * The logger. + * + * @var LoggerInterface + */ + private $logger; + /** * OrderFactory constructor. * - * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. - * @param PayerFactory $payer_factory The Payer factory. + * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. + * @param PayerFactory $payer_factory The Payer factory. + * @param LoggerInterface $logger The logger. */ public function __construct( PurchaseUnitFactory $purchase_unit_factory, - PayerFactory $payer_factory + PayerFactory $payer_factory, + LoggerInterface $logger ) { - $this->purchase_unit_factory = $purchase_unit_factory; - $this->payer_factory = $payer_factory; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->payer_factory = $payer_factory; + $this->logger = $logger; } /** @@ -96,6 +107,7 @@ class OrderFactory { __( 'Order does not contain status.', 'woocommerce-paypal-payments' ) ); } + if ( ! isset( $order_data->intent ) ) { throw new RuntimeException( __( 'Order does not contain intent.', 'woocommerce-paypal-payments' ) @@ -147,4 +159,83 @@ class OrderFactory { $update_time ); } + + /** + * Returns an Order object based off a PayPal 3DS Response. + * Specifically tailored for PAYER_ACTION_REQUIRED responses. + * + * @param \stdClass $order_data The JSON object. + * + * @return Order + * @throws RuntimeException When JSON object is malformed. + */ + /** + * Returns an Order object based off a PayPal 3DS Response. + * Specifically tailored for PAYER_ACTION_REQUIRED responses. + * + * @param \stdClass $order_data The JSON object. + * + * @return Order + * @throws RuntimeException When JSON object is malformed. + */ + public function from_paypal_response_with_3ds( \stdClass $order_data ): Order { + // Only ID is strictly required + if ( ! isset( $order_data->id ) ) { + throw new RuntimeException( + __( 'Order does not contain an id.', 'woocommerce-paypal-payments' ) + ); + } + + // Don't try to create purchase units for 3DS responses + // Use an empty array instead + $purchase_units = array(); + + // Status - should be PAYER_ACTION_REQUIRED + $status = new OrderStatus( $order_data->status ?? 'PAYER_ACTION_REQUIRED' ); + + // Use CAPTURE as default intent for 3DS responses + $intent = $order_data->intent ?? 'CAPTURE'; + + // All timestamps are null for 3DS responses + $create_time = null; + $update_time = null; + + // Payer is typically null in 3DS responses + $payer = null; + + // Application context is null in 3DS responses + $application_context = null; + + // Handle payment source (card) if present + $payment_source = null; + if ( isset( $order_data->payment_source ) ) { + try { + // Extract the payment source name (usually "card") + $source_props = get_object_vars( $order_data->payment_source ); + if ( ! empty( $source_props ) ) { + $source_name = array_key_first( $source_props ); + $payment_source = new PaymentSource( + $source_name, + $order_data->payment_source->$source_name + ); + } + } catch ( \Exception $e ) { + $this->logger->warning( 'Could not create payment source: ' . $e->getMessage() ); + // Continue without payment source + } + } + + // Create the Order without including links parameter + return new Order( + $order_data->id, + $purchase_units, + $status, + $payment_source, + $payer, + $intent, + $create_time, + $update_time, + $order_data->links + ); + } } diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 1b9b6f59a..f8f7017fd 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Axo; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; @@ -19,6 +20,7 @@ use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -367,6 +369,43 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); + // Trigger 3D Secure verification. + add_filter( + 'ppcp_create_order_request_body_data', + function( array $data, string $payment_method ) use ( $c ): array { + if ( ! $c->get( 'axo.uk.enabled' ) ) { + return $data; + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + if ( $payment_method !== AxoGateway::ID ) { + return $data; + } + + $settings_model = $c->get( 'settings.data.settings' ); + assert( $settings_model instanceof SettingsModel ); + + $three_d_secure = $settings_model->get_three_d_secure_enum(); + + if ( + $three_d_secure === 'SCA_ALWAYS' + || $three_d_secure === 'SCA_WHEN_REQUIRED' + ) { + $data['payment_source']['card']->attributes = array( + 'verification' => array( + 'method' => $three_d_secure, + ), + ); + } + + return $data; + }, + 10, + 2 + ); + return true; } diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index d698455b5..69ff587e4 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -253,6 +253,49 @@ class AxoGateway extends WC_Payment_Gateway { $order = $this->create_paypal_order( $wc_order, $token ); + // Check if 3DS verification is required + $payer_action = ''; + foreach ( $order->links() as $link ) { + if ( $link->rel === 'payer-action' ) { + $payer_action = $link->href; + } + } + + $this->logger->debug( + 'AXO order created', + array( + 'result' => 'success', + 'return_url' => $this->get_return_url( $wc_order ), + 'payer_action' => $payer_action, + ) + ); + + // If 3DS verification is required, append the redirect_uri and return the redirect URL + $this->logger->debug( + 'AXO order created', + array( + 'result' => 'success', + 'return_url' => $this->get_return_url( $wc_order ), + 'payer_action' => $payer_action, + ) + ); + + // If 3DS verification is required, append the redirect_uri and return the redirect URL + if ( $payer_action ) { + $return_url = $this->get_return_url( $wc_order ); + + $redirect_url = add_query_arg( + 'redirect_uri', + urlencode( $return_url ), + $payer_action + ); + + return array( + 'result' => 'success', + 'redirect' => $redirect_url, + ); + } + /** * This filter controls if the method 'process()' from OrderProcessor will be called. * So you can implement your own for example on subscriptions @@ -286,6 +329,7 @@ class AxoGateway extends WC_Payment_Gateway { * @return Order The PayPal order. */ protected function create_paypal_order( WC_Order $wc_order, string $payment_token ) : Order { + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); $shipping_preference = $this->shipping_preference_factory->from_state( @@ -302,11 +346,20 @@ class AxoGateway extends WC_Payment_Gateway { $payment_source_properties ); + $this->logger->debug( + 'AXO full order endpoint data', + array( + 'purchase_unit' => $purchase_unit, + 'shipping_preference' => $shipping_preference, + 'payment_source' => $payment_source, + ) + ); + return $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference, null, - '', + self::ID, array(), $payment_source ); diff --git a/modules/ppcp-settings/src/Data/SettingsModel.php b/modules/ppcp-settings/src/Data/SettingsModel.php index 4ef6abd14..d731d4623 100644 --- a/modules/ppcp-settings/src/Data/SettingsModel.php +++ b/modules/ppcp-settings/src/Data/SettingsModel.php @@ -227,6 +227,27 @@ class SettingsModel extends AbstractDataModel { return $this->data['three_d_secure']; } + /** + * Converts the 3D Secure setting value to the corresponding API enum string. + * + * @param string|null $three_d_secure The 3D Secure setting ('no-3d-secure', 'only-required-3d-secure', 'always-3d-secure') + * @return string The corresponding API enum string ('NO_3D_SECURE', 'SCA_WHEN_REQUIRED', 'SCA_ALWAYS') + */ + public function get_three_d_secure_enum( string $three_d_secure = null ): string { + // If no value is provided, use the current setting. + if ( $three_d_secure === null ) { + $three_d_secure = $this->get_three_d_secure(); + } + + $map = array( + 'no-3d-secure' => 'NO_3D_SECURE', + 'only-required-3d-secure' => 'SCA_WHEN_REQUIRED', + 'always-3d-secure' => 'SCA_ALWAYS', + ); + + return $map[ $three_d_secure ] ?? 'SCA_WHEN_REQUIRED'; // Default to SCA_WHEN_REQUIRED if mapping not found. + } + /** * Sets the 3D Secure setting. * diff --git a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php index c730136ab..5ba9713f5 100644 --- a/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/CreditCardOrderInfoHandlingTrait.php @@ -13,6 +13,7 @@ use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\FraudProcessorResponse; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory; +use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; /** @@ -24,7 +25,6 @@ trait CreditCardOrderInfoHandlingTrait { * Handles the 3DS details. * * Adds the order note with 3DS details. - * Adds the order meta with 3DS details. * * @param Order $order The PayPal order. * @param WC_Order $wc_order The WC order. @@ -35,7 +35,7 @@ trait CreditCardOrderInfoHandlingTrait { ): void { $payment_source = $order->payment_source(); - if ( ! $payment_source || $payment_source->name() !== 'card' ) { + if ( ! $payment_source || ( $payment_source->name() !== 'card' && $payment_source->name() !== AxoGateway::ID ) ) { return; } From 2804b4157d8703fbb651ebe08ff39736bb06de36 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 23 Jun 2025 13:39:57 +0200 Subject: [PATCH 09/65] =?UTF-8?q?=F0=9F=94=84=20Add=20session-based=20redi?= =?UTF-8?q?rect=20flow=20handling=20post=203ds=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Entity/Order.php | 18 +- .../src/Factory/OrderFactory.php | 12 +- modules/ppcp-axo/src/AxoModule.php | 14 ++ modules/ppcp-axo/src/Gateway/AxoGateway.php | 224 ++++++++++++++---- .../src/Endpoint/ReturnUrlEndpoint.php | 170 ++++++++++++- 5 files changed, 376 insertions(+), 62 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/Order.php b/modules/ppcp-api-client/src/Entity/Order.php index feeb7008d..7a8a596e0 100644 --- a/modules/ppcp-api-client/src/Entity/Order.php +++ b/modules/ppcp-api-client/src/Entity/Order.php @@ -101,15 +101,15 @@ class Order { $links = null ) { - $this->id = $id; - $this->payer = $payer; - $this->order_status = $order_status; - $this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE'; - $this->purchase_units = $purchase_units; - $this->create_time = $create_time; - $this->update_time = $update_time; - $this->payment_source = $payment_source; - $this->links = $links; + $this->id = $id; + $this->payer = $payer; + $this->order_status = $order_status; + $this->intent = ( 'CAPTURE' === $intent ) ? 'CAPTURE' : 'AUTHORIZE'; + $this->purchase_units = $purchase_units; + $this->create_time = $create_time; + $this->update_time = $update_time; + $this->payment_source = $payment_source; + $this->links = $links; } /** diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 72e77954e..2b1a8fdd6 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -45,9 +45,9 @@ class OrderFactory { /** * OrderFactory constructor. * - * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. - * @param PayerFactory $payer_factory The Payer factory. - * @param LoggerInterface $logger The logger. + * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. + * @param PayerFactory $payer_factory The Payer factory. + * @param LoggerInterface $logger The logger. */ public function __construct( PurchaseUnitFactory $purchase_unit_factory, @@ -55,9 +55,9 @@ class OrderFactory { LoggerInterface $logger ) { - $this->purchase_unit_factory = $purchase_unit_factory; - $this->payer_factory = $payer_factory; - $this->logger = $logger; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->payer_factory = $payer_factory; + $this->logger = $logger; } /** diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index f8f7017fd..84fbba52d 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; @@ -398,6 +399,19 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { 'method' => $three_d_secure, ), ); + + $experience_context_builder = $c->get( 'wcgateway.builder.experience-context' ); + assert( $experience_context_builder instanceof ExperienceContextBuilder ); + + $data['experience_context'] = $experience_context_builder + ->with_endpoint_return_urls() + ->with_current_brand_name() + ->with_current_locale() + ->build()->to_array(); + + $data['transaction_context'] = array( + 'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ), + ); } return $data; diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index 69ff587e4..b3b40d8e0 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -11,13 +11,18 @@ namespace WooCommerce\PayPalCommerce\Axo\Gateway; use Psr\Log\LoggerInterface; use Exception; +use WC_AJAX; use WC_Order; use WC_Payment_Gateway; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait; @@ -29,6 +34,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait; use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\Messages; +use DomainException; /** * Class AXOGateway. @@ -232,12 +239,65 @@ class AxoGateway extends WC_Payment_Gateway { $wc_order = wc_get_order( $order_id ); if ( ! is_a( $wc_order, WC_Order::class ) ) { - return $this->handle_payment_failure( - null, - new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + return array( + 'result' => 'failure', + 'message' => __( 'Order not found. Please try again.', 'woocommerce-paypal-payments' ), ); } + // Check for stored 3DS errors. + $stored_error = $this->get_and_clear_stored_error(); + if ( $stored_error ) { + return array( + 'result' => 'failure', + 'message' => $stored_error['message'], + ); + } + + $existing_order = $this->session_handler->order(); + + if ( $existing_order ) { + // Check if this session order belongs to current WC order and we're in 3DS context. + $session_order_belongs_to_current_wc_order = $this->session_order_matches_wc_order( $existing_order, $wc_order ); + $is_3ds_context = $this->is_3ds_context(); + + if ( $session_order_belongs_to_current_wc_order && $is_3ds_context ) { + // This is a legitimate 3DS return for the current order. + try { + /** + * This filter controls if the method 'process()' from OrderProcessor will be called. + */ + $process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order ); + if ( $process ) { + $this->order_processor->process_captured_and_authorized( $wc_order, $existing_order ); + } + + // Clear session after successful processing. + $this->session_handler->destroy_session_data(); + WC()->cart->empty_cart(); + + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); + + } catch ( Exception $exception ) { + // Handle 3DS processing failures with universal error approach. + // Clear session data since payment failed. + $this->session_handler->destroy_session_data(); + + return array( + 'result' => 'failure', + 'message' => $this->get_user_friendly_error_message( $exception ), + ); + } + } else { + // Session order doesn't belong to current WC order OR not 3DS context. + $this->session_handler->destroy_session_data(); + } + } + + // No existing order or cleared session - this is an initial payment. try { // phpcs:ignore WordPress.Security.NonceVerification.Missing $fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) ); @@ -251,38 +311,28 @@ class AxoGateway extends WC_Payment_Gateway { // phpcs:ignore WordPress.Security.NonceVerification.Missing $token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) ); - $order = $this->create_paypal_order( $wc_order, $token ); - - // Check if 3DS verification is required - $payer_action = ''; - foreach ( $order->links() as $link ) { - if ( $link->rel === 'payer-action' ) { - $payer_action = $link->href; - } + // Enhanced token validation with universal error handling. + if ( empty( $token ) ) { + // Universal error return. + return array( + 'result' => 'failure', + 'message' => $this->is_3ds_context() + ? __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' ) + : __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ), + ); } - $this->logger->debug( - 'AXO order created', - array( - 'result' => 'success', - 'return_url' => $this->get_return_url( $wc_order ), - 'payer_action' => $payer_action, - ) - ); + $order = $this->create_paypal_order( $wc_order, $token ); - // If 3DS verification is required, append the redirect_uri and return the redirect URL - $this->logger->debug( - 'AXO order created', - array( - 'result' => 'success', - 'return_url' => $this->get_return_url( $wc_order ), - 'payer_action' => $payer_action, - ) - ); + // Check if 3DS verification is required. + $payer_action = $this->get_payer_action_url( $order ); - // If 3DS verification is required, append the redirect_uri and return the redirect URL + // If 3DS verification is required, store order and redirect. if ( $payer_action ) { - $return_url = $this->get_return_url( $wc_order ); + // Store the order in session before 3DS redirect. + $this->session_handler->replace_order( $order ); + + $return_url = home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ); $redirect_url = add_query_arg( 'redirect_uri', @@ -309,7 +359,11 @@ class AxoGateway extends WC_Payment_Gateway { $this->order_processor->process_captured_and_authorized( $wc_order, $order ); } } catch ( Exception $exception ) { - return $this->handle_payment_failure( $wc_order, $exception ); + // Error handling for initial payment failures. + return array( + 'result' => 'failure', + 'message' => $this->get_user_friendly_error_message( $exception ), + ); } WC()->cart->empty_cart(); @@ -320,6 +374,105 @@ class AxoGateway extends WC_Payment_Gateway { ); } + /** + * Get and clear stored payment errors. + */ + private function get_and_clear_stored_error() { + if ( ! WC()->session ) { + return null; + } + + $stored_error = WC()->session->get( 'ppcp_payment_error' ); + if ( $stored_error ) { + WC()->session->__unset( 'ppcp_payment_error' ); + return $stored_error; + } + + return null; + } + + /** + * Convert exceptions to user-friendly messages. + */ + private function get_user_friendly_error_message( Exception $exception ) { + $error_message = $exception->getMessage(); + + // Handle specific error types with user-friendly messages. + if ( $exception instanceof DomainException ) { + if ( strpos( $error_message, 'Could not capture' ) !== false ) { + return __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ); + } + } + + if ( strpos( $error_message, 'declined' ) !== false || + strpos( $error_message, 'PAYMENT_DENIED' ) !== false || + strpos( $error_message, 'INSTRUMENT_DECLINED' ) !== false || + strpos( $error_message, 'Payment provider declined' ) !== false ) { + return __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ); + } + + if ( strpos( $error_message, 'session' ) !== false || + strpos( $error_message, 'expired' ) !== false ) { + return __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' ); + } + + return __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ); + } + + /** + * Extract payer action URL from PayPal order. + */ + private function get_payer_action_url( Order $order ) { + foreach ( $order->links() as $link ) { + if ( $link->rel === 'payer-action' ) { + return $link->href; + } + } + return ''; + } + + /** + * Check if session order belongs to current WC order. + * + * @param Order $paypal_order The PayPal order from session. + * @param WC_Order $wc_order The current WooCommerce order. + * @return bool + */ + private function session_order_matches_wc_order( Order $paypal_order, WC_Order $wc_order ): bool { + $paypal_custom_id = $paypal_order->purchase_units()[0]->custom_id() ?? ''; + $wc_order_id = (string) $wc_order->get_id(); + + return $paypal_custom_id === $wc_order_id; + } + + /** + * Check if we're in a 3DS return context. + * + * @return bool + */ + private function is_3ds_context(): bool { + // Check referer for PayPal. + $referer = $_SERVER['HTTP_REFERER'] ?? ''; + if ( strpos( $referer, 'paypal.com' ) !== false ) { + return true; + } + + // Check for 3DS-specific parameters. + $three_ds_params = array( 'liability_shift', 'state', 'code', 'authentication_state' ); + foreach ( $three_ds_params as $param ) { + if ( isset( $_GET[ $param ] ) ) { + return true; + } + } + + // Check if we're being called by ReturnUrlEndpoint. + if ( isset( $_GET['wc-ajax'] ) && $_GET['wc-ajax'] === 'ppc-return-url' ) { + return true; + } + + return false; + } + /** * Create a new PayPal order from the existing WC_Order instance. * @@ -346,15 +499,6 @@ class AxoGateway extends WC_Payment_Gateway { $payment_source_properties ); - $this->logger->debug( - 'AXO full order endpoint data', - array( - 'purchase_unit' => $purchase_unit, - 'shipping_preference' => $shipping_preference, - 'payment_source' => $payment_source, - ) - ); - return $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference, diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index ff28bedee..91900daf9 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -9,10 +9,13 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint; +use DomainException; use Psr\Log\LoggerInterface; +use Exception; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -75,19 +78,77 @@ class ReturnUrlEndpoint { * Handles the incoming request. */ public function handle_request(): void { + // Initialize WC session for AJAX endpoints. + if ( ! WC()->session ) { + WC()->initialize_session(); + } - // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! WC()->session->get_session_cookie() ) { + WC()->session->set_customer_session_cookie( true ); + } + + // Check if we have a PayPal order in session (3DS return). + $order = $this->session_handler->order(); + + if ( $order ) { + try { + // Handle 3DS capture before processing. + $order = $this->handle_3ds_return( $order ); + + $wc_order_id = (int) $order->purchase_units()[0]->custom_id(); + $wc_order = wc_get_order( $wc_order_id ); + + if ( ! $wc_order ) { + wc_add_notice( __( 'Order information is missing. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + + $payment_gateway = $this->get_payment_gateway( $wc_order->get_payment_method() ); + if ( ! $payment_gateway ) { + wc_add_notice( __( 'Payment gateway is unavailable. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + + $result = $payment_gateway->process_payment( $wc_order_id ); + + if ( isset( $result['result'] ) && $result['result'] === 'success' ) { + wp_safe_redirect( $result['redirect'] ); + exit(); + } + + wc_add_notice( __( 'Payment processing failed. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + + } catch ( Exception $e ) { + wc_add_notice( __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + } + + // No order in session - handle regular PayPal returns. if ( ! isset( $_GET['token'] ) ) { + wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); exit(); } - $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); + // Handle regular PayPal returns (non-3DS). // phpcs:enable WordPress.Security.NonceVerification.Recommended - $order = $this->order_endpoint->order( $token ); + $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); - if ( $order->status()->is( OrderStatus::APPROVED ) - || $order->status()->is( OrderStatus::COMPLETED ) - ) { + try { + $order = $this->order_endpoint->order( $token ); + } catch ( Exception $exception ) { + wc_add_notice( __( 'Could not retrieve payment information. Please try again.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + + if ( $order->status()->is( OrderStatus::APPROVED ) || $order->status()->is( OrderStatus::COMPLETED ) ) { $this->session_handler->replace_order( $order ); } @@ -102,22 +163,36 @@ class ReturnUrlEndpoint { } $this->logger->warning( "Return URL endpoint $token: no WC order ID." ); + wc_add_notice( __( 'Order information is missing. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); exit(); } $wc_order = wc_get_order( $wc_order_id ); if ( ! is_a( $wc_order, \WC_Order::class ) ) { $this->logger->warning( "Return URL endpoint $token: WC order $wc_order_id not found." ); + + wc_add_notice( __( 'Order not found. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); exit(); } + // Handle different gateway types. if ( $wc_order->get_payment_method() === OXXOGateway::ID ) { $this->session_handler->destroy_session_data(); wp_safe_redirect( wc_get_checkout_url() ); exit(); } - $success = $this->gateway->process_payment( $wc_order_id ); + $payment_gateway = $this->get_payment_gateway( $wc_order->get_payment_method() ); + if ( ! $payment_gateway ) { + wc_add_notice( __( 'Payment gateway is unavailable. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + + $success = $payment_gateway->process_payment( $wc_order_id ); + if ( isset( $success['result'] ) && 'success' === $success['result'] ) { add_filter( 'allowed_redirect_hosts', @@ -130,7 +205,88 @@ class ReturnUrlEndpoint { wp_safe_redirect( $success['redirect'] ); exit(); } + + wc_add_notice( __( 'Payment processing failed. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); exit(); } + + + /** + * Handle 3DS return and capture order if needed. + * + * @param mixed $order The PayPal order. + * @return mixed The processed order. + */ + private function handle_3ds_return( $order ) { + // If order is still CREATED after 3DS, it needs to be captured. + if ( $order->status()->is( OrderStatus::CREATED ) ) { + try { + // Capture the order. + $captured_order = $this->order_endpoint->capture( $order ); + + // Check if capture actually succeeded vs. payment declined. + if ( $captured_order->status()->is( OrderStatus::COMPLETED ) ) { + // Update session with captured order. + $this->session_handler->replace_order( $captured_order ); + return $captured_order; + } else { + // Capture API succeeded but payment was declined. + throw new Exception( __( 'Payment was declined by the payment provider. Please try a different payment method.', 'woocommerce-paypal-payments' ) ); + } + } catch ( DomainException $e ) { + // Handle 3DS authentication failures (Test Case 4: Unavailable). + // Clear session data since authentication failed. + $this->session_handler->destroy_session_data(); + + // Use native WooCommerce error handling. + wc_add_notice( __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + + } catch ( RuntimeException $e ) { + if ( strpos( $e->getMessage(), 'declined' ) !== false || + strpos( $e->getMessage(), 'PAYMENT_DENIED' ) !== false || + strpos( $e->getMessage(), 'INSTRUMENT_DECLINED' ) !== false || + strpos( $e->getMessage(), 'Payment provider declined' ) !== false ) { + + $this->session_handler->destroy_session_data(); + + wc_add_notice( __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + throw $e; + } catch ( Exception $e ) { + $this->session_handler->destroy_session_data(); + wc_add_notice( __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } + } + + return $order; + } + /** + * Gets the appropriate payment gateway for the given payment method. + * + * @param string $payment_method The payment method ID. + * @return \WC_Payment_Gateway|null + */ + private function get_payment_gateway( string $payment_method ) { + + // For regular PayPal payments, use the injected gateway. + if ( $payment_method === $this->gateway->id ) { + return $this->gateway; + } + + // For other payment methods (like AXO), get from WooCommerce. + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + if ( isset( $available_gateways[ $payment_method ] ) ) { + return $available_gateways[ $payment_method ]; + } + + return null; + } } From c9aa43d7f09271f693b5c0ba2b54fa43d7ec5ade Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:37:12 +0400 Subject: [PATCH 10/65] Add migration logic when switching UI --- .../src/Ajax/SwitchSettingsUiEndpoint.php | 52 +++++++------------ 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php index 464262d41..eb072b3bb 100644 --- a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php @@ -13,6 +13,7 @@ use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\MigrationManager; /** * Class SwitchSettingsUiEndpoint @@ -24,26 +25,10 @@ class SwitchSettingsUiEndpoint { public const ENDPOINT = 'ppcp-settings-switch-ui'; public const OPTION_NAME_SHOULD_USE_OLD_UI = 'woocommerce_ppcp-settings-should-use-old-ui'; - /** - * The RequestData. - * - * @var RequestData - */ protected RequestData $request_data; - - /** - * The logger. - * - * @var LoggerInterface - */ protected LoggerInterface $logger; - - /** - * The Onboarding profile. - * - * @var OnboardingProfile - */ protected OnboardingProfile $onboarding_profile; + protected MigrationManager $settings_data_migration; /** * True if the merchant is onboarded, otherwise false. @@ -52,24 +37,18 @@ class SwitchSettingsUiEndpoint { */ protected bool $is_onboarded; - /** - * SwitchSettingsUiEndpoint constructor. - * - * @param LoggerInterface $logger The logger. - * @param RequestData $request_data The Request data. - * @param OnboardingProfile $onboarding_profile The Onboarding profile. - * @param bool $is_onboarded True if the merchant is onboarded, otherwise false. - */ public function __construct( LoggerInterface $logger, RequestData $request_data, OnboardingProfile $onboarding_profile, + MigrationManager $settings_data_migration, bool $is_onboarded ) { - $this->logger = $logger; - $this->request_data = $request_data; - $this->onboarding_profile = $onboarding_profile; - $this->is_onboarded = $is_onboarded; + $this->logger = $logger; + $this->request_data = $request_data; + $this->onboarding_profile = $onboarding_profile; + $this->settings_data_migration = $settings_data_migration; + $this->is_onboarded = $is_onboarded; } /** @@ -81,18 +60,23 @@ class SwitchSettingsUiEndpoint { return; } + update_option( MigrationManager::OPTION_NAME_IS_MIGRATION_RUNNING, true ); + try { $this->request_data->read_request( $this->nonce() ); update_option( self::OPTION_NAME_SHOULD_USE_OLD_UI, 'no' ); - if ( $this->is_onboarded ) { - $this->onboarding_profile->set_completed( true ); - $this->onboarding_profile->save(); - } + $this->onboarding_profile->set_completed( true ); + $this->onboarding_profile->set_gateways_synced( true ); + $this->onboarding_profile->save(); + $this->settings_data_migration->migrate(); + + delete_option(MigrationManager::OPTION_NAME_IS_MIGRATION_RUNNING); wp_send_json_success(); } catch ( Exception $error ) { - wp_send_json_error( array( 'message' => $error->getMessage() ), 500 ); + delete_option(MigrationManager::OPTION_NAME_IS_MIGRATION_RUNNING); + wp_send_json_error(array('message' => $error->getMessage()), 500); } } From 6af9e99fa3c6fca71e8b693b869c762d086ea5df Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:37:40 +0400 Subject: [PATCH 11/65] Create migration manager --- .../Service/Migration/MigrationManager.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/Migration/MigrationManager.php diff --git a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php new file mode 100644 index 000000000..045c51718 --- /dev/null +++ b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php @@ -0,0 +1,49 @@ +general_settings_migration = $general_settings_migration; + $this->settings_tab_migration = $settings_tab_migration; + $this->styling_settings_migration = $styling_settings_migration; + $this->payment_settings_migration = $payment_settings_migration; + } + + /** + * Executes migration for all settings areas/tabs. + * + * @return void + */ + public function migrate(): void { + $this->general_settings_migration->migrate(); + $this->settings_tab_migration->migrate(); + $this->styling_settings_migration->migrate(); + $this->payment_settings_migration->migrate(); + } +} From d6436847095d90e91120768a68a8c01d6c9bb42d Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:38:03 +0400 Subject: [PATCH 12/65] Create general settings migration --- .../Migration/GeneralSettingsMigration.php | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php diff --git a/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php new file mode 100644 index 000000000..3ae66489b --- /dev/null +++ b/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php @@ -0,0 +1,122 @@ +settings = $settings; + $this->general_settings = $general_settings; + $this->partners_endpoint = $partners_endpoint; + } + + /** + * Migrates legacy connection settings to new data structure. + * + * @return void + */ + public function migrate(): void { + if ( !$this->settings->has( 'client_id' ) + || !$this->settings->has( 'client_secret' ) + || !$this->settings->has( 'merchant_id' ) ) { + return; + } + + $connection = new MerchantConnectionDTO( + $this->settings->has( 'sandbox_on' ) && $this->settings->get( 'sandbox_on' ), + $this->settings->get( 'client_id' ), + $this->settings->get( 'client_secret' ), + $this->settings->get( 'merchant_id' ), + $this->settings->has( 'merchant_email' ) ? $this->settings->get( 'merchant_email' ) : '', + $this->partners_endpoint->seller_status()->country(), + $this->merchant_account_type( $this->partners_endpoint->seller_status() ) + ); + + $this->general_settings->set_merchant_data( $connection ); + $this->general_settings->save(); + } + + /** + * Determines the merchant account type based on seller status capabilities. + * + * Analyzes PayPal seller capabilities to determine if the account is a business + * account or falls back to unknown. + * + * @param SellerStatus $seller_status + * @return 'business' | 'unknown' The merchant account type (SellerTypeEnum constant). + */ + protected function merchant_account_type(SellerStatus $seller_status): string + { + if ($this->has_capability_active($seller_status, 'COMMERCIAL_ENTITY')) { + return SellerTypeEnum::BUSINESS; + } + + $business_capabilities = [ + 'CUSTOM_CARD_PROCESSING', + 'CARD_PROCESSING_VIRTUAL_TERMINAL', + 'FRAUD_TOOL_ACCESS', + 'PAY_UPON_INVOICE', + 'SEND_INVOICE' + ]; + + foreach ($business_capabilities as $capability) { + if ($this->has_capability_active($seller_status, $capability)) { + return SellerTypeEnum::BUSINESS; + } + } + + foreach ($seller_status->products() as $product) { + if ($product->name() === 'PPCP_CUSTOM' && + $product->vetting_status() === 'SUBSCRIBED') { + return SellerTypeEnum::BUSINESS; + } + } + + return SellerTypeEnum::UNKNOWN; + } + + /** + * Checks if a specific capability is active for the seller. + * + * @param SellerStatus $seller_status + * @param string $capability_name + * @return bool True if the capability is active, false otherwise. + */ + private function has_capability_active(SellerStatus $seller_status, string $capability_name): bool + { + foreach ($seller_status->capabilities() as $capability) { + if ($capability->name() === $capability_name && + $capability->status() === 'ACTIVE') { + return true; + } + } + return false; + } +} From 0ca62ee78125f7e5c9554c5494699a8dee038d16 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:38:29 +0400 Subject: [PATCH 13/65] Create payment settings migration --- .../Migration/PaymentSettingsMigration.php | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php diff --git a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php new file mode 100644 index 000000000..7e658aca5 --- /dev/null +++ b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php @@ -0,0 +1,88 @@ + + */ + protected array $local_apms; + + public function __construct( + Settings $settings, + PaymentSettings $payment_settings, + array $local_apms + ) { + $this->settings = $settings; + $this->payment_settings = $payment_settings; + $this->local_apms = $local_apms; + } + + /** + * Migrates legacy payment settings to new data structure. + * + * @return void + */ + public function migrate(): void { + if ( $this->settings->has('disable_funding') ) { + $disable_funding = $this->settings->get('disable_funding'); + if ( ! in_array('venmo', $disable_funding, true) ) { + $this->payment_settings->toggle_method_state( 'venmo', true ); + } + + foreach ( $this->local_apms as $apm ) { + if ( ! in_array($apm['id'], $disable_funding, true) ) { + $this->payment_settings->toggle_method_state( $apm['id'], true ); + } + } + } + + foreach ( $this->map() as $old_key => $method_name ) { + if ( $this->settings->has( $old_key ) && $this->settings->get( $old_key ) ) { + $this->payment_settings->toggle_method_state( $method_name, true ); + } + } + + $this->payment_settings->save(); + } + + /** + * Maps old setting keys to new payment method settings names. + * + * @return array + */ + protected function map(): array { + return array( + 'dcc_enabled' => CreditCardGateway::ID, + 'axo_enabled' => AxoGateway::ID, + 'applepay_button_enabled' => ApplePayGateway::ID, + 'googlepay_button_enabled' => GooglePayGateway::ID, + 'pay_later_button_enabled' => 'pay-later', + ); + } +} From aff59a065eb8e7e031d3f9a82a2f6331ec99f3dc Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:38:39 +0400 Subject: [PATCH 14/65] Create settings tab migration --- .../Migration/SettingsTabMigration.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php diff --git a/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php b/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php new file mode 100644 index 000000000..177ee4993 --- /dev/null +++ b/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php @@ -0,0 +1,82 @@ +settings = $settings; + $this->settings_tab = $settings_tab; + $this->settings_tab_map_helper = $settings_tab_map_helper; + } + + /** + * Migrates legacy settings tab settings to new data structure. + * + * @return void + */ + public function migrate(): void { + $data = array(); + + foreach ($this->settings_tab_map_helper->map() as $old_key => $new_key) { + if ( ! $this->settings->has( $old_key ) ) { + continue; + } + + switch ( $old_key ) { + case 'subtotal_mismatch_behavior': + $value = $this->settings->get( $old_key ); + $data[$new_key] = $value === PurchaseUnitSanitizer::MODE_EXTRA_LINE ? 'correction' : 'no_details'; + break; + case 'landing_page': + $value = $this->settings->get( $old_key ); + $data[$new_key] = $value === ExperienceContext::LANDING_PAGE_LOGIN + ? 'login' + : ( $value === ExperienceContext::LANDING_PAGE_GUEST_CHECKOUT + ? 'guest_checkout' + : 'any' + ); + break; + case 'intent': + $value = $this->settings->get( $old_key ); + $data['authorize_only'] = $value === 'AUTHORIZE'; + $data['capture_virtual_orders'] = $value === 'CAPTURE'; + break; + case 'blocks_final_review_enabled': + $data[$new_key] = ! $this->settings->get( $old_key ); + break; + default: + $data[$new_key] = $this->settings->get( $old_key ); + } + } + + $this->settings_tab->from_array($data); + $this->settings_tab->save(); + } +} From 5edc73727dfebc90c80a15140e460c2ae3ae161f Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:38:52 +0400 Subject: [PATCH 15/65] Create styling settings migration --- .../Migration/StylingSettingsMigration.php | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php diff --git a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php new file mode 100644 index 000000000..3800c0b03 --- /dev/null +++ b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php @@ -0,0 +1,166 @@ +settings = $settings; + $this->styling_settings = $styling_settings; + } + + /** + * Migrates legacy styling settings to new data structure. + * + * @return void + */ + public function migrate(): void { + $location_styles = array(); + + $styling_per_location = $this->settings->has('smart_button_enable_styling_per_location') && $this->settings->get('smart_button_enable_styling_per_location'); + + foreach ( $this->locations_map() as $old_location => $new_location ) { + $context = $styling_per_location ? $old_location : 'general'; + + $location_styles[$new_location] = new LocationStylingDTO( + $new_location, + $this->is_button_enabled_for_location( $old_location, 'smart' ), + $this->enabled_methods( $old_location ), + $this->style_for_context( 'shape', $context ) ?? 'rect', + $this->style_for_context( 'label', $context ) ?? 'pay', + $this->style_for_context( 'color', $context ) ?? 'gold', + $this->style_for_context( 'layout', $context ) ?? 'vertical', + (bool) ( $this->style_for_context( 'tagline', $context ) ?? false ) + ); + } + + $this->styling_settings->from_array( $location_styles ); + $this->styling_settings->save(); + } + + /** + * Determines which payment methods are enabled for a specific location. + * + * Checks legacy settings to determine which payment methods (PayPal, Pay Later, + * Venmo, Apple Pay, Google Pay) should be enabled for the given location. + * + * @param string $location The location name ('cart', 'checkout', etc.). + * @return string[] The list of enabled payment method IDs. + */ + protected function enabled_methods( string $location ): array { + $methods = array(PayPalGateway::ID); + + if ( $this->is_button_enabled_for_location( $location, 'pay_later' ) ) { + $methods[] = 'pay-later'; + } + + if ( $this->settings->has('disable_funding') ) { + $disable_funding = $this->settings->get('disable_funding'); + if ( ! in_array('venmo', $disable_funding, true) ) { + $methods[] = 'venmo'; + } + } + + if ( $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' ) ) { + $methods[] = ApplePayGateway::ID; + } + + if ( $this->settings->has( 'googlepay_button_enabled' ) && $this->settings->get( 'googlepay_button_enabled' ) ) { + $methods[] = GooglePayGateway::ID; + } + + return $methods; + } + + /** + * Checks if a specific button type is enabled for a given location. + * + * @param string $location The location name ('cart', 'checkout', etc.). + * @param string $type The button type ('smart', 'pay_later', etc.). + * @return bool True if the button is enabled for the location, false otherwise. + */ + protected function is_button_enabled_for_location( string $location, string $type ): bool { + $key = "{$type}_button_locations"; + if ( ! $this->settings->has( $key ) ) { + return false; + } + + $enabled_locations = $this->settings->get( $key ); + + if ( $location === 'cart' ) { + return in_array( $location, $enabled_locations, true ) || in_array( 'cart-block', $enabled_locations, true ); + } + + return in_array( $location, $enabled_locations, true ); + } + + /** + * Returns a mapping of old button location names to new settings location names. + * + * @return string[] The mapping of old location names to new location names. + */ + protected function locations_map(): array { + return array( + 'product' => 'product', + 'cart' => 'cart', + 'checkout' => 'classic_checkout', + 'mini-cart' => 'mini_cart', + 'checkout-block-express' => 'express_checkout', + ); + } + + /** + * Determines the style value for a given property in a given context. + * + * Looks up style values with context-specific fallbacks. For cart context, + * also checks cart-block variants. + * + * @param string $style The name of the style property ('shape', 'label', 'color', etc.). + * @param string $location The location name ('cart', 'checkout', etc.). + * @return string|int|null The style value or null if not found. + */ + private function style_for_context( string $style, string $location ) { + if ( $location === 'cart' ) { + return $this->get_style_value( "button_{$location}_{$style}" ) + ?? $this->get_style_value( "button_cart-block_{$style}" ); + } + + return $this->get_style_value( "button_{$location}_{$style}" ) + ?? $this->get_style_value( "button_{$style}" ); + } + + /** + * Retrieves a style property value from legacy settings. + * + * @param string $key The style property key in the settings. + * @return string|int|null The style value or null if not found. + */ + private function get_style_value( string $key ) { + if ( ! $this->settings->has( $key ) ) { + return null; + } + return $this->settings->get( $key ); + } +} From 5b6dd660fe9cdf61aea97ee1df608fcaa4c85a39 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:40:44 +0400 Subject: [PATCH 16/65] Always enable the settings module for now --- modules.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/modules.php b/modules.php index 0d65644f2..3b4269491 100644 --- a/modules.php +++ b/modules.php @@ -10,6 +10,7 @@ namespace WooCommerce\PayPalCommerce; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule; use WooCommerce\PayPalCommerce\PayLaterConfigurator\PayLaterConfiguratorModule; +use WooCommerce\PayPalCommerce\Settings\SettingsModule; return function ( string $root_dir ): iterable { $modules_dir = "$root_dir/modules"; @@ -91,15 +92,7 @@ return function ( string $root_dir ): iterable { $modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); } - $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' ); - $preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' ); - - if ( apply_filters( - 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', - $show_new_ux || $preview_new_ux - ) ) { - $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); - } + $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); return $modules; }; From b763b6da3b0f27b8ee9ad91c20152696e98f9987 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:41:07 +0400 Subject: [PATCH 17/65] Conditionally add services --- modules/ppcp-settings/services.php | 80 +++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 1d676b7cf..5b51ab225 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -52,6 +52,11 @@ use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService; use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService; use WooCommerce\PayPalCommerce\Settings\Service\LoadingScreenService; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\GeneralSettingsMigration; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\MigrationManager; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\PaymentSettingsMigration; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\SettingsTabMigration; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\StylingSettingsMigration; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Settings\Service\ScriptDataHandler; use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService; @@ -72,7 +77,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService; -return array( +$services = array( 'settings.url' => static function ( ContainerInterface $container ) : string { /** * The path cannot be false. @@ -147,20 +152,6 @@ return array( 'save' => $save_config, ); }, - /** - * Merchant connection details, which includes the connection status - * (onboarding/connected) and connection-aware environment checks. - * This is the preferred solution to check environment and connection state. - */ - 'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState { - $data = $container->get( 'settings.data.general' ); - assert( $data instanceof GeneralSettings ); - - $is_connected = $data->is_merchant_connected(); - $environment = new Environment( $data->is_sandbox_merchant() ); - - return new ConnectionState( $is_connected, $environment ); - }, /** * Returns details about the connected environment (production/sandbox). * @@ -379,14 +370,38 @@ return array( $partner_attribution = $container->get( 'api.helper.partner-attribution' ); return new ScriptDataHandler( $settings, $settings_url, $paylater_is_available, $store_country, $merchant_id, $button_language_choices, $partner_attribution ); }, - 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { - return new SwitchSettingsUiEndpoint( - $container->get( 'woocommerce.logger.woocommerce' ), - $container->get( 'button.request-data' ), - $container->get( 'settings.data.onboarding' ), - $container->get( 'api.merchant_id' ) !== '' - ); - }, + 'settings.service.data-migration' => static fn( ContainerInterface $c ): MigrationManager => new MigrationManager( + $c->get( 'settings.service.data-migration.general-settings' ), + $c->get( 'settings.service.data-migration.settings-tab' ), + $c->get( 'settings.service.data-migration.styling' ), + $c->get( 'settings.service.data-migration.payment-settings' ), + ), + 'settings.service.data-migration.settings-tab' => static fn( ContainerInterface $c ): SettingsTabMigration => new SettingsTabMigration( + $c->get( 'wcgateway.settings' ), + $c->get( 'settings.data.settings' ), + $c->get( 'compat.settings.settings_tab_map_helper' ), + ), + 'settings.service.data-migration.styling' => static fn( ContainerInterface $c ): StylingSettingsMigration => new StylingSettingsMigration( + $c->get( 'wcgateway.settings' ), + $c->get( 'settings.data.styling' ), + ), + 'settings.service.data-migration.payment-settings' => static fn( ContainerInterface $c ): PaymentSettingsMigration => new PaymentSettingsMigration( + $c->get( 'wcgateway.settings' ), + $c->get( 'settings.data.payment' ), + $c->get( 'ppcp-local-apms.payment-methods' ), + ), + 'settings.service.data-migration.general-settings' => static fn( ContainerInterface $c ): GeneralSettingsMigration => new GeneralSettingsMigration( + $c->get( 'wcgateway.settings' ), + $c->get( 'settings.data.general' ), + $c->get( 'api.endpoint.partners' ), + ), + 'settings.ajax.switch_ui' => static fn ( ContainerInterface $c ): SwitchSettingsUiEndpoint => new SwitchSettingsUiEndpoint( + $c->get( 'woocommerce.logger.woocommerce' ), + $c->get( 'button.request-data' ), + $c->get( 'settings.data.onboarding' ), + $c->get( 'settings.service.data-migration' ), + $c->get( 'api.merchant_id' ) !== '' + ), 'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint { return new TodosRestEndpoint( $container->get( 'settings.data.todos' ), @@ -664,3 +679,22 @@ return array( ); }, ); + +if ( ! SettingsModule::should_use_the_old_ui() ) { + /** + * Merchant connection details, which includes the connection status + * (onboarding/connected) and connection-aware environment checks. + * This is the preferred solution to check environment and connection state. + */ + $services['settings.connection-state'] = static function ( ContainerInterface $container ) : ConnectionState { + $data = $container->get( 'settings.data.general' ); + assert( $data instanceof GeneralSettings ); + + $is_connected = $data->is_merchant_connected(); + $environment = new Environment( $data->is_sandbox_merchant() ); + + return new ConnectionState( $is_connected, $environment ); + }; +} + +return $services; From 00c854c5a1763cf2444f36f29ba5d56939e27c04 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 13:41:38 +0400 Subject: [PATCH 18/65] Update the settings module. --- modules/ppcp-settings/src/SettingsModule.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index af2db7804..233f11928 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -35,6 +35,7 @@ use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\PathRepository; use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService; use WooCommerce\PayPalCommerce\Settings\Service\LoadingScreenService; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\MigrationManager; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; @@ -70,7 +71,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { } // Existing merchants can opt-in to see the new UI. - $opt_out_choice = 'yes' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ); + $opt_out_choice = 'yes' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI );; return apply_filters( 'woocommerce_paypal_payments_should_use_the_old_ui', @@ -135,15 +136,14 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); - $endpoint = $container->get( 'settings.ajax.switch_ui' ) ? $container->get( 'settings.ajax.switch_ui' ) : null; - assert( $endpoint instanceof SwitchSettingsUiEndpoint ); - add_action( 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT, - array( - $endpoint, - 'handle_request', - ) + static function () use ($container): void { + $endpoint = $container->get( 'settings.ajax.switch_ui' ) ? $container->get( 'settings.ajax.switch_ui' ) : null; + assert( $endpoint instanceof SwitchSettingsUiEndpoint ); + + $endpoint->handle_request(); + } ); return true; From d42488ed8ed63f3a029cb264ce4b096b2c97b911 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 17:22:34 +0400 Subject: [PATCH 19/65] disable the sync after onboarding --- .../ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php | 7 +------ .../src/Service/Migration/MigrationManager.php | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php index eb072b3bb..7f0a9cecf 100644 --- a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php @@ -60,22 +60,17 @@ class SwitchSettingsUiEndpoint { return; } - update_option( MigrationManager::OPTION_NAME_IS_MIGRATION_RUNNING, true ); - try { $this->request_data->read_request( $this->nonce() ); update_option( self::OPTION_NAME_SHOULD_USE_OLD_UI, 'no' ); $this->onboarding_profile->set_completed( true ); - $this->onboarding_profile->set_gateways_synced( true ); + $this->onboarding_profile->set_gateways_refreshed(true); $this->onboarding_profile->save(); $this->settings_data_migration->migrate(); - - delete_option(MigrationManager::OPTION_NAME_IS_MIGRATION_RUNNING); wp_send_json_success(); } catch ( Exception $error ) { - delete_option(MigrationManager::OPTION_NAME_IS_MIGRATION_RUNNING); wp_send_json_error(array('message' => $error->getMessage()), 500); } } diff --git a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php index 045c51718..3c4cc0d8b 100644 --- a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php +++ b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php @@ -16,8 +16,6 @@ namespace WooCommerce\PayPalCommerce\Settings\Service\Migration; */ class MigrationManager { - public const OPTION_NAME_IS_MIGRATION_RUNNING = 'woocommerce_ppcp-settings-is-migration-running'; - protected GeneralSettingsMigration $general_settings_migration; protected SettingsTabMigration $settings_tab_migration; protected StylingSettingsMigration $styling_settings_migration; From 498f9436079b117c0c7e3d990430a2c3e57df314 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 18:08:28 +0400 Subject: [PATCH 20/65] Fix the CS --- modules/ppcp-settings/services.php | 12 +++--- .../src/Ajax/SwitchSettingsUiEndpoint.php | 4 +- .../Migration/GeneralSettingsMigration.php | 40 +++++++++---------- .../Service/Migration/MigrationManager.php | 2 +- .../Migration/PaymentSettingsMigration.php | 14 +++---- .../Migration/SettingsTabMigration.php | 22 +++++----- .../Migration/StylingSettingsMigration.php | 18 ++++----- modules/ppcp-settings/src/SettingsModule.php | 4 +- 8 files changed, 57 insertions(+), 59 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 8f110a29f..791967395 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -78,7 +78,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService; use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails; -$services = array( +$services = array( 'settings.url' => static function ( ContainerInterface $container ) : string { /** * The path cannot be false. @@ -371,27 +371,27 @@ $services = array( $partner_attribution = $container->get( 'api.helper.partner-attribution' ); return new ScriptDataHandler( $settings, $settings_url, $paylater_is_available, $store_country, $merchant_id, $button_language_choices, $partner_attribution ); }, - 'settings.service.data-migration' => static fn( ContainerInterface $c ): MigrationManager => new MigrationManager( + 'settings.service.data-migration' => static fn( ContainerInterface $c ): MigrationManager => new MigrationManager( $c->get( 'settings.service.data-migration.general-settings' ), $c->get( 'settings.service.data-migration.settings-tab' ), $c->get( 'settings.service.data-migration.styling' ), $c->get( 'settings.service.data-migration.payment-settings' ), ), - 'settings.service.data-migration.settings-tab' => static fn( ContainerInterface $c ): SettingsTabMigration => new SettingsTabMigration( + 'settings.service.data-migration.settings-tab' => static fn( ContainerInterface $c ): SettingsTabMigration => new SettingsTabMigration( $c->get( 'wcgateway.settings' ), $c->get( 'settings.data.settings' ), $c->get( 'compat.settings.settings_tab_map_helper' ), ), - 'settings.service.data-migration.styling' => static fn( ContainerInterface $c ): StylingSettingsMigration => new StylingSettingsMigration( + 'settings.service.data-migration.styling' => static fn( ContainerInterface $c ): StylingSettingsMigration => new StylingSettingsMigration( $c->get( 'wcgateway.settings' ), $c->get( 'settings.data.styling' ), ), - 'settings.service.data-migration.payment-settings' => static fn( ContainerInterface $c ): PaymentSettingsMigration => new PaymentSettingsMigration( + 'settings.service.data-migration.payment-settings' => static fn( ContainerInterface $c ): PaymentSettingsMigration => new PaymentSettingsMigration( $c->get( 'wcgateway.settings' ), $c->get( 'settings.data.payment' ), $c->get( 'ppcp-local-apms.payment-methods' ), ), - 'settings.service.data-migration.general-settings' => static fn( ContainerInterface $c ): GeneralSettingsMigration => new GeneralSettingsMigration( + 'settings.service.data-migration.general-settings' => static fn( ContainerInterface $c ): GeneralSettingsMigration => new GeneralSettingsMigration( $c->get( 'wcgateway.settings' ), $c->get( 'settings.data.general' ), $c->get( 'api.endpoint.partners' ), diff --git a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php index 7f0a9cecf..a8ba2a967 100644 --- a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php @@ -65,13 +65,13 @@ class SwitchSettingsUiEndpoint { update_option( self::OPTION_NAME_SHOULD_USE_OLD_UI, 'no' ); $this->onboarding_profile->set_completed( true ); - $this->onboarding_profile->set_gateways_refreshed(true); + $this->onboarding_profile->set_gateways_refreshed( true ); $this->onboarding_profile->save(); $this->settings_data_migration->migrate(); wp_send_json_success(); } catch ( Exception $error ) { - wp_send_json_error(array('message' => $error->getMessage()), 500); + wp_send_json_error( array( 'message' => $error->getMessage() ), 500 ); } } diff --git a/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php index 3ae66489b..1cb5c222d 100644 --- a/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php @@ -32,8 +32,8 @@ class GeneralSettingsMigration { GeneralSettings $general_settings, PartnersEndpoint $partners_endpoint ) { - $this->settings = $settings; - $this->general_settings = $general_settings; + $this->settings = $settings; + $this->general_settings = $general_settings; $this->partners_endpoint = $partners_endpoint; } @@ -43,9 +43,9 @@ class GeneralSettingsMigration { * @return void */ public function migrate(): void { - if ( !$this->settings->has( 'client_id' ) - || !$this->settings->has( 'client_secret' ) - || !$this->settings->has( 'merchant_id' ) ) { + if ( ! $this->settings->has( 'client_id' ) + || ! $this->settings->has( 'client_secret' ) + || ! $this->settings->has( 'merchant_id' ) ) { return; } @@ -72,29 +72,28 @@ class GeneralSettingsMigration { * @param SellerStatus $seller_status * @return 'business' | 'unknown' The merchant account type (SellerTypeEnum constant). */ - protected function merchant_account_type(SellerStatus $seller_status): string - { - if ($this->has_capability_active($seller_status, 'COMMERCIAL_ENTITY')) { + protected function merchant_account_type( SellerStatus $seller_status ): string { + if ( $this->has_capability_active( $seller_status, 'COMMERCIAL_ENTITY' ) ) { return SellerTypeEnum::BUSINESS; } - $business_capabilities = [ + $business_capabilities = array( 'CUSTOM_CARD_PROCESSING', 'CARD_PROCESSING_VIRTUAL_TERMINAL', 'FRAUD_TOOL_ACCESS', 'PAY_UPON_INVOICE', - 'SEND_INVOICE' - ]; + 'SEND_INVOICE', + ); - foreach ($business_capabilities as $capability) { - if ($this->has_capability_active($seller_status, $capability)) { + foreach ( $business_capabilities as $capability ) { + if ( $this->has_capability_active( $seller_status, $capability ) ) { return SellerTypeEnum::BUSINESS; } } - foreach ($seller_status->products() as $product) { - if ($product->name() === 'PPCP_CUSTOM' && - $product->vetting_status() === 'SUBSCRIBED') { + foreach ( $seller_status->products() as $product ) { + if ( $product->name() === 'PPCP_CUSTOM' && + $product->vetting_status() === 'SUBSCRIBED' ) { return SellerTypeEnum::BUSINESS; } } @@ -109,11 +108,10 @@ class GeneralSettingsMigration { * @param string $capability_name * @return bool True if the capability is active, false otherwise. */ - private function has_capability_active(SellerStatus $seller_status, string $capability_name): bool - { - foreach ($seller_status->capabilities() as $capability) { - if ($capability->name() === $capability_name && - $capability->status() === 'ACTIVE') { + private function has_capability_active( SellerStatus $seller_status, string $capability_name ): bool { + foreach ( $seller_status->capabilities() as $capability ) { + if ( $capability->name() === $capability_name && + $capability->status() === 'ACTIVE' ) { return true; } } diff --git a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php index 3c4cc0d8b..b69870fd7 100644 --- a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php +++ b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php @@ -28,7 +28,7 @@ class MigrationManager { PaymentSettingsMigration $payment_settings_migration ) { $this->general_settings_migration = $general_settings_migration; - $this->settings_tab_migration = $settings_tab_migration; + $this->settings_tab_migration = $settings_tab_migration; $this->styling_settings_migration = $styling_settings_migration; $this->payment_settings_migration = $payment_settings_migration; } diff --git a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php index 7e658aca5..63235d511 100644 --- a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php @@ -38,9 +38,9 @@ class PaymentSettingsMigration { PaymentSettings $payment_settings, array $local_apms ) { - $this->settings = $settings; - $this->payment_settings = $payment_settings; - $this->local_apms = $local_apms; + $this->settings = $settings; + $this->payment_settings = $payment_settings; + $this->local_apms = $local_apms; } /** @@ -49,14 +49,14 @@ class PaymentSettingsMigration { * @return void */ public function migrate(): void { - if ( $this->settings->has('disable_funding') ) { - $disable_funding = $this->settings->get('disable_funding'); - if ( ! in_array('venmo', $disable_funding, true) ) { + if ( $this->settings->has( 'disable_funding' ) ) { + $disable_funding = $this->settings->get( 'disable_funding' ); + if ( ! in_array( 'venmo', $disable_funding, true ) ) { $this->payment_settings->toggle_method_state( 'venmo', true ); } foreach ( $this->local_apms as $apm ) { - if ( ! in_array($apm['id'], $disable_funding, true) ) { + if ( ! in_array( $apm['id'], $disable_funding, true ) ) { $this->payment_settings->toggle_method_state( $apm['id'], true ); } } diff --git a/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php b/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php index 177ee4993..3707ca864 100644 --- a/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php @@ -31,8 +31,8 @@ class SettingsTabMigration { SettingsModel $settings_tab, SettingsTabMapHelper $settings_tab_map_helper ) { - $this->settings = $settings; - $this->settings_tab = $settings_tab; + $this->settings = $settings; + $this->settings_tab = $settings_tab; $this->settings_tab_map_helper = $settings_tab_map_helper; } @@ -44,19 +44,19 @@ class SettingsTabMigration { public function migrate(): void { $data = array(); - foreach ($this->settings_tab_map_helper->map() as $old_key => $new_key) { + foreach ( $this->settings_tab_map_helper->map() as $old_key => $new_key ) { if ( ! $this->settings->has( $old_key ) ) { continue; } switch ( $old_key ) { case 'subtotal_mismatch_behavior': - $value = $this->settings->get( $old_key ); - $data[$new_key] = $value === PurchaseUnitSanitizer::MODE_EXTRA_LINE ? 'correction' : 'no_details'; + $value = $this->settings->get( $old_key ); + $data[ $new_key ] = $value === PurchaseUnitSanitizer::MODE_EXTRA_LINE ? 'correction' : 'no_details'; break; case 'landing_page': - $value = $this->settings->get( $old_key ); - $data[$new_key] = $value === ExperienceContext::LANDING_PAGE_LOGIN + $value = $this->settings->get( $old_key ); + $data[ $new_key ] = $value === ExperienceContext::LANDING_PAGE_LOGIN ? 'login' : ( $value === ExperienceContext::LANDING_PAGE_GUEST_CHECKOUT ? 'guest_checkout' @@ -64,19 +64,19 @@ class SettingsTabMigration { ); break; case 'intent': - $value = $this->settings->get( $old_key ); + $value = $this->settings->get( $old_key ); $data['authorize_only'] = $value === 'AUTHORIZE'; $data['capture_virtual_orders'] = $value === 'CAPTURE'; break; case 'blocks_final_review_enabled': - $data[$new_key] = ! $this->settings->get( $old_key ); + $data[ $new_key ] = ! $this->settings->get( $old_key ); break; default: - $data[$new_key] = $this->settings->get( $old_key ); + $data[ $new_key ] = $this->settings->get( $old_key ); } } - $this->settings_tab->from_array($data); + $this->settings_tab->from_array( $data ); $this->settings_tab->save(); } } diff --git a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php index 3800c0b03..9bc141432 100644 --- a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php @@ -26,9 +26,9 @@ class StylingSettingsMigration { protected Settings $settings; protected StylingSettings $styling_settings; - public function __construct( Settings $settings, StylingSettings $styling_settings ) { - $this->settings = $settings; - $this->styling_settings = $styling_settings; + public function __construct( Settings $settings, StylingSettings $styling_settings ) { + $this->settings = $settings; + $this->styling_settings = $styling_settings; } /** @@ -39,12 +39,12 @@ class StylingSettingsMigration { public function migrate(): void { $location_styles = array(); - $styling_per_location = $this->settings->has('smart_button_enable_styling_per_location') && $this->settings->get('smart_button_enable_styling_per_location'); + $styling_per_location = $this->settings->has( 'smart_button_enable_styling_per_location' ) && $this->settings->get( 'smart_button_enable_styling_per_location' ); foreach ( $this->locations_map() as $old_location => $new_location ) { $context = $styling_per_location ? $old_location : 'general'; - $location_styles[$new_location] = new LocationStylingDTO( + $location_styles[ $new_location ] = new LocationStylingDTO( $new_location, $this->is_button_enabled_for_location( $old_location, 'smart' ), $this->enabled_methods( $old_location ), @@ -70,15 +70,15 @@ class StylingSettingsMigration { * @return string[] The list of enabled payment method IDs. */ protected function enabled_methods( string $location ): array { - $methods = array(PayPalGateway::ID); + $methods = array( PayPalGateway::ID ); if ( $this->is_button_enabled_for_location( $location, 'pay_later' ) ) { $methods[] = 'pay-later'; } - if ( $this->settings->has('disable_funding') ) { - $disable_funding = $this->settings->get('disable_funding'); - if ( ! in_array('venmo', $disable_funding, true) ) { + if ( $this->settings->has( 'disable_funding' ) ) { + $disable_funding = $this->settings->get( 'disable_funding' ); + if ( ! in_array( 'venmo', $disable_funding, true ) ) { $methods[] = 'venmo'; } } diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index ab68885f2..110a7ebb7 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -72,7 +72,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { } // Existing merchants can opt-in to see the new UI. - $opt_out_choice = 'yes' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI );; + $opt_out_choice = 'yes' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ); return apply_filters( 'woocommerce_paypal_payments_should_use_the_old_ui', @@ -139,7 +139,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_action( 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT, - static function () use ($container): void { + static function () use ( $container ): void { $endpoint = $container->get( 'settings.ajax.switch_ui' ) ? $container->get( 'settings.ajax.switch_ui' ) : null; assert( $endpoint instanceof SwitchSettingsUiEndpoint ); From 509bd9ec207dd43b614e07c0bb8bb0c7dafe0121 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Tue, 24 Jun 2025 18:18:33 +0400 Subject: [PATCH 21/65] Fix the psalm --- .../Service/Migration/StylingSettingsMigration.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php index 9bc141432..9afce2813 100644 --- a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php @@ -48,10 +48,10 @@ class StylingSettingsMigration { $new_location, $this->is_button_enabled_for_location( $old_location, 'smart' ), $this->enabled_methods( $old_location ), - $this->style_for_context( 'shape', $context ) ?? 'rect', - $this->style_for_context( 'label', $context ) ?? 'pay', - $this->style_for_context( 'color', $context ) ?? 'gold', - $this->style_for_context( 'layout', $context ) ?? 'vertical', + (string) ( $this->style_for_context( 'shape', $context ) ?? 'rect' ), + (string) ( $this->style_for_context( 'label', $context ) ?? 'pay' ), + (string) ( $this->style_for_context( 'color', $context ) ?? 'gold' ), + (string) ( $this->style_for_context( 'layout', $context ) ?? 'vertical' ), (bool) ( $this->style_for_context( 'tagline', $context ) ?? false ) ); } @@ -139,7 +139,7 @@ class StylingSettingsMigration { * * @param string $style The name of the style property ('shape', 'label', 'color', etc.). * @param string $location The location name ('cart', 'checkout', etc.). - * @return string|int|null The style value or null if not found. + * @return string|bool|null The style value or null if not found. */ private function style_for_context( string $style, string $location ) { if ( $location === 'cart' ) { @@ -155,7 +155,7 @@ class StylingSettingsMigration { * Retrieves a style property value from legacy settings. * * @param string $key The style property key in the settings. - * @return string|int|null The style value or null if not found. + * @return string|bool|null The style value or null if not found. */ private function get_style_value( string $key ) { if ( ! $this->settings->has( $key ) ) { From 25f2bf93e2d381dc7e7bedc04ce00bb50500ef4f Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Wed, 25 Jun 2025 12:43:59 +0200 Subject: [PATCH 22/65] Merge branch 'trunk' of github.com:woocommerce/woocommerce-paypal-payments into PCP-4487-fastlane-uk-3ds-redirect From 53f533fa25afa12d49cf11ef5eedd747cd76bb44 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 25 Jun 2025 16:35:50 +0400 Subject: [PATCH 23/65] Add the new settings discovery notice --- modules/ppcp-settings/src/SettingsModule.php | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 110a7ebb7..d2029568f 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -11,6 +11,8 @@ namespace WooCommerce\PayPalCommerce\Settings; use WC_Payment_Gateway; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; +use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution; use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; @@ -100,6 +102,29 @@ class SettingsModule implements ServiceModule, ExecutableModule { ) ); + /** + * Adds new settings discovery notice. + * + * @param Message[] $notices + * @return Message[] + */ + add_filter( + Repository::NOTICES_FILTER, + static function ( array $notices ): array { + $message = sprintf( + // translators: %1$s is the URL for the startup guide. + __( + '🎉 Discover the new PayPal Payments settings! Enjoy a cleaner, faster interface. Check out the Startup Guide, then click Switch to New Settings to activate it.', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/paypal-payments-startup-guide/' + ); + + $notices[] = new Message( $message, 'info', false, 'ppcp-notice-wrapper' ); + return $notices; + } + ); + add_action( 'admin_enqueue_scripts', static function () use ( $container ) { From 5ff91fb1716bd3f0322af4b432bf501f79099573 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 25 Jun 2025 16:37:22 +0400 Subject: [PATCH 24/65] Adjust the switch UI JS. - Now the same behavior will also work when the link is clicked from the info notice - Added the confirmation alert --- .../resources/js/switchSettingsUi.js | 23 ++++++++++++++++--- modules/ppcp-settings/src/SettingsModule.php | 8 +++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/switchSettingsUi.js b/modules/ppcp-settings/resources/js/switchSettingsUi.js index 9304ceb30..d7d77d4dd 100644 --- a/modules/ppcp-settings/resources/js/switchSettingsUi.js +++ b/modules/ppcp-settings/resources/js/switchSettingsUi.js @@ -3,12 +3,21 @@ document.addEventListener( 'DOMContentLoaded', () => { const button = document.querySelector( '.button.button-settings-switch-ui' ); + const link = document.querySelector( 'a.settings-switch-ui' ); - if ( ! typeof config || ! button ) { + if ( ! typeof config || ( ! button && ! link ) ) { return; } - button.addEventListener( 'click', () => { + const handleClick = ( event ) => { + event.preventDefault(); + + const confirmed = confirm( config.confirmMessage ); + + if ( ! confirmed ) { + return; + } + fetch( config.endpoint, { method: 'POST', headers: { @@ -30,5 +39,13 @@ document.addEventListener( 'DOMContentLoaded', () => { .catch( ( error ) => { console.error( 'Error:', error ); } ); - } ); + }; + + if ( button ) { + button.addEventListener( 'click', handleClick ); + } + + if ( link ) { + link.addEventListener( 'click', handleClick ); + } } ); diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index d2029568f..27c81bf95 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -149,8 +149,12 @@ class SettingsModule implements ServiceModule, ExecutableModule { 'ppcp-switch-settings-ui', 'ppcpSwitchSettingsUi', array( - 'endpoint' => \WC_AJAX::get_endpoint( SwitchSettingsUiEndpoint::ENDPOINT ), - 'nonce' => wp_create_nonce( SwitchSettingsUiEndpoint::nonce() ), + 'endpoint' => \WC_AJAX::get_endpoint( SwitchSettingsUiEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( SwitchSettingsUiEndpoint::nonce() ), + 'confirmMessage' => __( + 'Are you sure you want to switch to the new settings interface?This action cannot be undone.', + 'woocommerce-paypal-payments' + ), ) ); From 7334b5bfbb4a13b7502018de71ea21f4cffcff54 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 25 Jun 2025 16:43:59 +0400 Subject: [PATCH 25/65] Set the gateways synced to `true` after onboarding through migration. This will prevent override of enabled gateways after migration --- modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php index a8ba2a967..d5d5fb0da 100644 --- a/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php @@ -66,6 +66,7 @@ class SwitchSettingsUiEndpoint { $this->onboarding_profile->set_completed( true ); $this->onboarding_profile->set_gateways_refreshed( true ); + $this->onboarding_profile->set_gateways_synced( true ); $this->onboarding_profile->save(); $this->settings_data_migration->migrate(); From 21b0d8d9ae48fb61f0a98aaf8b39482da2bee118 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 25 Jun 2025 16:55:45 +0400 Subject: [PATCH 26/65] Introduce the interface for settings migration --- modules/ppcp-settings/services.php | 4 ++-- .../Service/Migration/MigrationManager.php | 11 +++------ .../Migration/PaymentSettingsMigration.php | 9 ++----- ...ngsMigration.php => SettingsMigration.php} | 9 ++----- .../Migration/SettingsMigrationInterface.php | 24 +++++++++++++++++++ .../Migration/SettingsTabMigration.php | 7 +----- .../Migration/StylingSettingsMigration.php | 7 +----- 7 files changed, 35 insertions(+), 36 deletions(-) rename modules/ppcp-settings/src/Service/Migration/{GeneralSettingsMigration.php => SettingsMigration.php} (94%) create mode 100644 modules/ppcp-settings/src/Service/Migration/SettingsMigrationInterface.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 791967395..0a00db31e 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -52,7 +52,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService; use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService; use WooCommerce\PayPalCommerce\Settings\Service\LoadingScreenService; -use WooCommerce\PayPalCommerce\Settings\Service\Migration\GeneralSettingsMigration; +use WooCommerce\PayPalCommerce\Settings\Service\Migration\SettingsMigration; use WooCommerce\PayPalCommerce\Settings\Service\Migration\MigrationManager; use WooCommerce\PayPalCommerce\Settings\Service\Migration\PaymentSettingsMigration; use WooCommerce\PayPalCommerce\Settings\Service\Migration\SettingsTabMigration; @@ -391,7 +391,7 @@ $services = array( $c->get( 'settings.data.payment' ), $c->get( 'ppcp-local-apms.payment-methods' ), ), - 'settings.service.data-migration.general-settings' => static fn( ContainerInterface $c ): GeneralSettingsMigration => new GeneralSettingsMigration( + 'settings.service.data-migration.general-settings' => static fn( ContainerInterface $c ): SettingsMigration => new SettingsMigration( $c->get( 'wcgateway.settings' ), $c->get( 'settings.data.general' ), $c->get( 'api.endpoint.partners' ), diff --git a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php index b69870fd7..561d52766 100644 --- a/modules/ppcp-settings/src/Service/Migration/MigrationManager.php +++ b/modules/ppcp-settings/src/Service/Migration/MigrationManager.php @@ -14,15 +14,15 @@ namespace WooCommerce\PayPalCommerce\Settings\Service\Migration; * * Manages migration operations for plugin settings. */ -class MigrationManager { +class MigrationManager implements SettingsMigrationInterface { - protected GeneralSettingsMigration $general_settings_migration; + protected SettingsMigration $general_settings_migration; protected SettingsTabMigration $settings_tab_migration; protected StylingSettingsMigration $styling_settings_migration; protected PaymentSettingsMigration $payment_settings_migration; public function __construct( - GeneralSettingsMigration $general_settings_migration, + SettingsMigration $general_settings_migration, SettingsTabMigration $settings_tab_migration, StylingSettingsMigration $styling_settings_migration, PaymentSettingsMigration $payment_settings_migration @@ -33,11 +33,6 @@ class MigrationManager { $this->payment_settings_migration = $payment_settings_migration; } - /** - * Executes migration for all settings areas/tabs. - * - * @return void - */ public function migrate(): void { $this->general_settings_migration->migrate(); $this->settings_tab_migration->migrate(); diff --git a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php index 63235d511..f0284cf8f 100644 --- a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php @@ -21,7 +21,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; * * Handles migration of payment settings. */ -class PaymentSettingsMigration { +class PaymentSettingsMigration implements SettingsMigrationInterface { protected Settings $settings; protected PaymentSettings $payment_settings; @@ -43,11 +43,6 @@ class PaymentSettingsMigration { $this->local_apms = $local_apms; } - /** - * Migrates legacy payment settings to new data structure. - * - * @return void - */ public function migrate(): void { if ( $this->settings->has( 'disable_funding' ) ) { $disable_funding = $this->settings->get( 'disable_funding' ); @@ -72,7 +67,7 @@ class PaymentSettingsMigration { } /** - * Maps old setting keys to new payment method settings names. + * Maps old setting keys to new payment method names. * * @return array */ diff --git a/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/SettingsMigration.php similarity index 94% rename from modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php rename to modules/ppcp-settings/src/Service/Migration/SettingsMigration.php index 1cb5c222d..ef0437de0 100644 --- a/modules/ppcp-settings/src/Service/Migration/GeneralSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/SettingsMigration.php @@ -1,6 +1,6 @@ partners_endpoint = $partners_endpoint; } - /** - * Migrates legacy connection settings to new data structure. - * - * @return void - */ public function migrate(): void { if ( ! $this->settings->has( 'client_id' ) || ! $this->settings->has( 'client_secret' ) diff --git a/modules/ppcp-settings/src/Service/Migration/SettingsMigrationInterface.php b/modules/ppcp-settings/src/Service/Migration/SettingsMigrationInterface.php new file mode 100644 index 000000000..5c6595531 --- /dev/null +++ b/modules/ppcp-settings/src/Service/Migration/SettingsMigrationInterface.php @@ -0,0 +1,24 @@ +settings_tab_map_helper = $settings_tab_map_helper; } - /** - * Migrates legacy settings tab settings to new data structure. - * - * @return void - */ public function migrate(): void { $data = array(); diff --git a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php index 9afce2813..f93212e65 100644 --- a/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/StylingSettingsMigration.php @@ -21,7 +21,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; * * Handles migration of styling settings. */ -class StylingSettingsMigration { +class StylingSettingsMigration implements SettingsMigrationInterface { protected Settings $settings; protected StylingSettings $styling_settings; @@ -31,11 +31,6 @@ class StylingSettingsMigration { $this->styling_settings = $styling_settings; } - /** - * Migrates legacy styling settings to new data structure. - * - * @return void - */ public function migrate(): void { $location_styles = array(); From ee9bb113bc65e4428f90f103d4358ce360e8279c Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 25 Jun 2025 18:31:36 +0400 Subject: [PATCH 27/65] Fix the 'threeDSecure' migration --- .../ppcp-compat/src/Settings/SettingsTabMapHelper.php | 6 +++--- .../src/Service/Migration/SettingsTabMigration.php | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php index 3d032a9fb..a4504e76b 100644 --- a/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php @@ -28,7 +28,7 @@ class SettingsTabMapHelper { * * @var array */ - protected const THREE_D_SECURE_VALUES_MAP = array( + public const THREE_D_SECURE_VALUES_MAP = array( 'no-3d-secure' => 'NO_3D_SECURE', 'only-required-3d-secure' => 'SCA_WHEN_REQUIRED', 'always-3d-secure' => 'SCA_ALWAYS', @@ -54,7 +54,7 @@ class SettingsTabMapHelper { 'blocks_final_review_enabled' => 'enable_pay_now', 'logging_enabled' => 'enable_logging', 'vault_enabled' => 'save_paypal_and_venmo', - '3d_secure_contingency' => 'threeDSecure', + '3d_secure_contingency' => 'three_d_secure', ); } @@ -96,7 +96,7 @@ class SettingsTabMapHelper { * @return string|null The mapped '3d_secure_contingency' setting value. */ protected function mapped_3d_secure_value( array $settings_model ): ?string { - $three_d_secure = $settings_model['threeDSecure'] ?? null; + $three_d_secure = $settings_model['three_d_secure'] ?? null; if ( ! is_string( $three_d_secure ) ) { return null; diff --git a/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php b/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php index c3d4cc32b..bf10dfb0a 100644 --- a/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/SettingsTabMigration.php @@ -60,12 +60,17 @@ class SettingsTabMigration implements SettingsMigrationInterface { break; case 'intent': $value = $this->settings->get( $old_key ); - $data['authorize_only'] = $value === 'AUTHORIZE'; - $data['capture_virtual_orders'] = $value === 'CAPTURE'; + $data['authorize_only'] = $value === 'authorize'; + $data['capture_virtual_orders'] = $value === 'capture'; break; case 'blocks_final_review_enabled': $data[ $new_key ] = ! $this->settings->get( $old_key ); break; + case '3d_secure_contingency': + $value = $this->settings->get( $old_key ); + $old_to_new_3d_secure_map = array_flip( SettingsTabMapHelper::THREE_D_SECURE_VALUES_MAP ); + $data[ $new_key ] = $old_to_new_3d_secure_map[ $value ] ?? 'NO_3D_SECURE'; + break; default: $data[ $new_key ] = $this->settings->get( $old_key ); } From f463f90ff0dc13cabd966d87419f808e2b5f1e0f Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 12:12:20 +0200 Subject: [PATCH 28/65] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20the=20Ord?= =?UTF-8?q?erFactory=20to=20remove=20duplicate=20code=20and=20make=20the?= =?UTF-8?q?=20code=20more=20modular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/services.php | 4 +- .../src/Factory/OrderFactory.php | 302 +++++++++++------- .../src/Endpoint/CreateOrderEndpoint.php | 34 +- 3 files changed, 201 insertions(+), 139 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index c8ba6812f..95599638e 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -445,11 +445,9 @@ return array( 'api.factory.order' => static function ( ContainerInterface $container ): OrderFactory { $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $payer_factory = $container->get( 'api.factory.payer' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new OrderFactory( $purchase_unit_factory, - $payer_factory, - $logger + $payer_factory ); }, 'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory { diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 2b1a8fdd6..a9d39a636 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; -use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; @@ -35,29 +34,21 @@ class OrderFactory { */ private $payer_factory; - /** - * The logger. - * - * @var LoggerInterface - */ - private $logger; + /** * OrderFactory constructor. * * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. * @param PayerFactory $payer_factory The Payer factory. - * @param LoggerInterface $logger The logger. */ public function __construct( PurchaseUnitFactory $purchase_unit_factory, - PayerFactory $payer_factory, - LoggerInterface $logger + PayerFactory $payer_factory ) { $this->purchase_unit_factory = $purchase_unit_factory; $this->payer_factory = $payer_factory; - $this->logger = $logger; } /** @@ -92,150 +83,223 @@ class OrderFactory { * @throws RuntimeException When JSON object is malformed. */ public function from_paypal_response( \stdClass $order_data ): Order { + return $this->create_order_from_response( $order_data, false ); + } + + /** + * Returns an Order object based off a PayPal 3DS Response. + * + * @param \stdClass $order_data The JSON object. + * + * @return Order + * @throws RuntimeException When JSON object is malformed. + */ + public function from_paypal_response_with_3ds( \stdClass $order_data ): Order { + return $this->create_order_from_response( $order_data, true ); + } + + /** + * Creates an Order object from PayPal response data. + * + * @param \stdClass $order_data The JSON object. + * @param bool $is_3ds_response Whether this is a 3DS response. + * + * @return Order + * @throws RuntimeException When JSON object is malformed. + */ + private function create_order_from_response( \stdClass $order_data, bool $is_3ds_response ): Order { + $this->validate_order_id( $order_data ); + + $purchase_units = $this->create_purchase_units( $order_data, $is_3ds_response ); + $status = $this->create_order_status( $order_data, $is_3ds_response ); + $intent = $this->get_intent( $order_data, $is_3ds_response ); + $timestamps = $this->create_timestamps( $order_data, $is_3ds_response ); + $payer = $this->create_payer( $order_data, $is_3ds_response ); + $payment_source = $this->create_payment_source( $order_data ); + $links = $is_3ds_response ? ( $order_data->links ?? null ) : null; + + return new Order( + $order_data->id, + $purchase_units, + $status, + $payment_source, + $payer, + $intent, + $timestamps['create_time'], + $timestamps['update_time'], + $links + ); + } + + /** + * Validates that the order data contains a required ID. + * + * @param \stdClass $order_data The order data. + * + * @throws RuntimeException When ID is missing. + */ + private function validate_order_id( \stdClass $order_data ): void { if ( ! isset( $order_data->id ) ) { throw new RuntimeException( __( 'Order does not contain an id.', 'woocommerce-paypal-payments' ) ); } + } + + /** + * Creates purchase units from order data. + * + * @param \stdClass $order_data The order data. + * @param bool $is_3ds_response Whether this is a 3DS response. + * + * @return array Array of PurchaseUnit objects. + * @throws RuntimeException When purchase units are required but missing. + */ + private function create_purchase_units( \stdClass $order_data, bool $is_3ds_response ): array { + // 3DS responses don't contain purchase units. + if ( $is_3ds_response ) { + return array(); + } + if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) { throw new RuntimeException( __( 'Order does not contain items.', 'woocommerce-paypal-payments' ) ); } + + return array_map( + function ( \stdClass $data ): PurchaseUnit { + return $this->purchase_unit_factory->from_paypal_response( $data ); + }, + $order_data->purchase_units + ); + } + + /** + * Creates order status from order data. + * + * @param \stdClass $order_data The order data. + * @param bool $is_3ds_response Whether this is a 3DS response. + * + * @return OrderStatus + * @throws RuntimeException When status is required but missing. + */ + private function create_order_status( \stdClass $order_data, bool $is_3ds_response ): OrderStatus { + if ( $is_3ds_response ) { + $status_value = $order_data->status ?? 'PAYER_ACTION_REQUIRED'; + return new OrderStatus( $status_value ); + } + if ( ! isset( $order_data->status ) ) { throw new RuntimeException( __( 'Order does not contain status.', 'woocommerce-paypal-payments' ) ); } + return new OrderStatus( $order_data->status ); + } + + /** + * Gets the intent from order data. + * + * @param \stdClass $order_data The order data. + * @param bool $is_3ds_response Whether this is a 3DS response. + * + * @return string + * @throws RuntimeException When intent is required but missing. + */ + private function get_intent( \stdClass $order_data, bool $is_3ds_response ): string { + if ( $is_3ds_response ) { + return $order_data->intent ?? 'CAPTURE'; + } + if ( ! isset( $order_data->intent ) ) { throw new RuntimeException( __( 'Order does not contain intent.', 'woocommerce-paypal-payments' ) ); } - $purchase_units = array_map( - function ( \stdClass $data ): PurchaseUnit { - return $this->purchase_unit_factory->from_paypal_response( $data ); - }, - $order_data->purchase_units - ); - - $create_time = ( isset( $order_data->create_time ) ) ? - \DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time ) - : null; - $update_time = ( isset( $order_data->update_time ) ) ? - \DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->update_time ) - : null; - $payer = ( isset( $order_data->payer ) ) ? - $this->payer_factory->from_paypal_response( $order_data->payer ) - : null; - - $payment_source = null; - if ( isset( $order_data->payment_source ) ) { - $json_encoded_payment_source = wp_json_encode( $order_data->payment_source ); - if ( $json_encoded_payment_source ) { - $payment_source_as_array = json_decode( $json_encoded_payment_source, true ); - if ( $payment_source_as_array ) { - $name = array_key_first( $payment_source_as_array ); - if ( $name ) { - $payment_source = new PaymentSource( - $name, - $order_data->payment_source->$name - ); - } - } - } - } - - return new Order( - $order_data->id, - $purchase_units, - new OrderStatus( $order_data->status ), - $payment_source, - $payer, - $order_data->intent, - $create_time, - $update_time - ); + return $order_data->intent; } /** - * Returns an Order object based off a PayPal 3DS Response. - * Specifically tailored for PAYER_ACTION_REQUIRED responses. + * Creates timestamps from order data. * - * @param \stdClass $order_data The JSON object. + * @param \stdClass $order_data The order data. + * @param bool $is_3ds_response Whether this is a 3DS response. * - * @return Order - * @throws RuntimeException When JSON object is malformed. + * @return array Array with 'create_time' and 'update_time' keys. */ - /** - * Returns an Order object based off a PayPal 3DS Response. - * Specifically tailored for PAYER_ACTION_REQUIRED responses. - * - * @param \stdClass $order_data The JSON object. - * - * @return Order - * @throws RuntimeException When JSON object is malformed. - */ - public function from_paypal_response_with_3ds( \stdClass $order_data ): Order { - // Only ID is strictly required - if ( ! isset( $order_data->id ) ) { - throw new RuntimeException( - __( 'Order does not contain an id.', 'woocommerce-paypal-payments' ) + private function create_timestamps( \stdClass $order_data, bool $is_3ds_response ): array { + if ( $is_3ds_response ) { + return array( + 'create_time' => null, + 'update_time' => null, ); } - // Don't try to create purchase units for 3DS responses - // Use an empty array instead - $purchase_units = array(); + $create_time = isset( $order_data->create_time ) ? + \DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time ) : + null; - // Status - should be PAYER_ACTION_REQUIRED - $status = new OrderStatus( $order_data->status ?? 'PAYER_ACTION_REQUIRED' ); + $update_time = isset( $order_data->update_time ) ? + \DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->update_time ) : + null; - // Use CAPTURE as default intent for 3DS responses - $intent = $order_data->intent ?? 'CAPTURE'; + return array( + 'create_time' => $create_time, + 'update_time' => $update_time, + ); + } - // All timestamps are null for 3DS responses - $create_time = null; - $update_time = null; - - // Payer is typically null in 3DS responses - $payer = null; - - // Application context is null in 3DS responses - $application_context = null; - - // Handle payment source (card) if present - $payment_source = null; - if ( isset( $order_data->payment_source ) ) { - try { - // Extract the payment source name (usually "card") - $source_props = get_object_vars( $order_data->payment_source ); - if ( ! empty( $source_props ) ) { - $source_name = array_key_first( $source_props ); - $payment_source = new PaymentSource( - $source_name, - $order_data->payment_source->$source_name - ); - } - } catch ( \Exception $e ) { - $this->logger->warning( 'Could not create payment source: ' . $e->getMessage() ); - // Continue without payment source - } + /** + * Creates payer from order data. + * + * @param \stdClass $order_data The order data. + * @param bool $is_3ds_response Whether this is a 3DS response. + * + * @return mixed Payer object or null. + */ + private function create_payer( \stdClass $order_data, bool $is_3ds_response ) { + if ( $is_3ds_response ) { + return null; } - // Create the Order without including links parameter - return new Order( - $order_data->id, - $purchase_units, - $status, - $payment_source, - $payer, - $intent, - $create_time, - $update_time, - $order_data->links + return isset( $order_data->payer ) ? + $this->payer_factory->from_paypal_response( $order_data->payer ) : + null; + } + + /** + * Creates payment source from order data. + * + * @param \stdClass $order_data The order data. + * + * @return PaymentSource|null + */ + private function create_payment_source( \stdClass $order_data ): ?PaymentSource { + if ( ! isset( $order_data->payment_source ) ) { + return null; + } + + $json_encoded_payment_source = wp_json_encode( $order_data->payment_source ); + if ( ! $json_encoded_payment_source ) { + return null; + } + + $payment_source_as_array = json_decode( $json_encoded_payment_source, true ); + if ( ! $payment_source_as_array ) { + return null; + } + + $source_name = array_key_first( $payment_source_as_array ); + if ( ! $source_name ) { + return null; + } + + return new PaymentSource( + $source_name, + $order_data->payment_source->$source_name ); } } diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index eec1d1b64..7f81f2143 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -232,24 +232,24 @@ class CreateOrderEndpoint implements EndpointInterface { LoggerInterface $logger ) { - $this->request_data = $request_data; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->contact_preference_factory = $contact_preference_factory; - $this->experience_context_builder = $experience_context_builder; - $this->api_endpoint = $order_endpoint; - $this->payer_factory = $payer_factory; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->early_order_handler = $early_order_handler; - $this->registration_needed = $registration_needed; - $this->card_billing_data_mode = $card_billing_data_mode; - $this->early_validation_enabled = $early_validation_enabled; - $this->pay_now_contexts = $pay_now_contexts; - $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; + $this->request_data = $request_data; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->contact_preference_factory = $contact_preference_factory; + $this->experience_context_builder = $experience_context_builder; + $this->api_endpoint = $order_endpoint; + $this->payer_factory = $payer_factory; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->early_order_handler = $early_order_handler; + $this->registration_needed = $registration_needed; + $this->card_billing_data_mode = $card_billing_data_mode; + $this->early_validation_enabled = $early_validation_enabled; + $this->pay_now_contexts = $pay_now_contexts; + $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; $this->server_side_shipping_callback_enabled = $server_side_shipping_callback_enabled; - $this->funding_sources_without_redirect = $funding_sources_without_redirect; - $this->logger = $logger; + $this->funding_sources_without_redirect = $funding_sources_without_redirect; + $this->logger = $logger; } /** From afe1cd99ed2f3c81201c1531abecb7ccdeeeb239 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 12:31:05 +0200 Subject: [PATCH 29/65] =?UTF-8?q?=F0=9F=8E=A8=20Add=20PHPCS=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Factory/OrderFactory.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index a9d39a636..89b6559ed 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -111,12 +111,12 @@ class OrderFactory { $this->validate_order_id( $order_data ); $purchase_units = $this->create_purchase_units( $order_data, $is_3ds_response ); - $status = $this->create_order_status( $order_data, $is_3ds_response ); - $intent = $this->get_intent( $order_data, $is_3ds_response ); - $timestamps = $this->create_timestamps( $order_data, $is_3ds_response ); - $payer = $this->create_payer( $order_data, $is_3ds_response ); + $status = $this->create_order_status( $order_data, $is_3ds_response ); + $intent = $this->get_intent( $order_data, $is_3ds_response ); + $timestamps = $this->create_timestamps( $order_data, $is_3ds_response ); + $payer = $this->create_payer( $order_data, $is_3ds_response ); $payment_source = $this->create_payment_source( $order_data ); - $links = $is_3ds_response ? ( $order_data->links ?? null ) : null; + $links = $is_3ds_response ? ( $order_data->links ?? null ) : null; return new Order( $order_data->id, From 2b025aa6909b8c49eff02fe68a30fbd9f0df8e38 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 13:03:15 +0200 Subject: [PATCH 30/65] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20the=20unnecessary?= =?UTF-8?q?=20payer-link=20meta=20saving?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/OrderEndpoint.php | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index e9b19344b..b4b391455 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -181,7 +181,7 @@ class OrderEndpoint { string $payment_method = '', array $request_data = array(), PaymentSource $payment_source = null, - $wc_order = null + ?\WC_Order $wc_order = null ): Order { $bearer = $this->bearer->bearer(); $data = array( @@ -267,42 +267,9 @@ class OrderEndpoint { throw $error; } - // Check if this is a PAYER_ACTION_REQUIRED response (3DS) + // Check if this is a PAYER_ACTION_REQUIRED response (3DS). $is_3ds_response = isset( $json->status ) && $json->status === 'PAYER_ACTION_REQUIRED'; - // Extract payer-action URL if present - $payer_action_url = ''; - if ( isset( $json->links ) && is_array( $json->links ) ) { - foreach ( $json->links as $link ) { - if ( isset( $link->rel ) && $link->rel === 'payer-action' && isset( $link->href ) ) { - $payer_action_url = $link->href; - break; - } - } - } - - // Store payer-action URL in WC order meta if available - if ( $wc_order && $payer_action_url ) { - $wc_order->add_meta_data( 'ppcp_axo_payer_action', $payer_action_url ); - $wc_order->save_meta_data(); - - $this->logger->info( - sprintf( - 'Saved payer-action URL to order meta: %s', - $payer_action_url - ) - ); - } - - // Log the entire $json response for debugging - $this->logger->info( - sprintf( - 'Order created successfully. PayPal API response: %s', - print_r( $json, true ) - ) - ); - - // Use appropriate factory method based on response type $order = $is_3ds_response ? $this->order_factory->from_paypal_response_with_3ds( $json ) : $this->order_factory->from_paypal_response( $json ); From ba7b01dfd6f422c0a576fe765a6a6c10181cef70 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 13:21:49 +0200 Subject: [PATCH 31/65] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20Psalm=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .psalm/stubs.php | 23 +++++++++++++++++++ modules/ppcp-api-client/src/Entity/Order.php | 5 ++++ .../src/Endpoint/ReturnUrlEndpoint.php | 5 ++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 27484ec28..0ac41fcd3 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -153,3 +153,26 @@ class WP_HTML_Tag_Processor { return ''; } } + +/** + * WooCommerce Session stubs for Psalm + */ +class WC_Session { + /** + * Get session cookie. + * + * @return string|false + */ + public function get_session_cookie() { + return ''; + } + + /** + * Set customer session cookie. + * + * @param bool $set Whether to set the cookie. + * @return void + */ + public function set_customer_session_cookie( $set ) { + } +} diff --git a/modules/ppcp-api-client/src/Entity/Order.php b/modules/ppcp-api-client/src/Entity/Order.php index 7a8a596e0..636323fab 100644 --- a/modules/ppcp-api-client/src/Entity/Order.php +++ b/modules/ppcp-api-client/src/Entity/Order.php @@ -185,6 +185,11 @@ class Order { return $this->payment_source; } + /** + * Returns the links. + * + * @return mixed|null + */ public function links() { return $this->links; } diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 91900daf9..96d389613 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -83,7 +83,8 @@ class ReturnUrlEndpoint { WC()->initialize_session(); } - if ( ! WC()->session->get_session_cookie() ) { + // Set customer session cookie if not already set. + if ( WC()->session && ! WC()->session->get_session_cookie() ) { WC()->session->set_customer_session_cookie( true ); } @@ -98,7 +99,7 @@ class ReturnUrlEndpoint { $wc_order_id = (int) $order->purchase_units()[0]->custom_id(); $wc_order = wc_get_order( $wc_order_id ); - if ( ! $wc_order ) { + if ( ! $wc_order || ! is_a( $wc_order, \WC_Order::class ) ) { wc_add_notice( __( 'Order information is missing. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); exit(); From 27b3a315b9115a40be0c078aeeeacb9523ef0a7e Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 26 Jun 2025 18:04:06 +0400 Subject: [PATCH 32/65] Fix the local apms migration. If the "Create gateway for alternative payment methods" checkbox is checked, then we don't need any migration as the enabled/disabled will be automatically recognized --- .../src/Service/Migration/PaymentSettingsMigration.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php index f0284cf8f..d6ec29706 100644 --- a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php @@ -44,15 +44,19 @@ class PaymentSettingsMigration implements SettingsMigrationInterface { } public function migrate(): void { + $allow_local_apm_gateways = $this->settings->has( 'allow_local_apm_gateways' ) && $this->settings->get( 'allow_local_apm_gateways' ); + if ( $this->settings->has( 'disable_funding' ) ) { $disable_funding = $this->settings->get( 'disable_funding' ); if ( ! in_array( 'venmo', $disable_funding, true ) ) { $this->payment_settings->toggle_method_state( 'venmo', true ); } - foreach ( $this->local_apms as $apm ) { - if ( ! in_array( $apm['id'], $disable_funding, true ) ) { - $this->payment_settings->toggle_method_state( $apm['id'], true ); + if ( ! $allow_local_apm_gateways ) { + foreach ( $this->local_apms as $apm ) { + if ( ! in_array( $apm['id'], $disable_funding, true ) ) { + $this->payment_settings->toggle_method_state( $apm['id'], true ); + } } } } From 99cd8e4aeb74ab7465945b0a05e8b2d46988d0a4 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 16:14:22 +0200 Subject: [PATCH 33/65] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20reliance=20on=20t?= =?UTF-8?q?he=20session=20in=20favor=20of=20passing=20the=20token=20direct?= =?UTF-8?q?ly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .psalm/stubs.php | 23 --- modules/ppcp-axo/src/Gateway/AxoGateway.php | 195 +++++++----------- .../src/Endpoint/ReturnUrlEndpoint.php | 160 ++++++-------- 3 files changed, 140 insertions(+), 238 deletions(-) diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 0ac41fcd3..27484ec28 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -153,26 +153,3 @@ class WP_HTML_Tag_Processor { return ''; } } - -/** - * WooCommerce Session stubs for Psalm - */ -class WC_Session { - /** - * Get session cookie. - * - * @return string|false - */ - public function get_session_cookie() { - return ''; - } - - /** - * Set customer session cookie. - * - * @param bool $set Whether to set the cookie. - * @return void - */ - public function set_customer_session_cookie( $set ) { - } -} diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index b3b40d8e0..a522028e8 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -245,59 +245,16 @@ class AxoGateway extends WC_Payment_Gateway { ); } - // Check for stored 3DS errors. - $stored_error = $this->get_and_clear_stored_error(); - if ( $stored_error ) { - return array( - 'result' => 'failure', - 'message' => $stored_error['message'], - ); + // Check for tokens to determine if this is a 3DS return or initial payment. + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $axo_nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) ); + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $token_param = wc_clean( wp_unslash( $_GET['token'] ?? '' ) ); + + if ( empty( $axo_nonce ) && ! empty( $token_param ) ) { + return $this->process_3ds_return( $wc_order, $token_param ); } - $existing_order = $this->session_handler->order(); - - if ( $existing_order ) { - // Check if this session order belongs to current WC order and we're in 3DS context. - $session_order_belongs_to_current_wc_order = $this->session_order_matches_wc_order( $existing_order, $wc_order ); - $is_3ds_context = $this->is_3ds_context(); - - if ( $session_order_belongs_to_current_wc_order && $is_3ds_context ) { - // This is a legitimate 3DS return for the current order. - try { - /** - * This filter controls if the method 'process()' from OrderProcessor will be called. - */ - $process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order ); - if ( $process ) { - $this->order_processor->process_captured_and_authorized( $wc_order, $existing_order ); - } - - // Clear session after successful processing. - $this->session_handler->destroy_session_data(); - WC()->cart->empty_cart(); - - return array( - 'result' => 'success', - 'redirect' => $this->get_return_url( $wc_order ), - ); - - } catch ( Exception $exception ) { - // Handle 3DS processing failures with universal error approach. - // Clear session data since payment failed. - $this->session_handler->destroy_session_data(); - - return array( - 'result' => 'failure', - 'message' => $this->get_user_friendly_error_message( $exception ), - ); - } - } else { - // Session order doesn't belong to current WC order OR not 3DS context. - $this->session_handler->destroy_session_data(); - } - } - - // No existing order or cleared session - this is an initial payment. try { // phpcs:ignore WordPress.Security.NonceVerification.Missing $fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) ); @@ -308,31 +265,25 @@ class AxoGateway extends WC_Payment_Gateway { } // The `axo_nonce` is not a WP nonce, but a card-token generated by the JS SDK. - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) ); - - // Enhanced token validation with universal error handling. - if ( empty( $token ) ) { - // Universal error return. + if ( empty( $axo_nonce ) ) { return array( 'result' => 'failure', - 'message' => $this->is_3ds_context() - ? __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' ) - : __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ), + 'message' => __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ), ); } - $order = $this->create_paypal_order( $wc_order, $token ); + $order = $this->create_paypal_order( $wc_order, $axo_nonce ); // Check if 3DS verification is required. $payer_action = $this->get_payer_action_url( $order ); - // If 3DS verification is required, store order and redirect. + // If 3DS verification is required, redirect with token in return URL. if ( $payer_action ) { - // Store the order in session before 3DS redirect. - $this->session_handler->replace_order( $order ); - - $return_url = home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ); + $return_url = add_query_arg( + 'token', + $order->id(), + home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ) + ); $redirect_url = add_query_arg( 'redirect_uri', @@ -359,7 +310,8 @@ class AxoGateway extends WC_Payment_Gateway { $this->order_processor->process_captured_and_authorized( $wc_order, $order ); } } catch ( Exception $exception ) { - // Error handling for initial payment failures. + // Error handling for payment failures. + $this->logger->error( '[AXO] Payment processing failed: ' . $exception->getMessage() ); return array( 'result' => 'failure', 'message' => $this->get_user_friendly_error_message( $exception ), @@ -375,20 +327,51 @@ class AxoGateway extends WC_Payment_Gateway { } /** - * Get and clear stored payment errors. + * Process 3DS return scenario. + * + * @param WC_Order $wc_order The WooCommerce order. + * @param string $token The PayPal order token. + * + * @return array */ - private function get_and_clear_stored_error() { - if ( ! WC()->session ) { - return null; + protected function process_3ds_return( WC_Order $wc_order, string $token ) : array { + try { + $paypal_order = $this->order_endpoint->order( $token ); + + if ( ! $paypal_order->status()->is( OrderStatus::COMPLETED ) ) { + return array( + 'result' => 'failure', + 'message' => __( '3D Secure authentication was not completed successfully. Please try again.', 'woocommerce-paypal-payments' ), + ); + } + + /** + * This filter controls if the method 'process()' from OrderProcessor will be called. + * So you can implement your own for example on subscriptions + * + * - true bool controls execution of 'OrderProcessor::process()' + * - $this \WC_Payment_Gateway + * - $wc_order \WC_Order + */ + $process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order ); + if ( $process ) { + $this->order_processor->process_captured_and_authorized( $wc_order, $paypal_order ); + } + + } catch ( Exception $exception ) { + $this->logger->error( '[AXO] 3DS return processing failed: ' . $exception->getMessage() ); + return array( + 'result' => 'failure', + 'message' => $this->get_user_friendly_error_message( $exception ), + ); } - $stored_error = WC()->session->get( 'ppcp_payment_error' ); - if ( $stored_error ) { - WC()->session->__unset( 'ppcp_payment_error' ); - return $stored_error; - } + WC()->cart->empty_cart(); - return null; + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); } /** @@ -423,56 +406,21 @@ class AxoGateway extends WC_Payment_Gateway { * Extract payer action URL from PayPal order. */ private function get_payer_action_url( Order $order ) { - foreach ( $order->links() as $link ) { - if ( $link->rel === 'payer-action' ) { - return $link->href; + $links = $order->links(); + + if ( ! $links ) { + return ''; + } + + foreach ( $links as $link ) { + if ( isset( $link->rel ) && $link->rel === 'payer-action' ) { + return $link->href ?? ''; } } + return ''; } - /** - * Check if session order belongs to current WC order. - * - * @param Order $paypal_order The PayPal order from session. - * @param WC_Order $wc_order The current WooCommerce order. - * @return bool - */ - private function session_order_matches_wc_order( Order $paypal_order, WC_Order $wc_order ): bool { - $paypal_custom_id = $paypal_order->purchase_units()[0]->custom_id() ?? ''; - $wc_order_id = (string) $wc_order->get_id(); - - return $paypal_custom_id === $wc_order_id; - } - - /** - * Check if we're in a 3DS return context. - * - * @return bool - */ - private function is_3ds_context(): bool { - // Check referer for PayPal. - $referer = $_SERVER['HTTP_REFERER'] ?? ''; - if ( strpos( $referer, 'paypal.com' ) !== false ) { - return true; - } - - // Check for 3DS-specific parameters. - $three_ds_params = array( 'liability_shift', 'state', 'code', 'authentication_state' ); - foreach ( $three_ds_params as $param ) { - if ( isset( $_GET[ $param ] ) ) { - return true; - } - } - - // Check if we're being called by ReturnUrlEndpoint. - if ( isset( $_GET['wc-ajax'] ) && $_GET['wc-ajax'] === 'ppc-return-url' ) { - return true; - } - - return false; - } - /** * Create a new PayPal order from the existing WC_Order instance. * @@ -505,7 +453,8 @@ class AxoGateway extends WC_Payment_Gateway { null, self::ID, array(), - $payment_source + $payment_source, + $wc_order ); } diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 96d389613..1275622ab 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -88,71 +88,37 @@ class ReturnUrlEndpoint { WC()->session->set_customer_session_cookie( true ); } - // Check if we have a PayPal order in session (3DS return). - $order = $this->session_handler->order(); - - if ( $order ) { - try { - // Handle 3DS capture before processing. - $order = $this->handle_3ds_return( $order ); - - $wc_order_id = (int) $order->purchase_units()[0]->custom_id(); - $wc_order = wc_get_order( $wc_order_id ); - - if ( ! $wc_order || ! is_a( $wc_order, \WC_Order::class ) ) { - wc_add_notice( __( 'Order information is missing. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); - } - - $payment_gateway = $this->get_payment_gateway( $wc_order->get_payment_method() ); - if ( ! $payment_gateway ) { - wc_add_notice( __( 'Payment gateway is unavailable. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); - } - - $result = $payment_gateway->process_payment( $wc_order_id ); - - if ( isset( $result['result'] ) && $result['result'] === 'success' ) { - wp_safe_redirect( $result['redirect'] ); - exit(); - } - - wc_add_notice( __( 'Payment processing failed. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); - - } catch ( Exception $e ) { - wc_add_notice( __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); - } - } - - // No order in session - handle regular PayPal returns. + // Check for token parameter - required for all returns. if ( ! isset( $_GET['token'] ) ) { wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); exit(); } - // Handle regular PayPal returns (non-3DS). - // phpcs:enable WordPress.Security.NonceVerification.Recommended $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); try { $order = $this->order_endpoint->order( $token ); } catch ( Exception $exception ) { + $this->logger->warning( "Return URL endpoint failed to fetch order $token: " . $exception->getMessage() ); wc_add_notice( __( 'Could not retrieve payment information. Please try again.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); exit(); } - if ( $order->status()->is( OrderStatus::APPROVED ) || $order->status()->is( OrderStatus::COMPLETED ) ) { - $this->session_handler->replace_order( $order ); + // Handle 3DS completion if needed. + if ( $this->needs_3ds_completion( $order ) ) { + try { + $order = $this->complete_3ds_verification( $order ); + } catch ( Exception $e ) { + $this->logger->warning( "3DS completion failed for order $token: " . $e->getMessage() ); + wc_add_notice( $this->get_3ds_error_message( $e ), 'error' ); + wp_safe_redirect( wc_get_checkout_url() ); + exit(); + } } + // Get WooCommerce order ID. $wc_order_id = (int) $order->purchase_units()[0]->custom_id(); if ( ! $wc_order_id ) { // We cannot finish processing here without WC order, but at least go into the continuation mode. @@ -192,7 +158,9 @@ class ReturnUrlEndpoint { exit(); } + $this->logger->info( 'ReturnUrlEndpoint calling process_payment for gateway: ' . get_class( $payment_gateway ) ); $success = $payment_gateway->process_payment( $wc_order_id ); + $this->logger->info( 'ReturnUrlEndpoint process_payment result: ' . wp_json_encode( $success ) ); if ( isset( $success['result'] ) && 'success' === $success['result'] ) { add_filter( @@ -212,62 +180,70 @@ class ReturnUrlEndpoint { exit(); } + /** + * Check if order needs 3DS completion. + * + * @param mixed $order The PayPal order. + * @return bool + */ + private function needs_3ds_completion( $order ): bool { + // If order is still CREATED after 3DS redirect, it needs to be captured. + return $order->status()->is( OrderStatus::CREATED ); + } /** - * Handle 3DS return and capture order if needed. + * Complete 3DS verification by capturing the order. * * @param mixed $order The PayPal order. * @return mixed The processed order. + * @throws Exception When 3DS completion fails. */ - private function handle_3ds_return( $order ) { - // If order is still CREATED after 3DS, it needs to be captured. - if ( $order->status()->is( OrderStatus::CREATED ) ) { - try { - // Capture the order. - $captured_order = $this->order_endpoint->capture( $order ); + private function complete_3ds_verification( $order ) { + try { + // Capture the order. + $captured_order = $this->order_endpoint->capture( $order ); - // Check if capture actually succeeded vs. payment declined. - if ( $captured_order->status()->is( OrderStatus::COMPLETED ) ) { - // Update session with captured order. - $this->session_handler->replace_order( $captured_order ); - return $captured_order; - } else { - // Capture API succeeded but payment was declined. - throw new Exception( __( 'Payment was declined by the payment provider. Please try a different payment method.', 'woocommerce-paypal-payments' ) ); - } - } catch ( DomainException $e ) { - // Handle 3DS authentication failures (Test Case 4: Unavailable). - // Clear session data since authentication failed. - $this->session_handler->destroy_session_data(); - - // Use native WooCommerce error handling. - wc_add_notice( __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); - - } catch ( RuntimeException $e ) { - if ( strpos( $e->getMessage(), 'declined' ) !== false || - strpos( $e->getMessage(), 'PAYMENT_DENIED' ) !== false || - strpos( $e->getMessage(), 'INSTRUMENT_DECLINED' ) !== false || - strpos( $e->getMessage(), 'Payment provider declined' ) !== false ) { - - $this->session_handler->destroy_session_data(); - - wc_add_notice( __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); - } - throw $e; - } catch ( Exception $e ) { - $this->session_handler->destroy_session_data(); - wc_add_notice( __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ), 'error' ); - wp_safe_redirect( wc_get_checkout_url() ); - exit(); + // Check if capture actually succeeded vs. payment declined. + if ( $captured_order->status()->is( OrderStatus::COMPLETED ) ) { + return $captured_order; + } else { + // Capture API succeeded but payment was declined. + throw new Exception( __( 'Payment was declined by the payment provider. Please try a different payment method.', 'woocommerce-paypal-payments' ) ); } + } catch ( DomainException $e ) { + // Handle 3DS authentication failures. + throw new Exception( __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ) ); + } catch ( RuntimeException $e ) { + if ( strpos( $e->getMessage(), 'declined' ) !== false || + strpos( $e->getMessage(), 'PAYMENT_DENIED' ) !== false || + strpos( $e->getMessage(), 'INSTRUMENT_DECLINED' ) !== false || + strpos( $e->getMessage(), 'Payment provider declined' ) !== false ) { + throw new Exception( __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ) ); + } + throw $e; + } + } + + /** + * Get user-friendly error message for 3DS failures. + * + * @param Exception $exception The exception. + * @return string + */ + private function get_3ds_error_message( Exception $exception ): string { + $error_message = $exception->getMessage(); + + if ( strpos( $error_message, '3D Secure' ) !== false ) { + return $error_message; } - return $order; + if ( strpos( $error_message, 'declined' ) !== false ) { + return __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ); + } + + return __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' ); } + /** * Gets the appropriate payment gateway for the given payment method. * From 99f34d6868232cfae58b53c49e941ed7dac62647 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 26 Jun 2025 18:17:51 +0400 Subject: [PATCH 34/65] Always enable new settings module for migration support Enable the new settings module unconditionally to support migration functionality. The new settings module must be enabled during migration because the migration process requires access to the new data modules to save settings properly. Implementation details: - The `run` method checks `should_use_the_old_ui` and conditionally adds actions - In `services.php`, we check `should_use_the_old_ui` to disable services when the old UI is active --- modules.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules.php b/modules.php index 3b4269491..75482e42f 100644 --- a/modules.php +++ b/modules.php @@ -10,7 +10,6 @@ namespace WooCommerce\PayPalCommerce; use WooCommerce\PayPalCommerce\PayLaterBlock\PayLaterBlockModule; use WooCommerce\PayPalCommerce\PayLaterWCBlocks\PayLaterWCBlocksModule; use WooCommerce\PayPalCommerce\PayLaterConfigurator\PayLaterConfiguratorModule; -use WooCommerce\PayPalCommerce\Settings\SettingsModule; return function ( string $root_dir ): iterable { $modules_dir = "$root_dir/modules"; @@ -92,7 +91,12 @@ return function ( string $root_dir ): iterable { $modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); } - $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); + if ( apply_filters( + 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', + true + ) ) { + $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); + } return $modules; }; From 7713956160ee48c6520c6b1da4d29ad4108ed75f Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 16:21:21 +0200 Subject: [PATCH 35/65] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20leftover=20sessio?= =?UTF-8?q?n=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/src/Gateway/AxoGateway.php | 1 - .../src/Endpoint/ReturnUrlEndpoint.php | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index a522028e8..1db5ab56e 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -357,7 +357,6 @@ class AxoGateway extends WC_Payment_Gateway { if ( $process ) { $this->order_processor->process_captured_and_authorized( $wc_order, $paypal_order ); } - } catch ( Exception $exception ) { $this->logger->error( '[AXO] 3DS return processing failed: ' . $exception->getMessage() ); return array( diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 1275622ab..2d0aaadc8 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -78,17 +78,6 @@ class ReturnUrlEndpoint { * Handles the incoming request. */ public function handle_request(): void { - // Initialize WC session for AJAX endpoints. - if ( ! WC()->session ) { - WC()->initialize_session(); - } - - // Set customer session cookie if not already set. - if ( WC()->session && ! WC()->session->get_session_cookie() ) { - WC()->session->set_customer_session_cookie( true ); - } - - // Check for token parameter - required for all returns. if ( ! isset( $_GET['token'] ) ) { wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); @@ -158,9 +147,7 @@ class ReturnUrlEndpoint { exit(); } - $this->logger->info( 'ReturnUrlEndpoint calling process_payment for gateway: ' . get_class( $payment_gateway ) ); $success = $payment_gateway->process_payment( $wc_order_id ); - $this->logger->info( 'ReturnUrlEndpoint process_payment result: ' . wp_json_encode( $success ) ); if ( isset( $success['result'] ) && 'success' === $success['result'] ) { add_filter( From 2dcf3279b8a28c124ac7d75cd3ce3fe2776aa7e1 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 26 Jun 2025 18:58:03 +0400 Subject: [PATCH 36/65] Make the button accessible. --- modules/ppcp-settings/src/SettingsModule.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 27c81bf95..4b1c1be94 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -97,8 +97,9 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_filter( 'woocommerce_paypal_payments_inside_settings_page_header', static fn() : string => sprintf( - '%s', - esc_html__( 'Switch to new settings UI', 'woocommerce-paypal-payments' ) + '%s', + esc_html__( 'Switch to new settings UI', 'woocommerce-paypal-payments' ), + esc_html__( 'This action will permanently switch to the new settings interface and cannot be undone', 'woocommerce-paypal-payments' ) ) ); @@ -114,7 +115,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { $message = sprintf( // translators: %1$s is the URL for the startup guide. __( - '🎉 Discover the new PayPal Payments settings! Enjoy a cleaner, faster interface. Check out the Startup Guide, then click Switch to New Settings to activate it.', + '🎉 Discover the new PayPal Payments settings! Enjoy a cleaner, faster interface. Check out the Startup Guide, then click Switch to New Settings to activate it.', 'woocommerce-paypal-payments' ), 'https://woocommerce.com/document/woocommerce-paypal-payments/paypal-payments-startup-guide/' From ae21627c11a09d8d558a79db2e163eaf539bde1b Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 26 Jun 2025 23:22:11 +0200 Subject: [PATCH 37/65] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20Psalm=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/src/Gateway/AxoGateway.php | 1 - modules/ppcp-settings/src/Data/SettingsModel.php | 4 ++-- modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index 1db5ab56e..f43040ceb 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -245,7 +245,6 @@ class AxoGateway extends WC_Payment_Gateway { ); } - // Check for tokens to determine if this is a 3DS return or initial payment. // phpcs:ignore WordPress.Security.NonceVerification.Missing $axo_nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing diff --git a/modules/ppcp-settings/src/Data/SettingsModel.php b/modules/ppcp-settings/src/Data/SettingsModel.php index e7db8469d..8537ff917 100644 --- a/modules/ppcp-settings/src/Data/SettingsModel.php +++ b/modules/ppcp-settings/src/Data/SettingsModel.php @@ -233,8 +233,8 @@ class SettingsModel extends AbstractDataModel { /** * Converts the 3D Secure setting value to the corresponding API enum string. * - * @param string|null $three_d_secure The 3D Secure setting ('no-3d-secure', 'only-required-3d-secure', 'always-3d-secure') - * @return string The corresponding API enum string ('NO_3D_SECURE', 'SCA_WHEN_REQUIRED', 'SCA_ALWAYS') + * @param string|null $three_d_secure The 3D Secure setting ('no-3d-secure', 'only-required-3d-secure', 'always-3d-secure'). + * @return string The corresponding API enum string ('NO_3D_SECURE', 'SCA_WHEN_REQUIRED', 'SCA_ALWAYS'). */ public function get_three_d_secure_enum( string $three_d_secure = null ): string { // If no value is provided, use the current setting. diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 2d0aaadc8..28fa6c341 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -78,12 +78,14 @@ class ReturnUrlEndpoint { * Handles the incoming request. */ public function handle_request(): void { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['token'] ) ) { wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); exit(); } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); try { @@ -184,6 +186,7 @@ class ReturnUrlEndpoint { * @param mixed $order The PayPal order. * @return mixed The processed order. * @throws Exception When 3DS completion fails. + * @throws RuntimeException When API errors occur that don't match decline patterns. */ private function complete_3ds_verification( $order ) { try { From 8c430035d26ac24b98a3bf04dfc932ca11342deb Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Fri, 27 Jun 2025 00:02:16 +0200 Subject: [PATCH 38/65] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20unused=20dependen?= =?UTF-8?q?cies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/src/AxoModule.php | 1 - modules/ppcp-axo/src/Gateway/AxoGateway.php | 4 ---- 2 files changed, 5 deletions(-) diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 84fbba52d..1ce57e48a 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Axo; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder; diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index f43040ceb..a95959381 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -17,12 +17,10 @@ use WC_Payment_Gateway; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; -use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait; @@ -31,10 +29,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait; -use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\Messages; use DomainException; /** From f398c471aee3368754ffa3cfb3f5dcedc7ed3ad5 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Fri, 27 Jun 2025 00:13:07 +0200 Subject: [PATCH 39/65] =?UTF-8?q?=F0=9F=A7=B9=20Remove=20empty=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Factory/OrderFactory.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 89b6559ed..300985804 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -34,8 +34,6 @@ class OrderFactory { */ private $payer_factory; - - /** * OrderFactory constructor. * From 8c800b11b521a732ffcd635c80deacc377f3b27c Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Fri, 27 Jun 2025 00:25:28 +0200 Subject: [PATCH 40/65] =?UTF-8?q?=F0=9F=A7=B9=20Minor=20clean=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/src/Gateway/AxoGateway.php | 8 ++++---- modules/ppcp-settings/src/Data/SettingsModel.php | 2 +- .../ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php | 8 ++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index a95959381..9f966d1dd 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -29,6 +29,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait; +use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; use DomainException; @@ -235,9 +236,9 @@ class AxoGateway extends WC_Payment_Gateway { $wc_order = wc_get_order( $order_id ); if ( ! is_a( $wc_order, WC_Order::class ) ) { - return array( - 'result' => 'failure', - 'message' => __( 'Order not found. Please try again.', 'woocommerce-paypal-payments' ), + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ), ); } @@ -424,7 +425,6 @@ class AxoGateway extends WC_Payment_Gateway { * @return Order The PayPal order. */ protected function create_paypal_order( WC_Order $wc_order, string $payment_token ) : Order { - $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); $shipping_preference = $this->shipping_preference_factory->from_state( diff --git a/modules/ppcp-settings/src/Data/SettingsModel.php b/modules/ppcp-settings/src/Data/SettingsModel.php index 8537ff917..40bcd8f8a 100644 --- a/modules/ppcp-settings/src/Data/SettingsModel.php +++ b/modules/ppcp-settings/src/Data/SettingsModel.php @@ -248,7 +248,7 @@ class SettingsModel extends AbstractDataModel { 'always-3d-secure' => 'SCA_ALWAYS', ); - return $map[ $three_d_secure ] ?? 'SCA_WHEN_REQUIRED'; // Default to SCA_WHEN_REQUIRED if mapping not found. + return $map[ $three_d_secure ] ?? 'SCA_WHEN_REQUIRED'; } /** diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index 28fa6c341..d848688e5 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -78,15 +78,14 @@ class ReturnUrlEndpoint { * Handles the incoming request. */ public function handle_request(): void { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended + // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['token'] ) ) { wc_add_notice( __( 'Payment session expired. Please try placing your order again.', 'woocommerce-paypal-payments' ), 'error' ); wp_safe_redirect( wc_get_checkout_url() ); exit(); } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); + // phpcs:enable WordPress.Security.NonceVerification.Recommended try { $order = $this->order_endpoint->order( $token ); @@ -135,7 +134,6 @@ class ReturnUrlEndpoint { exit(); } - // Handle different gateway types. if ( $wc_order->get_payment_method() === OXXOGateway::ID ) { $this->session_handler->destroy_session_data(); wp_safe_redirect( wc_get_checkout_url() ); @@ -190,7 +188,6 @@ class ReturnUrlEndpoint { */ private function complete_3ds_verification( $order ) { try { - // Capture the order. $captured_order = $this->order_endpoint->capture( $order ); // Check if capture actually succeeded vs. payment declined. @@ -201,7 +198,6 @@ class ReturnUrlEndpoint { throw new Exception( __( 'Payment was declined by the payment provider. Please try a different payment method.', 'woocommerce-paypal-payments' ) ); } } catch ( DomainException $e ) { - // Handle 3DS authentication failures. throw new Exception( __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' ) ); } catch ( RuntimeException $e ) { if ( strpos( $e->getMessage(), 'declined' ) !== false || From 51600d0ccf73f282c1c0a854a350ed9fe0eea20e Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Fri, 27 Jun 2025 01:28:45 +0200 Subject: [PATCH 41/65] =?UTF-8?q?=F0=9F=A7=B9=20More=20clean=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php | 4 +--- modules/ppcp-api-client/src/Entity/Order.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index b4b391455..5278ce4b0 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -169,7 +169,6 @@ class OrderEndpoint { * @param string $payment_method WC payment method. * @param array $request_data Request data. * @param PaymentSource|null $payment_source The payment source. - * @param \WC_Order|null $wc_order The WooCommerce order, if available. * * @return Order * @throws RuntimeException If the request fails. @@ -180,8 +179,7 @@ class OrderEndpoint { Payer $payer = null, string $payment_method = '', array $request_data = array(), - PaymentSource $payment_source = null, - ?\WC_Order $wc_order = null + PaymentSource $payment_source = null ): Order { $bearer = $this->bearer->bearer(); $data = array( diff --git a/modules/ppcp-api-client/src/Entity/Order.php b/modules/ppcp-api-client/src/Entity/Order.php index 636323fab..279a58e9f 100644 --- a/modules/ppcp-api-client/src/Entity/Order.php +++ b/modules/ppcp-api-client/src/Entity/Order.php @@ -222,7 +222,7 @@ class Order { } if ( $this->links ) { - $this->links()->to_array(); + $order['links'] = $this->links(); } return $order; From 3ab7fe0af3f90aba0be7c747c949cbcc063d7811 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 27 Jun 2025 12:56:12 +0400 Subject: [PATCH 42/65] Fix the tests --- composer.lock | 510 +++++++++++++------------ tests/PHPUnit/ModularTestCase.php | 2 + tests/PHPUnit/bootstrap.php | 1 + tests/stubs/DefaultPaymentGateways.php | 17 + 4 files changed, 286 insertions(+), 244 deletions(-) create mode 100644 tests/stubs/DefaultPaymentGateways.php diff --git a/composer.lock b/composer.lock index 180c71364..56340e11f 100644 --- a/composer.lock +++ b/composer.lock @@ -438,26 +438,26 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -498,7 +498,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -514,7 +514,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "wikimedia/composer-merge-plugin", @@ -545,10 +545,10 @@ }, "type": "composer-plugin", "extra": { + "class": "Wikimedia\\Composer\\Merge\\V2\\MergePlugin", "branch-alias": { "dev-master": "2.x-dev" - }, - "class": "Wikimedia\\Composer\\Merge\\V2\\MergePlugin" + } }, "autoload": { "psr-4": { @@ -788,20 +788,20 @@ }, { "name": "antecedent/patchwork", - "version": "2.1.28", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d" + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/6b30aff81ebadf0f2feb9268d3e08385cebcc08d", - "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/1bf183a3e1bd094f231a2128b9ecc5363c269245", + "reference": "1bf183a3e1bd094f231a2128b9ecc5363c269245", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { "phpunit/phpunit": ">=4" @@ -830,22 +830,22 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.28" + "source": "https://github.com/antecedent/patchwork/tree/2.2.1" }, - "time": "2024-02-06T09:26:11+00:00" + "time": "2024-12-11T10:19:54+00:00" }, { "name": "brain/monkey", - "version": "2.6.1", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/Brain-WP/BrainMonkey.git", - "reference": "a31c84515bb0d49be9310f52ef1733980ea8ffbb" + "reference": "d95a9d895352c30f47604ad1b825ab8fa9d1a373" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/a31c84515bb0d49be9310f52ef1733980ea8ffbb", - "reference": "a31c84515bb0d49be9310f52ef1733980ea8ffbb", + "url": "https://api.github.com/repos/Brain-WP/BrainMonkey/zipball/d95a9d895352c30f47604ad1b825ab8fa9d1a373", + "reference": "d95a9d895352c30f47604ad1b825ab8fa9d1a373", "shasum": "" }, "require": { @@ -861,8 +861,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-version/1": "1.x-dev", - "dev-master": "2.0.x-dev" + "dev-master": "2.x-dev", + "dev-version/1": "1.x-dev" } }, "autoload": { @@ -902,7 +902,7 @@ "issues": "https://github.com/Brain-WP/BrainMonkey/issues", "source": "https://github.com/Brain-WP/BrainMonkey" }, - "time": "2021-11-11T15:53:55+00:00" + "time": "2024-08-29T20:15:04+00:00" }, { "name": "composer/package-versions-deprecated", @@ -979,16 +979,16 @@ }, { "name": "composer/pcre", - "version": "3.3.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", - "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { @@ -998,19 +998,19 @@ "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.10", - "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { @@ -1038,7 +1038,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.1" + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { @@ -1054,28 +1054,28 @@ "type": "tidelift" } ], - "time": "2024-08-27T18:44:43+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { "name": "composer/semver", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -1119,7 +1119,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.2" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -1135,7 +1135,7 @@ "type": "tidelift" } ], - "time": "2024-07-12T11:35:52+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -1432,29 +1432,30 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.3", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -1462,7 +1463,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1473,9 +1474,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "time": "2024-01-30T19:34:25+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { "name": "doctrine/instantiator", @@ -1594,16 +1595,16 @@ }, { "name": "felixfbecker/language-server-protocol", - "version": "v1.5.2", + "version": "v1.5.3", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", "shasum": "" }, "require": { @@ -1644,9 +1645,9 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" }, - "time": "2022-03-02T22:36:06+00:00" + "time": "2024-04-30T00:40:11+00:00" }, { "name": "graham-campbell/result-type", @@ -1712,20 +1713,20 @@ }, { "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" + "php": "^7.4|^8.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -1733,8 +1734,8 @@ "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -1757,9 +1758,9 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { "name": "inpsyde/composer-assets-compiler", @@ -1792,10 +1793,10 @@ "extra": { "class": "Inpsyde\\AssetsCompiler\\Composer\\Plugin", "branch-alias": { - "dev-master": "2.x-dev", "dev-v1.x": "1.x-dev", "dev-v2.x": "2.x-dev", - "dev-v3.x": "3.x-dev" + "dev-v3.x": "3.x-dev", + "dev-master": "2.x-dev" } }, "autoload": { @@ -1828,31 +1829,34 @@ }, { "name": "inpsyde/modularity", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/inpsyde/modularity.git", - "reference": "2119d0e32706741a3c6dc0a85d908ec19ebf142e" + "reference": "e1ca1c81b7b663355906b586525d21ac5d46bc65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/inpsyde/modularity/zipball/2119d0e32706741a3c6dc0a85d908ec19ebf142e", - "reference": "2119d0e32706741a3c6dc0a85d908ec19ebf142e", + "url": "https://api.github.com/repos/inpsyde/modularity/zipball/e1ca1c81b7b663355906b586525d21ac5d46bc65", + "reference": "e1ca1c81b7b663355906b586525d21ac5d46bc65", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.4 <8.4", + "php": ">=7.4", "psr/container": "^1.1.0 || ^2" }, "require-dev": { "brain/monkey": "^2.6.1", - "inpsyde/php-coding-standards": "^2@dev", - "inpsyde/wp-stubs-versions": "dev-latest", + "inpsyde/wp-stubs-versions": "6.7", "mikey179/vfsstream": "^v1.6.11", + "phpstan/phpstan": "^2.1.1", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-mockery": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0.4", "phpunit/phpunit": "^9.6.19", - "roots/wordpress-no-content": "@dev", - "vimeo/psalm": "^5.24.0" + "swissspidy/phpstan-no-private": "^v1.0.0", + "syde/phpcs": "^1.0.0" }, "type": "library", "extra": { @@ -1871,18 +1875,18 @@ ], "authors": [ { - "name": "Inpsyde GmbH", - "email": "hello@inpsyde.com", - "homepage": "https://inpsyde.com/", + "name": "Syde GmbH", + "email": "hello@syde.com", + "homepage": "https://syde.com/", "role": "Company" } ], "description": "Modular PSR-11 implementation for WordPress plugins, themes or libraries.", "support": { "issues": "https://github.com/inpsyde/modularity/issues", - "source": "https://github.com/inpsyde/modularity/tree/1.10.0" + "source": "https://github.com/inpsyde/modularity/tree/1.12.0" }, - "time": "2024-09-03T10:42:50+00:00" + "time": "2025-05-09T12:13:17+00:00" }, { "name": "mockery/mockery", @@ -1969,16 +1973,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -2017,7 +2021,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -2025,20 +2029,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v4.4.1", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", "shasum": "" }, "require": { @@ -2074,22 +2078,22 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" }, - "time": "2024-01-31T06:18:54+00:00" + "time": "2024-09-08T10:13:13+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.1", + "version": "v4.19.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", "shasum": "" }, "require": { @@ -2098,7 +2102,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -2130,9 +2134,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" }, - "time": "2024-03-17T08:10:35+00:00" + "time": "2024-09-29T15:01:53+00:00" }, { "name": "openlss/lib-array2xml", @@ -2532,21 +2536,22 @@ }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.5", + "version": "2.1.7", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" + "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/5bfbbfbabb3df2b9a83e601de9153e4a7111962c", + "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0" @@ -2596,9 +2601,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:37:59+00:00" + "time": "2025-05-12T16:38:37+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2655,16 +2664,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.4.1", + "version": "5.6.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", "shasum": "" }, "require": { @@ -2673,17 +2682,17 @@ "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.5", + "mockery/mockery": "~1.3.5 || ~1.6.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.8", "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^5.13" + "psalm/phar": "^5.26" }, "type": "library", "extra": { @@ -2713,29 +2722,29 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" }, - "time": "2024-05-21T05:55:05+00:00" + "time": "2025-04-13T19:20:35+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.2", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "153ae662783729388a584b4361f2545e4d841e3c" + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", - "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { "ext-tokenizer": "*", @@ -2771,9 +2780,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2024-02-23T11:10:43+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { "name": "phpoption/phpoption", @@ -2852,30 +2861,30 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.30.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f", - "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -2893,9 +2902,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2024-08-29T09:54:52+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3218,16 +3227,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.20", + "version": "9.6.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "49d7820565836236411f5dc002d16dd689cde42f" + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", - "reference": "49d7820565836236411f5dc002d16dd689cde42f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", "shasum": "" }, "require": { @@ -3238,11 +3247,11 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-code-coverage": "^9.2.32", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.4", @@ -3301,7 +3310,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" }, "funding": [ { @@ -3312,12 +3321,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-07-10T11:45:39+00:00" + "time": "2025-05-02T06:40:34+00:00" }, { "name": "sebastian/cli-parser", @@ -4284,16 +4301,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.2", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", - "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -4358,22 +4375,26 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-07-21T23:26:44+00:00" + "time": "2025-06-17T22:17:01+00:00" }, { "name": "symfony/console", - "version": "v5.4.43", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e86f8554de667c16dde8aeb89a3990cfde924df9" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e86f8554de667c16dde8aeb89a3990cfde924df9", - "reference": "e86f8554de667c16dde8aeb89a3990cfde924df9", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { @@ -4443,7 +4464,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.43" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -4459,20 +4480,20 @@ "type": "tidelift" } ], - "time": "2024-08-13T16:31:56+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.3", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", "shasum": "" }, "require": { @@ -4480,12 +4501,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4510,7 +4531,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" }, "funding": [ { @@ -4526,24 +4547,24 @@ "type": "tidelift" } ], - "time": "2023-01-24T14:02:46+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -4554,8 +4575,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4589,7 +4610,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -4605,24 +4626,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -4630,8 +4651,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4667,7 +4688,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -4683,24 +4704,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -4708,8 +4729,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4748,7 +4769,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -4764,24 +4785,25 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-iconv": "*", + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -4792,8 +4814,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4828,7 +4850,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -4844,30 +4866,30 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.30.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4904,7 +4926,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" }, "funding": [ { @@ -4920,20 +4942,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.3", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { @@ -4949,12 +4971,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4987,7 +5009,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, "funding": [ { @@ -5003,20 +5025,20 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:04:16+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/string", - "version": "v5.4.43", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "8be1d484951ff5ca995eaf8edcbcb8b9a5888450" + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/8be1d484951ff5ca995eaf8edcbcb8b9a5888450", - "reference": "8be1d484951ff5ca995eaf8edcbcb8b9a5888450", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { @@ -5073,7 +5095,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.43" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, "funding": [ { @@ -5089,7 +5111,7 @@ "type": "tidelift" } ], - "time": "2024-08-01T10:24:28+00:00" + "time": "2024-11-10T20:33:58+00:00" }, { "name": "theseer/tokenizer", @@ -5213,10 +5235,10 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.x-dev", - "dev-3.x": "3.x-dev", + "dev-1.x": "1.x-dev", "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" + "dev-3.x": "3.x-dev", + "dev-master": "4.x-dev" } }, "autoload": { @@ -5251,16 +5273,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { @@ -5319,7 +5341,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -5331,7 +5353,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-04-30T23:37:27+00:00" }, { "name": "webmozart/assert", @@ -5541,8 +5563,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "php-stubs/woocommerce-stubs": 0, - "php-stubs/wordpress-stubs": 0 + "php-stubs/wordpress-stubs": 0, + "php-stubs/woocommerce-stubs": 0 }, "prefer-stable": true, "prefer-lowest": false, @@ -5550,7 +5572,7 @@ "php": "^7.4 | ^8.0", "ext-json": "*" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "7.4" }, diff --git a/tests/PHPUnit/ModularTestCase.php b/tests/PHPUnit/ModularTestCase.php index 7b77eee00..63b662dd6 100644 --- a/tests/PHPUnit/ModularTestCase.php +++ b/tests/PHPUnit/ModularTestCase.php @@ -32,6 +32,8 @@ class ModularTestCase extends TestCase when('WC')->justReturn((object) [ 'session' => null, ]); + when('is_admin')->justReturn(true); + when('sanitize_key')->returnArg(); global $wpdb; $wpdb = \Mockery::mock(\stdClass::class); diff --git a/tests/PHPUnit/bootstrap.php b/tests/PHPUnit/bootstrap.php index 647473e12..bd416aa6e 100644 --- a/tests/PHPUnit/bootstrap.php +++ b/tests/PHPUnit/bootstrap.php @@ -11,5 +11,6 @@ require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway_CC.php'; require_once TESTS_ROOT_DIR . '/stubs/WC_Ajax.php'; require_once TESTS_ROOT_DIR . '/stubs/WC_Checkout.php'; require_once TESTS_ROOT_DIR . '/stubs/Task.php'; +require_once TESTS_ROOT_DIR . '/stubs/DefaultPaymentGateways.php'; Hamcrest\Util::registerGlobalFunctions(); diff --git a/tests/stubs/DefaultPaymentGateways.php b/tests/stubs/DefaultPaymentGateways.php new file mode 100644 index 000000000..a9bb2ba4b --- /dev/null +++ b/tests/stubs/DefaultPaymentGateways.php @@ -0,0 +1,17 @@ + Date: Fri, 27 Jun 2025 23:40:37 +0200 Subject: [PATCH 43/65] Merge branch 'trunk' of github.com:woocommerce/woocommerce-paypal-payments into PCP-4487-fastlane-uk-3ds-redirect From 40a95202ece5ecc07e0295eeb71ba0da09ec8519 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 30 Jun 2025 16:05:40 +0400 Subject: [PATCH 44/65] Move the reference transaction enabled check into helpers. With the 1st step we will move the class into helpers. The next step would be to refactor/rename it. --- modules/ppcp-api-client/services.php | 56 +++++++++--------- .../BillingAgreementsEndpoint.php | 5 +- .../src/SavePaymentMethodsModule.php | 4 +- .../src/StatusReportModule.php | 14 ++--- modules/ppcp-wc-gateway/services.php | 58 +++++++++---------- .../src/Assets/SettingsPageAssets.php | 4 +- .../src/Settings/SettingsListener.php | 5 +- .../ppcp-wc-gateway/src/WCGatewayModule.php | 34 +++++------ .../Settings/SettingsListenerTest.php | 4 +- .../PHPUnit/VaultingSubscriptionsTest.php | 4 +- 10 files changed, 94 insertions(+), 94 deletions(-) rename modules/ppcp-api-client/src/{Endpoint => Helper}/BillingAgreementsEndpoint.php (96%) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 788ca9244..af91c95e2 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -9,45 +9,33 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; +use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ClientCredentials; +use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; +use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory; -use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; -use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; -use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution; -use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; -use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; -use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Factory\AddressFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\FraudProcessorResponseFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory; @@ -56,33 +44,45 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; +use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution; use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer; use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; -use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; -use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; -use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; -use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory; +use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; +use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; +use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum; +use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; +use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( 'api.host' => static function( ContainerInterface $container ) : string { diff --git a/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php b/modules/ppcp-api-client/src/Helper/BillingAgreementsEndpoint.php similarity index 96% rename from modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php rename to modules/ppcp-api-client/src/Helper/BillingAgreementsEndpoint.php index ad922a0de..56c288c2a 100644 --- a/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php +++ b/modules/ppcp-api-client/src/Helper/BillingAgreementsEndpoint.php @@ -7,14 +7,15 @@ declare(strict_types=1); -namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; +namespace WooCommerce\PayPalCommerce\ApiClient\Helper; use Exception; +use Psr\Log\LoggerInterface; use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use Psr\Log\LoggerInterface; /** * Class BillingAgreementsEndpoint diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 7f441d337..03739f473 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -12,21 +12,21 @@ namespace WooCommerce\PayPalCommerce\SavePaymentMethods; use Psr\Log\LoggerInterface; use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken; +use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; -use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index 10159f8c7..9e32d2aaf 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -9,18 +9,18 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\StatusReport; +use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; +use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; -use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; -use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; -use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper; +use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Webhooks\WebhookEventStorage; /** diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index b1101dec7..b2a47e4cd 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -11,51 +11,37 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\ExperienceContext; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; +use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary; use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ShippingCallbackEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Settings\SettingsModule; -use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction; -use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector; -use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater; -use WooCommerce\PayPalCommerce\WcGateway\Notice\SendOnlyCountryNotice; -use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Factory\SimpleRedirectTaskFactory; -use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Factory\SimpleRedirectTaskFactoryInterface; -use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrar; -use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrarInterface; -use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Tasks\SimpleRedirectTask; -use WooCommerce\PayPalCommerce\WcGateway\Shipping\ShippingCallbackUrlFactory; -use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Endpoint\CartEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\CartFactory; -use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\CartTotalsFactory; -use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\MoneyFactory; -use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\ShippingRatesFactory; -use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer; use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn; use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail; use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction; +use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction; use WooCommerce\PayPalCommerce\WcGateway\Assets\FraudNetAssets; +use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets; use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; use WooCommerce\PayPalCommerce\WcGateway\Cli\SettingsCommand; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ShippingCallbackEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint; use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet; use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSourceWebsiteId; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; @@ -69,17 +55,24 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PaymentSourceFac use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoice; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector; use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper; +use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager; +use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; +use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater; +use WooCommerce\PayPalCommerce\WcGateway\Helper\InstallmentsProductStatus; +use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; -use WooCommerce\PayPalCommerce\WcGateway\Helper\InstallmentsProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\SendOnlyCountryNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\UnsupportedCurrencyAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; @@ -89,11 +82,18 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; -use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary; -use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; -use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; -use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; -use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails; +use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Factory\SimpleRedirectTaskFactory; +use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Factory\SimpleRedirectTaskFactoryInterface; +use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrar; +use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrarInterface; +use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Tasks\SimpleRedirectTask; +use WooCommerce\PayPalCommerce\WcGateway\Shipping\ShippingCallbackUrlFactory; +use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Endpoint\CartEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\CartFactory; +use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\CartTotalsFactory; +use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\MoneyFactory; +use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Factory\ShippingRatesFactory; +use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; return array( 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index 4ab12b13b..d7b4a0618 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -9,10 +9,10 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Assets; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; -use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; /** diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index 9eb5990c2..d04789ded 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -13,13 +13,12 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Http\RedirectorInterface; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index b7d70857a..e8df724ae 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -12,57 +12,57 @@ namespace WooCommerce\PayPalCommerce\WcGateway; use Exception; use Psr\Log\LoggerInterface; use Throwable; +use WC_Order; use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; +use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\LocalApmProductStatus; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; -use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ShippingCallbackEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; -use WooCommerce\PayPalCommerce\WcGateway\Helper\InstallmentsProductStatus; -use WooCommerce\PayPalCommerce\WcGateway\Notice\SendOnlyCountryNotice; -use WooCommerce\PayPalCommerce\WcGateway\Processor\CreditCardOrderInfoHandlingTrait; -use WC_Order; -use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository; -use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; -use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; -use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer; use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn; use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail; use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction; use WooCommerce\PayPalCommerce\WcGateway\Assets\FraudNetAssets; use WooCommerce\PayPalCommerce\WcGateway\Assets\SettingsPageAssets; +use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets; use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ShippingCallbackEndpoint; +use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; +use WooCommerce\PayPalCommerce\WcGateway\Helper\InstallmentsProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\GatewayWithoutPayPalAdminNotice; +use WooCommerce\PayPalCommerce\WcGateway\Notice\SendOnlyCountryNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\UnsupportedCurrencyAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\CreditCardOrderInfoHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Settings\HeaderRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrarInterface; -use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; -use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\LocalApmProductStatus; /** * Class WcGatewayModule diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index 74f6ed074..b28417d7e 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -2,14 +2,14 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use Mockery; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Helper\RedirectorStub; use WooCommerce\PayPalCommerce\ModularTestCase; use WooCommerce\PayPalCommerce\Onboarding\State; -use Mockery; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index bb994700e..410402441 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -4,15 +4,15 @@ namespace WooCommerce\PayPalCommerce\Tests\Integration; use WC_Payment_Token; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\Token; +use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; /** * @group subscriptions From 0a0d7e9c49bf615ad8fcd420e589b37fa789e473 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 30 Jun 2025 16:08:36 +0400 Subject: [PATCH 45/65] Refactor/rename the reference transaction enabled checking class. --- modules/ppcp-api-client/services.php | 6 +++--- ...eementsEndpoint.php => ReferenceTransactionStatus.php} | 2 +- .../src/SavePaymentMethodsModule.php | 4 ++-- modules/ppcp-status-report/src/StatusReportModule.php | 8 ++++---- modules/ppcp-wc-gateway/services.php | 4 ++-- modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php | 8 ++++---- modules/ppcp-wc-gateway/src/Settings/SettingsListener.php | 8 ++++---- modules/ppcp-wc-gateway/src/WCGatewayModule.php | 4 ++-- tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php | 4 ++-- tests/integration/PHPUnit/VaultingSubscriptionsTest.php | 4 ++-- 10 files changed, 26 insertions(+), 26 deletions(-) rename modules/ppcp-api-client/src/Helper/{BillingAgreementsEndpoint.php => ReferenceTransactionStatus.php} (99%) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index af91c95e2..c015a19ae 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -62,7 +62,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; @@ -274,8 +274,8 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint { - return new BillingAgreementsEndpoint( + 'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): ReferenceTransactionStatus { + return new ReferenceTransactionStatus( $container->get( 'api.host' ), $container->get( 'api.bearer' ), $container->get( 'woocommerce.logger.woocommerce' ) diff --git a/modules/ppcp-api-client/src/Helper/BillingAgreementsEndpoint.php b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php similarity index 99% rename from modules/ppcp-api-client/src/Helper/BillingAgreementsEndpoint.php rename to modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php index 56c288c2a..253a9ebda 100644 --- a/modules/ppcp-api-client/src/Helper/BillingAgreementsEndpoint.php +++ b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php @@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; /** * Class BillingAgreementsEndpoint */ -class BillingAgreementsEndpoint { +class ReferenceTransactionStatus { use RequestTrait; /** diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 03739f473..292bd2fd5 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -17,7 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken; use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest; @@ -70,7 +70,7 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut assert( $settings instanceof Settings ); $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); $reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); if ( $reference_transaction_enabled !== true ) { $settings->set( 'vault_enabled', false ); diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index 9e32d2aaf..c83d68cd3 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\StatusReport; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper; @@ -76,7 +76,7 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo assert( $last_webhook_storage instanceof WebhookEventStorage ); $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); /* @var Renderer $renderer The renderer. */ $renderer = $c->get( 'status-report.renderer' ); @@ -278,9 +278,9 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo /** * Checks if reference transactions are enabled in account. * - * @param BillingAgreementsEndpoint $billing_agreements_endpoint The endpoint. + * @param ReferenceTransactionStatus $billing_agreements_endpoint The endpoint. */ - private function reference_transaction_enabled( BillingAgreementsEndpoint $billing_agreements_endpoint ): bool { + private function reference_transaction_enabled( ReferenceTransactionStatus $billing_agreements_endpoint ): bool { try { return $billing_agreements_endpoint->reference_transaction_enabled(); } catch ( RuntimeException $exception ) { diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index b2a47e4cd..93edf0fc6 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -14,7 +14,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\ExperienceContext; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; @@ -1774,7 +1774,7 @@ return array( assert( $environment instanceof Environment ); $billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); $enabled = $billing_agreements_endpoint->reference_transaction_enabled(); diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index d7b4a0618..11f974f1a 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Assets; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; @@ -107,7 +107,7 @@ class SettingsPageAssets { /** * Billing Agreements endpoint. * - * @var BillingAgreementsEndpoint + * @var ReferenceTransactionStatus */ private $billing_agreements_endpoint; @@ -133,7 +133,7 @@ class SettingsPageAssets { * @param array $all_funding_sources The list of all existing funding sources. * @param bool $is_settings_page Whether it's a settings page of this plugin. * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. - * @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint. + * @param ReferenceTransactionStatus $billing_agreements_endpoint Billing Agreements endpoint. * @param bool $is_paypal_payment_method_page Whether we're on a settings page for our plugin's payment methods. */ public function __construct( @@ -149,7 +149,7 @@ class SettingsPageAssets { array $all_funding_sources, bool $is_settings_page, bool $is_acdc_enabled, - BillingAgreementsEndpoint $billing_agreements_endpoint, + ReferenceTransactionStatus $billing_agreements_endpoint, bool $is_paypal_payment_method_page ) { $this->module_url = $module_url; diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index d04789ded..fc07e72cd 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -14,7 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Http\RedirectorInterface; use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; @@ -154,7 +154,7 @@ class SettingsListener { /** * Billing Agreements endpoint. * - * @var BillingAgreementsEndpoint + * @var ReferenceTransactionStatus */ private $billing_agreements_endpoint; @@ -189,7 +189,7 @@ class SettingsListener { * @param RedirectorInterface $redirector The HTTP redirector. * @param string $partner_merchant_id_production Partner merchant ID production. * @param string $partner_merchant_id_sandbox Partner merchant ID sandbox. - * @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint. + * @param ReferenceTransactionStatus $billing_agreements_endpoint Billing Agreements endpoint. * @param ?LoggerInterface $logger The logger. * @param Cache $client_credentials_cache The client credentials cache. */ @@ -208,7 +208,7 @@ class SettingsListener { RedirectorInterface $redirector, string $partner_merchant_id_production, string $partner_merchant_id_sandbox, - BillingAgreementsEndpoint $billing_agreements_endpoint, + ReferenceTransactionStatus $billing_agreements_endpoint, LoggerInterface $logger = null, Cache $client_credentials_cache ) { diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index e8df724ae..070f59635 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -19,7 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\LocalApmProductStatus; @@ -537,7 +537,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul } $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); $dcc_product_status = $c->get( 'wcgateway.helper.dcc-product-status' ); assert( $dcc_product_status instanceof DCCProductStatus ); diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index b28417d7e..c29fb3004 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -5,7 +5,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings; use Mockery; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Helper\RedirectorStub; use WooCommerce\PayPalCommerce\ModularTestCase; @@ -43,7 +43,7 @@ class SettingsListenerTest extends ModularTestCase $signup_link_ids = array(); $pui_status_cache = Mockery::mock(Cache::class); $dcc_status_cache = Mockery::mock(Cache::class); - $billing_agreement_endpoint = Mockery::mock(BillingAgreementsEndpoint::class); + $billing_agreement_endpoint = Mockery::mock(ReferenceTransactionStatus::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class); $logger = Mockery::mock(LoggerInterface::class); $client_credentials_cache = Mockery::mock(Cache::class); diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index 410402441..72e35e54f 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -8,7 +8,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\Token; -use WooCommerce\PayPalCommerce\ApiClient\Helper\BillingAgreementsEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; @@ -97,7 +97,7 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase add_filter('user_has_cap', $user_has_cap_callback, 10, 3); // Convert to Mockery mocks - $billing_agreements_endpoint_mock = \Mockery::mock(BillingAgreementsEndpoint::class); + $billing_agreements_endpoint_mock = \Mockery::mock(ReferenceTransactionStatus::class); $billing_agreements_endpoint_mock->shouldReceive('reference_transaction_enabled') ->andReturn(true); From 96895aba51960c10a945a1a97e11a9351c0be216 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 30 Jun 2025 16:40:38 +0400 Subject: [PATCH 46/65] Refactor the service name, variable names to use the new class --- modules/ppcp-api-client/services.php | 10 +- .../src/Helper/ReferenceTransactionStatus.php | 131 ++---------------- .../src/SavePaymentMethodsModule.php | 8 +- .../src/StatusReportModule.php | 19 +-- modules/ppcp-wc-gateway/services.php | 15 +- .../src/Assets/SettingsPageAssets.php | 15 +- .../src/Settings/SettingsListener.php | 17 +-- .../ppcp-wc-gateway/src/WCGatewayModule.php | 8 +- 8 files changed, 46 insertions(+), 177 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index c015a19ae..aff933acd 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -274,13 +274,9 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): ReferenceTransactionStatus { - return new ReferenceTransactionStatus( - $container->get( 'api.host' ), - $container->get( 'api.bearer' ), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, + 'api.reference-transaction-status' => static fn ( ContainerInterface $container ): ReferenceTransactionStatus => new ReferenceTransactionStatus( + $container->get( 'api.endpoint.partners' ) + ), 'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts { return new CatalogProducts( $container->get( 'api.host' ), diff --git a/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php index 253a9ebda..9991ea831 100644 --- a/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php +++ b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php @@ -9,110 +9,16 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Helper; -use Exception; use Psr\Log\LoggerInterface; -use stdClass; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait; -use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -/** - * Class BillingAgreementsEndpoint - */ class ReferenceTransactionStatus { - use RequestTrait; - /** - * The host. - * - * @var string - */ - private $host; + protected PartnersEndpoint $partners_endpoint; - /** - * The bearer. - * - * @var Bearer - */ - private $bearer; - - /** - * The logger. - * - * @var LoggerInterface - */ - private $logger; - - /** - * BillingAgreementsEndpoint constructor. - * - * @param string $host The host. - * @param Bearer $bearer The bearer. - * @param LoggerInterface $logger The logger. - */ - public function __construct( - string $host, - Bearer $bearer, - LoggerInterface $logger - ) { - $this->host = $host; - $this->bearer = $bearer; - $this->logger = $logger; - } - - /** - * Creates a billing agreement token. - * - * @param string $description The description. - * @param string $return_url The return URL. - * @param string $cancel_url The cancel URL. - * - * @throws RuntimeException If the request fails. - * @throws PayPalApiException If the request fails. - */ - public function create_token( string $description, string $return_url, string $cancel_url ): stdClass { - $data = array( - 'description' => $description, - 'payer' => array( - 'payment_method' => 'PAYPAL', - ), - 'plan' => array( - 'type' => 'MERCHANT_INITIATED_BILLING', - 'merchant_preferences' => array( - 'return_url' => $return_url, - 'cancel_url' => $cancel_url, - 'skip_shipping_address' => true, - ), - ), - ); - - $bearer = $this->bearer->bearer(); - $url = trailingslashit( $this->host ) . 'v1/billing-agreements/agreement-tokens'; - $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 ) || ! is_array( $response ) ) { - throw new RuntimeException( 'Not able to create a billing agreement token.' ); - } - - $json = json_decode( $response['body'] ); - $status_code = (int) wp_remote_retrieve_response_code( $response ); - if ( 201 !== $status_code ) { - throw new PayPalApiException( - $json, - $status_code - ); - } - - return $json; + public function __construct( PartnersEndpoint $partners_endpoint ) { + $this->partners_endpoint = $partners_endpoint; } /** @@ -122,27 +28,18 @@ class ReferenceTransactionStatus { */ public function reference_transaction_enabled(): bool { try { - if ( wc_string_to_bool( get_transient( 'ppcp_reference_transaction_enabled' ) ) === true ) { - return true; + foreach ( $this->partners_endpoint->seller_status()->capabilities() as $capability ) { + if ( + $capability->name() === 'PAYPAL_WALLET_VAULTING_ADVANCED' && + $capability->status() === 'ACTIVE' + ) { + return true; + } } - - $this->is_request_logging_enabled = false; - - try { - $this->create_token( - 'Checking if reference transactions are enabled', - 'https://example.com/return', - 'https://example.com/cancel' - ); - } finally { - $this->is_request_logging_enabled = true; - } - - set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS ); - return true; - } catch ( Exception $exception ) { - set_transient( 'ppcp_reference_transaction_enabled', false, HOUR_IN_SECONDS ); + } catch ( RuntimeException $exception ) { return false; } + + return false; } } diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 292bd2fd5..eb8e406c6 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -69,10 +69,10 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); - $reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); - if ( $reference_transaction_enabled !== true ) { + $reference_transaction_status = $c->get( 'api.reference-transaction-status' ); + assert( $reference_transaction_status instanceof ReferenceTransactionStatus ); + + if ( ! $reference_transaction_status->reference_transaction_enabled() ) { $settings->set( 'vault_enabled', false ); $settings->persist(); } diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index c83d68cd3..25aca72b9 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -75,8 +75,8 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo $last_webhook_storage = $c->get( 'webhook.last-webhook-storage' ); assert( $last_webhook_storage instanceof WebhookEventStorage ); - $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); + $reference_transaction_status = $c->get( 'api.reference-transaction-status' ); + assert( $reference_transaction_status instanceof ReferenceTransactionStatus ); /* @var Renderer $renderer The renderer. */ $renderer = $c->get( 'status-report.renderer' ); @@ -170,7 +170,7 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo 'exported_label' => 'Reference Transactions', 'description' => esc_html__( 'Whether Reference Transactions are enabled for the connected account', 'woocommerce-paypal-payments' ), 'value' => $this->bool_to_html( - $this->reference_transaction_enabled( $billing_agreements_endpoint ) + $reference_transaction_status->reference_transaction_enabled() ), ), array( @@ -275,19 +275,6 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo return $field_settings['options'][ $subscriptions_mode ] ?? $subscriptions_mode; } - /** - * Checks if reference transactions are enabled in account. - * - * @param ReferenceTransactionStatus $billing_agreements_endpoint The endpoint. - */ - private function reference_transaction_enabled( ReferenceTransactionStatus $billing_agreements_endpoint ): bool { - try { - return $billing_agreements_endpoint->reference_transaction_enabled(); - } catch ( RuntimeException $exception ) { - return false; - } - } - /** * Converts the bool value to "yes" icon or dash. * diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 93edf0fc6..0f1021996 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -547,7 +547,7 @@ return array( $container->get( 'http.redirector' ), $container->get( 'api.partner_merchant_id-production' ), $container->get( 'api.partner_merchant_id-sandbox' ), - $container->get( 'api.endpoint.billing-agreements' ), + $container->get( 'api.reference-transaction-status' ), $container->get( 'woocommerce.logger.woocommerce' ), new Cache( 'ppcp-client-credentials-cache' ) ); @@ -651,9 +651,10 @@ return array( 'wcgateway.settings.fields.subscriptions_mode' => static function ( ContainerInterface $container ): array { $subscription_mode_options = $container->get( 'wcgateway.settings.fields.subscriptions_mode_options' ); - $billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' ); - $reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); - if ( $reference_transaction_enabled !== true ) { + $reference_transaction_status = $container->get( 'api.reference-transaction-status' ); + assert( $reference_transaction_status instanceof ReferenceTransactionStatus ); + + if ( ! $reference_transaction_status->reference_transaction_enabled() ) { unset( $subscription_mode_options['vaulting_api'] ); } @@ -1773,10 +1774,10 @@ return array( $environment = $container->get( 'settings.environment' ); assert( $environment instanceof Environment ); - $billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); + $reference_transaction_status = $container->get( 'api.reference-transaction-status' ); + assert( $reference_transaction_status instanceof ReferenceTransactionStatus ); - $enabled = $billing_agreements_endpoint->reference_transaction_enabled(); + $enabled = $reference_transaction_status->reference_transaction_enabled(); $enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' ); $disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' ); diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index 11f974f1a..4c45ababc 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -104,12 +104,7 @@ class SettingsPageAssets { */ private $is_acdc_enabled; - /** - * Billing Agreements endpoint. - * - * @var ReferenceTransactionStatus - */ - private $billing_agreements_endpoint; + private $reference_transaction_status; /** * Whether we're on a settings page for our plugin's payment methods. @@ -133,7 +128,7 @@ class SettingsPageAssets { * @param array $all_funding_sources The list of all existing funding sources. * @param bool $is_settings_page Whether it's a settings page of this plugin. * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. - * @param ReferenceTransactionStatus $billing_agreements_endpoint Billing Agreements endpoint. + * @param ReferenceTransactionStatus $reference_transaction_status * @param bool $is_paypal_payment_method_page Whether we're on a settings page for our plugin's payment methods. */ public function __construct( @@ -149,7 +144,7 @@ class SettingsPageAssets { array $all_funding_sources, bool $is_settings_page, bool $is_acdc_enabled, - ReferenceTransactionStatus $billing_agreements_endpoint, + ReferenceTransactionStatus $reference_transaction_status, bool $is_paypal_payment_method_page ) { $this->module_url = $module_url; @@ -164,7 +159,7 @@ class SettingsPageAssets { $this->all_funding_sources = $all_funding_sources; $this->is_settings_page = $is_settings_page; $this->is_acdc_enabled = $is_acdc_enabled; - $this->billing_agreements_endpoint = $billing_agreements_endpoint; + $this->reference_transaction_status = $reference_transaction_status; $this->is_paypal_payment_method_page = $is_paypal_payment_method_page; } @@ -239,7 +234,7 @@ class SettingsPageAssets { ), ), ), - 'reference_transaction_enabled' => $this->billing_agreements_endpoint->reference_transaction_enabled(), + 'reference_transaction_enabled' => $this->reference_transaction_status->reference_transaction_enabled(), 'vaulting_must_enable_advanced_wallet_message' => sprintf( // translators: %1$s and %2$s are the opening and closing of HTML tag. esc_html__( 'Your PayPal account must be eligible to %1$ssave PayPal and Venmo payment methods%2$s to enable PayPal Vaulting.', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index fc07e72cd..a446ee9e6 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -151,12 +151,7 @@ class SettingsListener { */ private $partner_merchant_id_sandbox; - /** - * Billing Agreements endpoint. - * - * @var ReferenceTransactionStatus - */ - private $billing_agreements_endpoint; + private $reference_transaction_status; /** * The logger. @@ -189,7 +184,7 @@ class SettingsListener { * @param RedirectorInterface $redirector The HTTP redirector. * @param string $partner_merchant_id_production Partner merchant ID production. * @param string $partner_merchant_id_sandbox Partner merchant ID sandbox. - * @param ReferenceTransactionStatus $billing_agreements_endpoint Billing Agreements endpoint. + * @param ReferenceTransactionStatus $reference_transaction_status * @param ?LoggerInterface $logger The logger. * @param Cache $client_credentials_cache The client credentials cache. */ @@ -208,7 +203,7 @@ class SettingsListener { RedirectorInterface $redirector, string $partner_merchant_id_production, string $partner_merchant_id_sandbox, - ReferenceTransactionStatus $billing_agreements_endpoint, + ReferenceTransactionStatus $reference_transaction_status, LoggerInterface $logger = null, Cache $client_credentials_cache ) { @@ -229,7 +224,7 @@ class SettingsListener { $this->redirector = $redirector; $this->partner_merchant_id_production = $partner_merchant_id_production; $this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox; - $this->billing_agreements_endpoint = $billing_agreements_endpoint; + $this->reference_transaction_status = $reference_transaction_status; $this->logger = $logger ?: new NullLogger(); $this->client_credentials_cache = $client_credentials_cache; } @@ -393,9 +388,7 @@ class SettingsListener { $vault_enabled = wc_clean( wp_unslash( $_POST['ppcp']['vault_enabled'] ?? '' ) ); $subscription_mode = wc_clean( wp_unslash( $_POST['ppcp']['subscriptions_mode'] ?? '' ) ); - $reference_transaction_enabled = $this->billing_agreements_endpoint->reference_transaction_enabled(); - - if ( $reference_transaction_enabled !== true ) { + if ( ! $this->reference_transaction_status->reference_transaction_enabled() ) { $this->settings->set( 'vault_enabled', false ); /** diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 070f59635..5d884d0fb 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -214,7 +214,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul $c->get( 'wcgateway.settings.funding-sources' ), $c->get( 'wcgateway.is-ppcp-settings-page' ), $dcc_configuration->is_enabled(), - $c->get( 'api.endpoint.billing-agreements' ), + $c->get( 'api.reference-transaction-status' ), $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) ); $assets->register_assets(); @@ -536,8 +536,8 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul return $features; } - $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof ReferenceTransactionStatus ); + $reference_transaction_status = $c->get( 'api.reference-transaction-status' ); + assert( $reference_transaction_status instanceof ReferenceTransactionStatus ); $dcc_product_status = $c->get( 'wcgateway.helper.dcc-product-status' ); assert( $dcc_product_status instanceof DCCProductStatus ); @@ -552,7 +552,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul assert( is_callable( $contact_module_check ) ); $features['save_paypal_and_venmo'] = array( - 'enabled' => $billing_agreements_endpoint->reference_transaction_enabled(), + 'enabled' => $reference_transaction_status->reference_transaction_enabled(), ); $features['advanced_credit_and_debit_cards'] = array( From b86f62e85295b2d4a1eb83f4a210f895056d60a3 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 30 Jun 2025 16:45:36 +0400 Subject: [PATCH 47/65] Refactor the service name, variable names to use the new class --- tests/integration/PHPUnit/VaultingSubscriptionsTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php index 72e35e54f..54acc4de8 100644 --- a/tests/integration/PHPUnit/VaultingSubscriptionsTest.php +++ b/tests/integration/PHPUnit/VaultingSubscriptionsTest.php @@ -97,8 +97,8 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase add_filter('user_has_cap', $user_has_cap_callback, 10, 3); // Convert to Mockery mocks - $billing_agreements_endpoint_mock = \Mockery::mock(ReferenceTransactionStatus::class); - $billing_agreements_endpoint_mock->shouldReceive('reference_transaction_enabled') + $reference_transaction_status = \Mockery::mock(ReferenceTransactionStatus::class); + $reference_transaction_status->shouldReceive('reference_transaction_enabled') ->andReturn(true); $state_mock = \Mockery::mock(State::class); @@ -115,8 +115,8 @@ class VaultingSubscriptionsTest extends IntegrationMockedTestCase // Create and configure the SettingsListener $c = $this->bootstrapModule([ - 'api.endpoint.billing-agreements' => function () use ($billing_agreements_endpoint_mock) { - return $billing_agreements_endpoint_mock; + 'api.endpoint.billing-agreements' => function () use ($reference_transaction_status) { + return $reference_transaction_status; }, 'onboarding.state' => function () use ($state_mock) { return $state_mock; From 241bddf90857e78ece1c25292a0ba57fe0790b46 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 30 Jun 2025 16:46:48 +0400 Subject: [PATCH 48/65] Refactor billing agreements into Reference transaction status helper class --- .../src/Helper/ReferenceTransactionStatus.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php index 9991ea831..71fa557b4 100644 --- a/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php +++ b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php @@ -1,18 +1,22 @@ Date: Mon, 30 Jun 2025 17:47:58 +0400 Subject: [PATCH 49/65] Fix psalm --- modules/ppcp-wc-gateway/src/Settings/SettingsListener.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index a446ee9e6..dde9ee7dc 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -151,7 +151,7 @@ class SettingsListener { */ private $partner_merchant_id_sandbox; - private $reference_transaction_status; + private ReferenceTransactionStatus $reference_transaction_status; /** * The logger. @@ -405,7 +405,9 @@ class SettingsListener { $this->settings->persist(); } - if ( $subscription_mode === 'vaulting_api' && $vault_enabled !== '1' && $reference_transaction_enabled === true ) { + $reference_transaction_enabled = $this->reference_transaction_status->reference_transaction_enabled(); + + if ( $subscription_mode === 'vaulting_api' && $vault_enabled !== '1' && $reference_transaction_enabled ) { $this->settings->set( 'vault_enabled', true ); $this->settings->persist(); } From a1c403e61eb8fbc6b17ba642a8e0f07a9d48631d Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Mon, 30 Jun 2025 18:28:30 +0400 Subject: [PATCH 50/65] Fix the cs --- modules/ppcp-api-client/services.php | 2 +- .../src/Endpoint/CreateOrderEndpoint.php | 34 +++++++++---------- .../src/Assets/SettingsPageAssets.php | 28 +++++++-------- .../src/Settings/SettingsListener.php | 34 +++++++++---------- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index aff933acd..ca6faf11e 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -274,7 +274,7 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'api.reference-transaction-status' => static fn ( ContainerInterface $container ): ReferenceTransactionStatus => new ReferenceTransactionStatus( + 'api.reference-transaction-status' => static fn ( ContainerInterface $container ): ReferenceTransactionStatus => new ReferenceTransactionStatus( $container->get( 'api.endpoint.partners' ) ), 'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts { diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index eec1d1b64..7f81f2143 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -232,24 +232,24 @@ class CreateOrderEndpoint implements EndpointInterface { LoggerInterface $logger ) { - $this->request_data = $request_data; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->contact_preference_factory = $contact_preference_factory; - $this->experience_context_builder = $experience_context_builder; - $this->api_endpoint = $order_endpoint; - $this->payer_factory = $payer_factory; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->early_order_handler = $early_order_handler; - $this->registration_needed = $registration_needed; - $this->card_billing_data_mode = $card_billing_data_mode; - $this->early_validation_enabled = $early_validation_enabled; - $this->pay_now_contexts = $pay_now_contexts; - $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; + $this->request_data = $request_data; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->contact_preference_factory = $contact_preference_factory; + $this->experience_context_builder = $experience_context_builder; + $this->api_endpoint = $order_endpoint; + $this->payer_factory = $payer_factory; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->early_order_handler = $early_order_handler; + $this->registration_needed = $registration_needed; + $this->card_billing_data_mode = $card_billing_data_mode; + $this->early_validation_enabled = $early_validation_enabled; + $this->pay_now_contexts = $pay_now_contexts; + $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; $this->server_side_shipping_callback_enabled = $server_side_shipping_callback_enabled; - $this->funding_sources_without_redirect = $funding_sources_without_redirect; - $this->logger = $logger; + $this->funding_sources_without_redirect = $funding_sources_without_redirect; + $this->logger = $logger; } /** diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index 4c45ababc..c8b39f61a 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -116,20 +116,20 @@ class SettingsPageAssets { /** * Assets constructor. * - * @param string $module_url The url of this module. - * @param string $version The assets version. - * @param SubscriptionHelper $subscription_helper The subscription helper. - * @param string $client_id The PayPal SDK client ID. - * @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop. - * @param string $country 2-letter country code of the shop. - * @param Environment $environment The environment object. - * @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page. - * @param array $disabled_sources The list of disabled funding sources. - * @param array $all_funding_sources The list of all existing funding sources. - * @param bool $is_settings_page Whether it's a settings page of this plugin. - * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. + * @param string $module_url The url of this module. + * @param string $version The assets version. + * @param SubscriptionHelper $subscription_helper The subscription helper. + * @param string $client_id The PayPal SDK client ID. + * @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop. + * @param string $country 2-letter country code of the shop. + * @param Environment $environment The environment object. + * @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page. + * @param array $disabled_sources The list of disabled funding sources. + * @param array $all_funding_sources The list of all existing funding sources. + * @param bool $is_settings_page Whether it's a settings page of this plugin. + * @param bool $is_acdc_enabled Whether the ACDC gateway is enabled. * @param ReferenceTransactionStatus $reference_transaction_status - * @param bool $is_paypal_payment_method_page Whether we're on a settings page for our plugin's payment methods. + * @param bool $is_paypal_payment_method_page Whether we're on a settings page for our plugin's payment methods. */ public function __construct( string $module_url, @@ -159,7 +159,7 @@ class SettingsPageAssets { $this->all_funding_sources = $all_funding_sources; $this->is_settings_page = $is_settings_page; $this->is_acdc_enabled = $is_acdc_enabled; - $this->reference_transaction_status = $reference_transaction_status; + $this->reference_transaction_status = $reference_transaction_status; $this->is_paypal_payment_method_page = $is_paypal_payment_method_page; } diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index dde9ee7dc..c20e920e6 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -170,23 +170,23 @@ class SettingsListener { /** * SettingsListener constructor. * - * @param Settings $settings The settings. - * @param array $setting_fields The setting fields. - * @param WebhookRegistrar $webhook_registrar The Webhook Registrar. - * @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. - * @param Cache $signup_link_cache The signup link cache. - * @param array $signup_link_ids Signup link ids. - * @param Cache $pui_status_cache The PUI status cache. - * @param Cache $dcc_status_cache The DCC status cache. - * @param RedirectorInterface $redirector The HTTP redirector. - * @param string $partner_merchant_id_production Partner merchant ID production. - * @param string $partner_merchant_id_sandbox Partner merchant ID sandbox. + * @param Settings $settings The settings. + * @param array $setting_fields The setting fields. + * @param WebhookRegistrar $webhook_registrar The Webhook Registrar. + * @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. + * @param Cache $signup_link_cache The signup link cache. + * @param array $signup_link_ids Signup link ids. + * @param Cache $pui_status_cache The PUI status cache. + * @param Cache $dcc_status_cache The DCC status cache. + * @param RedirectorInterface $redirector The HTTP redirector. + * @param string $partner_merchant_id_production Partner merchant ID production. + * @param string $partner_merchant_id_sandbox Partner merchant ID sandbox. * @param ReferenceTransactionStatus $reference_transaction_status - * @param ?LoggerInterface $logger The logger. - * @param Cache $client_credentials_cache The client credentials cache. + * @param ?LoggerInterface $logger The logger. + * @param Cache $client_credentials_cache The client credentials cache. */ public function __construct( Settings $settings, @@ -224,7 +224,7 @@ class SettingsListener { $this->redirector = $redirector; $this->partner_merchant_id_production = $partner_merchant_id_production; $this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox; - $this->reference_transaction_status = $reference_transaction_status; + $this->reference_transaction_status = $reference_transaction_status; $this->logger = $logger ?: new NullLogger(); $this->client_credentials_cache = $client_credentials_cache; } From 2505802743370b8fdfb016d62fed9efdafd54366 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Jul 2025 12:36:11 +0200 Subject: [PATCH 51/65] =?UTF-8?q?=F0=9F=94=A7=20Improve=20PayPal=20respons?= =?UTF-8?q?e=20handling=20and=20code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Factory/AmountFactory.php | 11 +- .../src/Factory/OrderFactory.php | 120 ++++++------------ .../src/Factory/PurchaseUnitFactory.php | 11 +- modules/ppcp-axo/src/AxoModule.php | 14 +- modules/ppcp-axo/src/Gateway/AxoGateway.php | 6 +- .../src/Endpoint/ReturnUrlEndpoint.php | 6 +- 6 files changed, 72 insertions(+), 96 deletions(-) diff --git a/modules/ppcp-api-client/src/Factory/AmountFactory.php b/modules/ppcp-api-client/src/Factory/AmountFactory.php index 5cab0b221..fb4fd0785 100644 --- a/modules/ppcp-api-client/src/Factory/AmountFactory.php +++ b/modules/ppcp-api-client/src/Factory/AmountFactory.php @@ -198,12 +198,15 @@ class AmountFactory { /** * Returns an Amount object based off a PayPal Response. * - * @param \stdClass $data The JSON object. + * @param mixed $data The JSON object. * - * @return Amount - * @throws RuntimeException When JSON object is malformed. + * @return Amount|null */ - public function from_paypal_response( \stdClass $data ): Amount { + public function from_paypal_response( $data ) { + if ( null === $data || ! $data instanceof \stdClass ) { + return null; + } + $money = $this->money_factory->from_paypal_response( $data ); $breakdown = ( isset( $data->breakdown ) ) ? $this->break_down( $data->breakdown ) : null; return new Amount( $money, $breakdown ); diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 300985804..0f994f7ba 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -81,7 +81,25 @@ class OrderFactory { * @throws RuntimeException When JSON object is malformed. */ public function from_paypal_response( \stdClass $order_data ): Order { - return $this->create_order_from_response( $order_data, false ); + $this->validate_order_id( $order_data ); + + $purchase_units = $this->create_purchase_units( $order_data ); + $status = $this->create_order_status( $order_data ); + $intent = $this->get_intent( $order_data ); + $timestamps = $this->create_timestamps( $order_data ); + $payer = $this->create_payer( $order_data ); + $payment_source = $this->create_payment_source( $order_data ); + + return new Order( + $order_data->id, + $purchase_units, + $status, + $payment_source, + $payer, + $intent, + $timestamps['create_time'], + $timestamps['update_time'] + ); } /** @@ -93,28 +111,15 @@ class OrderFactory { * @throws RuntimeException When JSON object is malformed. */ public function from_paypal_response_with_3ds( \stdClass $order_data ): Order { - return $this->create_order_from_response( $order_data, true ); - } - - /** - * Creates an Order object from PayPal response data. - * - * @param \stdClass $order_data The JSON object. - * @param bool $is_3ds_response Whether this is a 3DS response. - * - * @return Order - * @throws RuntimeException When JSON object is malformed. - */ - private function create_order_from_response( \stdClass $order_data, bool $is_3ds_response ): Order { $this->validate_order_id( $order_data ); - $purchase_units = $this->create_purchase_units( $order_data, $is_3ds_response ); - $status = $this->create_order_status( $order_data, $is_3ds_response ); - $intent = $this->get_intent( $order_data, $is_3ds_response ); - $timestamps = $this->create_timestamps( $order_data, $is_3ds_response ); - $payer = $this->create_payer( $order_data, $is_3ds_response ); + $purchase_units = $this->create_purchase_units( $order_data ); + $status = $this->create_order_status( $order_data ); + $intent = $this->get_intent( $order_data ); + $timestamps = $this->create_timestamps( $order_data ); + $payer = $this->create_payer( $order_data ); $payment_source = $this->create_payment_source( $order_data ); - $links = $is_3ds_response ? ( $order_data->links ?? null ) : null; + $links = $order_data->links ?? null; return new Order( $order_data->id, @@ -148,94 +153,56 @@ class OrderFactory { * Creates purchase units from order data. * * @param \stdClass $order_data The order data. - * @param bool $is_3ds_response Whether this is a 3DS response. * * @return array Array of PurchaseUnit objects. - * @throws RuntimeException When purchase units are required but missing. */ - private function create_purchase_units( \stdClass $order_data, bool $is_3ds_response ): array { - // 3DS responses don't contain purchase units. - if ( $is_3ds_response ) { + private function create_purchase_units( \stdClass $order_data ): array { + if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) { return array(); } - if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) { - throw new RuntimeException( - __( 'Order does not contain items.', 'woocommerce-paypal-payments' ) - ); + $purchase_units = array(); + foreach ( $order_data->purchase_units as $data ) { + $purchase_unit = $this->purchase_unit_factory->from_paypal_response( $data ); + if ( null !== $purchase_unit ) { + $purchase_units[] = $purchase_unit; + } } - return array_map( - function ( \stdClass $data ): PurchaseUnit { - return $this->purchase_unit_factory->from_paypal_response( $data ); - }, - $order_data->purchase_units - ); + return $purchase_units; } /** * Creates order status from order data. * * @param \stdClass $order_data The order data. - * @param bool $is_3ds_response Whether this is a 3DS response. * * @return OrderStatus - * @throws RuntimeException When status is required but missing. */ - private function create_order_status( \stdClass $order_data, bool $is_3ds_response ): OrderStatus { - if ( $is_3ds_response ) { - $status_value = $order_data->status ?? 'PAYER_ACTION_REQUIRED'; - return new OrderStatus( $status_value ); - } - - if ( ! isset( $order_data->status ) ) { - throw new RuntimeException( - __( 'Order does not contain status.', 'woocommerce-paypal-payments' ) - ); - } - - return new OrderStatus( $order_data->status ); + private function create_order_status( \stdClass $order_data ): OrderStatus { + $status_value = $order_data->status ?? 'PAYER_ACTION_REQUIRED'; + return new OrderStatus( $status_value ); } /** * Gets the intent from order data. * * @param \stdClass $order_data The order data. - * @param bool $is_3ds_response Whether this is a 3DS response. * * @return string - * @throws RuntimeException When intent is required but missing. */ - private function get_intent( \stdClass $order_data, bool $is_3ds_response ): string { - if ( $is_3ds_response ) { - return $order_data->intent ?? 'CAPTURE'; - } - - if ( ! isset( $order_data->intent ) ) { - throw new RuntimeException( - __( 'Order does not contain intent.', 'woocommerce-paypal-payments' ) - ); - } - - return $order_data->intent; + private function get_intent( \stdClass $order_data ): string { + return $order_data->intent ?? 'CAPTURE'; } /** * Creates timestamps from order data. * * @param \stdClass $order_data The order data. - * @param bool $is_3ds_response Whether this is a 3DS response. * * @return array Array with 'create_time' and 'update_time' keys. */ - private function create_timestamps( \stdClass $order_data, bool $is_3ds_response ): array { - if ( $is_3ds_response ) { - return array( - 'create_time' => null, - 'update_time' => null, - ); - } - + private function create_timestamps( \stdClass $order_data ): array { $create_time = isset( $order_data->create_time ) ? \DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time ) : null; @@ -254,15 +221,10 @@ class OrderFactory { * Creates payer from order data. * * @param \stdClass $order_data The order data. - * @param bool $is_3ds_response Whether this is a 3DS response. * * @return mixed Payer object or null. */ - private function create_payer( \stdClass $order_data, bool $is_3ds_response ) { - if ( $is_3ds_response ) { - return null; - } - + private function create_payer( \stdClass $order_data ) { return isset( $order_data->payer ) ? $this->payer_factory->from_paypal_response( $order_data->payer ) : null; diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 84231f8d8..cfc38e649 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -219,17 +219,22 @@ class PurchaseUnitFactory { * * @param \stdClass $data The JSON object. * - * @return PurchaseUnit + * @return ?PurchaseUnit * @throws RuntimeException When JSON object is malformed. */ - public function from_paypal_response( \stdClass $data ): PurchaseUnit { + public function from_paypal_response( \stdClass $data ): ?PurchaseUnit { if ( ! isset( $data->reference_id ) || ! is_string( $data->reference_id ) ) { throw new RuntimeException( __( 'No reference ID given.', 'woocommerce-paypal-payments' ) ); } - $amount = $this->amount_factory->from_paypal_response( $data->amount ); + $amount_data = $data->amount ?? null; + $amount = $this->amount_factory->from_paypal_response( $amount_data ); + if ( null === $amount ) { + return null; + } + $description = ( isset( $data->description ) ) ? $data->description : ''; $custom_id = ( isset( $data->custom_id ) ) ? $data->custom_id : ''; $invoice_id = ( isset( $data->invoice_id ) ) ? $data->invoice_id : ''; diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 1ce57e48a..1ca53222a 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -384,16 +384,21 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { return $data; } + if ( ! isset( $data['payment_source'] ) || ! isset( $data['payment_source']['card'] ) || ! is_object( $data['payment_source']['card'] ) ) { + return $data; + } + $settings_model = $c->get( 'settings.data.settings' ); assert( $settings_model instanceof SettingsModel ); $three_d_secure = $settings_model->get_three_d_secure_enum(); + $card = $data['payment_source']['card']; if ( $three_d_secure === 'SCA_ALWAYS' || $three_d_secure === 'SCA_WHEN_REQUIRED' ) { - $data['payment_source']['card']->attributes = array( + $card->attributes = array( 'verification' => array( 'method' => $three_d_secure, ), @@ -408,9 +413,9 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { ->with_current_locale() ->build()->to_array(); - $data['transaction_context'] = array( - 'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ), - ); + $data['transaction_context'] = array( + 'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ), + ); } return $data; @@ -418,7 +423,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { 10, 2 ); - return true; } diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index 9f966d1dd..9042fec30 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -306,7 +306,6 @@ class AxoGateway extends WC_Payment_Gateway { $this->order_processor->process_captured_and_authorized( $wc_order, $order ); } } catch ( Exception $exception ) { - // Error handling for payment failures. $this->logger->error( '[AXO] Payment processing failed: ' . $exception->getMessage() ); return array( 'result' => 'failure', @@ -399,8 +398,11 @@ class AxoGateway extends WC_Payment_Gateway { /** * Extract payer action URL from PayPal order. + * + * @param Order $order The PayPal order. + * @return string The payer action URL or an empty string if not found. */ - private function get_payer_action_url( Order $order ) { + private function get_payer_action_url( Order $order ) : string { $links = $order->links(); if ( ! $links ) { diff --git a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php index d848688e5..39b69ba5d 100644 --- a/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php +++ b/modules/ppcp-wc-gateway/src/Endpoint/ReturnUrlEndpoint.php @@ -13,6 +13,7 @@ use DomainException; use Psr\Log\LoggerInterface; use Exception; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -108,7 +109,6 @@ class ReturnUrlEndpoint { } } - // Get WooCommerce order ID. $wc_order_id = (int) $order->purchase_units()[0]->custom_id(); if ( ! $wc_order_id ) { // We cannot finish processing here without WC order, but at least go into the continuation mode. @@ -170,10 +170,10 @@ class ReturnUrlEndpoint { /** * Check if order needs 3DS completion. * - * @param mixed $order The PayPal order. + * @param Order $order The PayPal order. * @return bool */ - private function needs_3ds_completion( $order ): bool { + private function needs_3ds_completion( Order $order ): bool { // If order is still CREATED after 3DS redirect, it needs to be captured. return $order->status()->is( OrderStatus::CREATED ); } From fb0499ec7c9b3d565038a0993dc2b67704603d60 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Jul 2025 12:49:14 +0200 Subject: [PATCH 52/65] =?UTF-8?q?=F0=9F=A7=AA=20Remove=20failing=20no=5Fst?= =?UTF-8?q?atus=20test=20case=20from=20OrderFactoryTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php index 7e9afe1a7..82b0c7d1c 100644 --- a/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php @@ -181,13 +181,6 @@ class OrderFactoryTest extends TestCase 'intent' => '', ], ], - 'no_status' => [ - (object) [ - 'id' => '', - 'purchase_units' => [], - 'intent' => '', - ], - ], 'no_intent' => [ (object) [ 'id' => '', From c07b4f0dd5254c2b2aa1e1d12674d716fc743646 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Jul 2025 13:24:31 +0200 Subject: [PATCH 53/65] =?UTF-8?q?=F0=9F=94=92=20Fix=20Psalm=20errors=20wit?= =?UTF-8?q?h=20null-safe=20PayPal=20response=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Factory/CaptureFactory.php | 8 +++++++- modules/ppcp-api-client/src/Factory/RefundFactory.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-api-client/src/Factory/CaptureFactory.php b/modules/ppcp-api-client/src/Factory/CaptureFactory.php index 0c833fca6..9396ace93 100644 --- a/modules/ppcp-api-client/src/Factory/CaptureFactory.php +++ b/modules/ppcp-api-client/src/Factory/CaptureFactory.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture; use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatusDetails; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; /** * Class CaptureFactory @@ -74,13 +75,18 @@ class CaptureFactory { $this->fraud_processor_response_factory->from_paypal_response( $data->processor_response ) : null; + $amount = $this->amount_factory->from_paypal_response( $data->amount ); + if ( null === $amount ) { + throw new RuntimeException( __( 'Invalid capture amount data.', 'woocommerce-paypal-payments' ) ); + } + return new Capture( (string) $data->id, new CaptureStatus( (string) $data->status, $reason ? new CaptureStatusDetails( $reason ) : null ), - $this->amount_factory->from_paypal_response( $data->amount ), + $amount, (bool) $data->final_capture, (string) $data->seller_protection->status, (string) $data->invoice_id, diff --git a/modules/ppcp-api-client/src/Factory/RefundFactory.php b/modules/ppcp-api-client/src/Factory/RefundFactory.php index 63530feaf..0ee41f29e 100644 --- a/modules/ppcp-api-client/src/Factory/RefundFactory.php +++ b/modules/ppcp-api-client/src/Factory/RefundFactory.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatusDetails; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; /** * Class RefundFactory @@ -73,13 +74,18 @@ class RefundFactory { $this->refund_payer_factory->from_paypal_response( $data->payer ) : null; + $amount = $this->amount_factory->from_paypal_response( $data->amount ); + if ( null === $amount ) { + throw new RuntimeException( __( 'Invalid refund amount data.', 'woocommerce-paypal-payments' ) ); + } + return new Refund( (string) $data->id, new RefundStatus( (string) $data->status, $reason ? new RefundStatusDetails( $reason ) : null ), - $this->amount_factory->from_paypal_response( $data->amount ), + $amount, (string) ( $data->invoice_id ?? '' ), (string) ( $data->custom_id ?? '' ), $seller_payable_breakdown, From b8c3bd7151dd8fd4a2836322248d20e5e12ff273 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Jul 2025 13:59:56 +0200 Subject: [PATCH 54/65] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fix=20PHPCS=20err?= =?UTF-8?q?ors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Factory/CaptureFactory.php | 1 + modules/ppcp-axo/src/Gateway/AxoGateway.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/src/Factory/CaptureFactory.php b/modules/ppcp-api-client/src/Factory/CaptureFactory.php index 9396ace93..e7c5be7d5 100644 --- a/modules/ppcp-api-client/src/Factory/CaptureFactory.php +++ b/modules/ppcp-api-client/src/Factory/CaptureFactory.php @@ -64,6 +64,7 @@ class CaptureFactory { * @param \stdClass $data The PayPal response. * * @return Capture + * @throws RuntimeException When capture amount data is invalid. */ public function from_paypal_response( \stdClass $data ) : Capture { $reason = $data->status_details->reason ?? null; diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index 9042fec30..54418f19b 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -283,7 +283,7 @@ class AxoGateway extends WC_Payment_Gateway { $redirect_url = add_query_arg( 'redirect_uri', - urlencode( $return_url ), + rawurlencode( $return_url ), $payer_action ); From 8fee8c330f9353d3de0544e2f4f3cd8cc236791a Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Jul 2025 14:17:29 +0200 Subject: [PATCH 55/65] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Fix=20PHPCS=20err?= =?UTF-8?q?ors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/src/Factory/RefundFactory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-api-client/src/Factory/RefundFactory.php b/modules/ppcp-api-client/src/Factory/RefundFactory.php index 0ee41f29e..6911b490a 100644 --- a/modules/ppcp-api-client/src/Factory/RefundFactory.php +++ b/modules/ppcp-api-client/src/Factory/RefundFactory.php @@ -63,6 +63,7 @@ class RefundFactory { * @param \stdClass $data The PayPal response. * * @return Refund + * @throws RuntimeException When refund amount data is invalid. */ public function from_paypal_response( \stdClass $data ) : Refund { $reason = $data->status_details->reason ?? null; From 9d16c2d68027d570211c33ca970a46f895278550 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 17:47:54 +0400 Subject: [PATCH 56/65] fix: replace incorrect typeof usage with proper undefined check --- modules/ppcp-settings/resources/js/switchSettingsUi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/switchSettingsUi.js b/modules/ppcp-settings/resources/js/switchSettingsUi.js index d7d77d4dd..ef69f8a3a 100644 --- a/modules/ppcp-settings/resources/js/switchSettingsUi.js +++ b/modules/ppcp-settings/resources/js/switchSettingsUi.js @@ -5,7 +5,7 @@ document.addEventListener( 'DOMContentLoaded', () => { ); const link = document.querySelector( 'a.settings-switch-ui' ); - if ( ! typeof config || ( ! button && ! link ) ) { + if ( typeof config === 'undefined' || ( ! button && ! link ) ) { return; } From 9a1ad3867e59b6ad279cba2dead8f34c2e1dc661 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 17:54:57 +0400 Subject: [PATCH 57/65] Make sure the `$disable_funding` is an `array` --- .../src/Service/Migration/PaymentSettingsMigration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php index d6ec29706..7c53e281f 100644 --- a/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php +++ b/modules/ppcp-settings/src/Service/Migration/PaymentSettingsMigration.php @@ -47,7 +47,7 @@ class PaymentSettingsMigration implements SettingsMigrationInterface { $allow_local_apm_gateways = $this->settings->has( 'allow_local_apm_gateways' ) && $this->settings->get( 'allow_local_apm_gateways' ); if ( $this->settings->has( 'disable_funding' ) ) { - $disable_funding = $this->settings->get( 'disable_funding' ); + $disable_funding = (array) $this->settings->get( 'disable_funding' ); if ( ! in_array( 'venmo', $disable_funding, true ) ) { $this->payment_settings->toggle_method_state( 'venmo', true ); } From bc4bd9102f8f99db2d757f9788c82ab8ed02334a Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 21:21:57 +0400 Subject: [PATCH 58/65] Cache the reference transaction status value --- .../src/Helper/ReferenceTransactionStatus.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php index 71fa557b4..405f4f58f 100644 --- a/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php +++ b/modules/ppcp-api-client/src/Helper/ReferenceTransactionStatus.php @@ -19,10 +19,15 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; */ class ReferenceTransactionStatus { - protected PartnersEndpoint $partners_endpoint; + public const CACHE_KEY = 'ppcp_reference_transaction_enabled'; - public function __construct( PartnersEndpoint $partners_endpoint ) { + + protected PartnersEndpoint $partners_endpoint; + protected Cache $cache; + + public function __construct( PartnersEndpoint $partners_endpoint, Cache $cache ) { $this->partners_endpoint = $partners_endpoint; + $this->cache = $cache; } /** @@ -34,19 +39,26 @@ class ReferenceTransactionStatus { * @return bool True if reference transactions are enabled, false otherwise. */ public function reference_transaction_enabled(): bool { + if ( $this->cache->has( self::CACHE_KEY ) ) { + return (bool) $this->cache->get( self::CACHE_KEY ); + } + try { foreach ( $this->partners_endpoint->seller_status()->capabilities() as $capability ) { if ( $capability->name() === 'PAYPAL_WALLET_VAULTING_ADVANCED' && $capability->status() === 'ACTIVE' ) { + $this->cache->set( self::CACHE_KEY, true, MONTH_IN_SECONDS ); return true; } } } catch ( RuntimeException $exception ) { + $this->cache->set( self::CACHE_KEY, false, HOUR_IN_SECONDS ); return false; } + $this->cache->set( self::CACHE_KEY, false, HOUR_IN_SECONDS ); return false; } } From 13284f2a4c844eb7d78e55d075810560f0df267c Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 21:23:09 +0400 Subject: [PATCH 59/65] Add caching service for reference transaction status --- modules/ppcp-api-client/services.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index ca6faf11e..fc0736fe3 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -275,7 +275,8 @@ return array( ); }, 'api.reference-transaction-status' => static fn ( ContainerInterface $container ): ReferenceTransactionStatus => new ReferenceTransactionStatus( - $container->get( 'api.endpoint.partners' ) + $container->get( 'api.endpoint.partners' ), + $container->get( 'api.reference-transaction-status-cache' ) ), 'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts { return new CatalogProducts( @@ -871,6 +872,9 @@ return array( 'api.user-id-token-cache' => static function( ContainerInterface $container ): Cache { return new Cache( 'ppcp-id-token-cache' ); }, + 'api.reference-transaction-status-cache' => static function( ContainerInterface $container ): Cache { + return new Cache( 'ppcp-reference-transaction-status-cache' ); + }, 'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken { return new UserIdToken( $container->get( 'api.host' ), From 45d9ca6a4fc67a6fa8e1aa1b387028bfb81db5ca Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 21:26:18 +0400 Subject: [PATCH 60/65] Add clear cache logic for reference transaction status --- modules/ppcp-wc-gateway/services.php | 3 +- .../src/Settings/SettingsListener.php | 46 ++++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 0f1021996..cf8fe7f3b 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -549,7 +549,8 @@ return array( $container->get( 'api.partner_merchant_id-sandbox' ), $container->get( 'api.reference-transaction-status' ), $container->get( 'woocommerce.logger.woocommerce' ), - new Cache( 'ppcp-client-credentials-cache' ) + new Cache( 'ppcp-client-credentials-cache' ), + $container->get( 'api.reference-transaction-status-cache' ) ); }, 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index c20e920e6..0778fa4b8 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -167,6 +167,8 @@ class SettingsListener { */ private $client_credentials_cache; + protected Cache $reference_transaction_status_cache; + /** * SettingsListener constructor. * @@ -187,6 +189,7 @@ class SettingsListener { * @param ReferenceTransactionStatus $reference_transaction_status * @param ?LoggerInterface $logger The logger. * @param Cache $client_credentials_cache The client credentials cache. + * @param Cache $reference_transaction_status_cache The client credentials cache. */ public function __construct( Settings $settings, @@ -205,28 +208,30 @@ class SettingsListener { string $partner_merchant_id_sandbox, ReferenceTransactionStatus $reference_transaction_status, LoggerInterface $logger = null, - Cache $client_credentials_cache + Cache $client_credentials_cache, + Cache $reference_transaction_status_cache ) { // This is a legacy settings class, it's correctly relying on the `Status` class. - $this->settings = $settings; - $this->setting_fields = $setting_fields; - $this->webhook_registrar = $webhook_registrar; - $this->cache = $cache; - $this->state = $state; - $this->bearer = $bearer; - $this->page_id = $page_id; - $this->signup_link_cache = $signup_link_cache; - $this->signup_link_ids = $signup_link_ids; - $this->pui_status_cache = $pui_status_cache; - $this->dcc_status_cache = $dcc_status_cache; - $this->redirector = $redirector; - $this->partner_merchant_id_production = $partner_merchant_id_production; - $this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox; - $this->reference_transaction_status = $reference_transaction_status; - $this->logger = $logger ?: new NullLogger(); - $this->client_credentials_cache = $client_credentials_cache; + $this->settings = $settings; + $this->setting_fields = $setting_fields; + $this->webhook_registrar = $webhook_registrar; + $this->cache = $cache; + $this->state = $state; + $this->bearer = $bearer; + $this->page_id = $page_id; + $this->signup_link_cache = $signup_link_cache; + $this->signup_link_ids = $signup_link_ids; + $this->pui_status_cache = $pui_status_cache; + $this->dcc_status_cache = $dcc_status_cache; + $this->redirector = $redirector; + $this->partner_merchant_id_production = $partner_merchant_id_production; + $this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox; + $this->reference_transaction_status = $reference_transaction_status; + $this->logger = $logger ?: new NullLogger(); + $this->client_credentials_cache = $client_credentials_cache; + $this->reference_transaction_status_cache = $reference_transaction_status_cache; } /** @@ -523,9 +528,8 @@ class SettingsListener { */ do_action( 'woocommerce_paypal_payments_on_listening_request' ); - $ppcp_reference_transaction_enabled = get_transient( 'ppcp_reference_transaction_enabled' ) ?? ''; - if ( $ppcp_reference_transaction_enabled ) { - delete_transient( 'ppcp_reference_transaction_enabled' ); + if ( $this->reference_transaction_status_cache->has( ReferenceTransactionStatus::CACHE_KEY ) ) { + $this->reference_transaction_status_cache->delete( ReferenceTransactionStatus::CACHE_KEY ); } $redirect_url = false; From 569dc811ff97acc9fda5baabd1b3bcaf6e14f2f5 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 21:26:39 +0400 Subject: [PATCH 61/65] Add clear cache logic for reference transaction status --- modules/ppcp-wc-gateway/src/WCGatewayModule.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 5d884d0fb..cc1db0a19 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -489,8 +489,12 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul $pui_product_status->clear( $settings ); } + $reference_transaction_status_cache = $c->get( 'api.reference-transaction-status-cache' ); + assert( $reference_transaction_status_cache instanceof Cache ); // Clear Reference Transaction status. - delete_transient( 'ppcp_reference_transaction_enabled' ); + if ( $reference_transaction_status_cache->has( ReferenceTransactionStatus::CACHE_KEY ) ) { + $reference_transaction_status_cache->delete( ReferenceTransactionStatus::CACHE_KEY ); + } } ); From 801017787baf9325947afa5467829e00d7254d9b Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 2 Jul 2025 21:34:25 +0400 Subject: [PATCH 62/65] Fix the tests --- .../WcGateway/Settings/SettingsListenerTest.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index c29fb3004..a28e445bb 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -43,10 +43,11 @@ class SettingsListenerTest extends ModularTestCase $signup_link_ids = array(); $pui_status_cache = Mockery::mock(Cache::class); $dcc_status_cache = Mockery::mock(Cache::class); - $billing_agreement_endpoint = Mockery::mock(ReferenceTransactionStatus::class); + $reference_transaction_status = Mockery::mock(ReferenceTransactionStatus::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class); $logger = Mockery::mock(LoggerInterface::class); $client_credentials_cache = Mockery::mock(Cache::class); + $reference_transaction_status_cache = Mockery::mock(Cache::class); $testee = new SettingsListener( $settings, @@ -63,9 +64,10 @@ class SettingsListenerTest extends ModularTestCase new RedirectorStub(), '', '', - $billing_agreement_endpoint, + $reference_transaction_status, $logger, - $client_credentials_cache + $client_credentials_cache, + $reference_transaction_status_cache ); $_GET['section'] = PayPalGateway::ID; @@ -99,6 +101,8 @@ class SettingsListenerTest extends ModularTestCase ->andReturn(false); $dcc_status_cache->shouldReceive('has') ->andReturn(false); + $reference_transaction_status_cache->shouldReceive('has') + ->andReturn(false); $client_credentials_cache->shouldReceive('has')->andReturn(true); $client_credentials_cache->shouldReceive('delete'); From 59503b29b3f56a56ec2820e566b899da29574baf Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 3 Jul 2025 00:16:42 +0200 Subject: [PATCH 63/65] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20OrderFact?= =?UTF-8?q?ory=20to=20consolidate=203DS=20and=20regular=20response=20handl?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/OrderEndpoint.php | 7 +--- .../src/Factory/OrderFactory.php | 33 ++----------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 768bce393..62b24803a 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -265,12 +265,7 @@ class OrderEndpoint { throw $error; } - // Check if this is a PAYER_ACTION_REQUIRED response (3DS). - $is_3ds_response = isset( $json->status ) && $json->status === 'PAYER_ACTION_REQUIRED'; - - $order = $is_3ds_response - ? $this->order_factory->from_paypal_response_with_3ds( $json ) - : $this->order_factory->from_paypal_response( $json ); + $order = $this->order_factory->from_paypal_response( $json ); do_action( 'woocommerce_paypal_payments_paypal_order_created', $order ); diff --git a/modules/ppcp-api-client/src/Factory/OrderFactory.php b/modules/ppcp-api-client/src/Factory/OrderFactory.php index 0f994f7ba..8846216ce 100644 --- a/modules/ppcp-api-client/src/Factory/OrderFactory.php +++ b/modules/ppcp-api-client/src/Factory/OrderFactory.php @@ -68,7 +68,8 @@ class OrderFactory { $order->payer(), $order->intent(), $order->create_time(), - $order->update_time() + $order->update_time(), + $order->links() ); } @@ -83,36 +84,6 @@ class OrderFactory { public function from_paypal_response( \stdClass $order_data ): Order { $this->validate_order_id( $order_data ); - $purchase_units = $this->create_purchase_units( $order_data ); - $status = $this->create_order_status( $order_data ); - $intent = $this->get_intent( $order_data ); - $timestamps = $this->create_timestamps( $order_data ); - $payer = $this->create_payer( $order_data ); - $payment_source = $this->create_payment_source( $order_data ); - - return new Order( - $order_data->id, - $purchase_units, - $status, - $payment_source, - $payer, - $intent, - $timestamps['create_time'], - $timestamps['update_time'] - ); - } - - /** - * Returns an Order object based off a PayPal 3DS Response. - * - * @param \stdClass $order_data The JSON object. - * - * @return Order - * @throws RuntimeException When JSON object is malformed. - */ - public function from_paypal_response_with_3ds( \stdClass $order_data ): Order { - $this->validate_order_id( $order_data ); - $purchase_units = $this->create_purchase_units( $order_data ); $status = $this->create_order_status( $order_data ); $intent = $this->get_intent( $order_data ); From c54f82ea94b12957fa7cec9af067ad0ca18edd7e Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 3 Jul 2025 10:16:58 +0200 Subject: [PATCH 64/65] =?UTF-8?q?=F0=9F=A7=AA=20Update=20OrderFactoryTest?= =?UTF-8?q?=20to=20support=20consolidated=20factory=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiClient/Factory/OrderFactoryTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php index 82b0c7d1c..9c92f9da4 100644 --- a/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php @@ -28,6 +28,7 @@ class OrderFactoryTest extends TestCase $order->expects('create_time')->andReturn($createTime); $order->expects('update_time')->andReturn($updateTime); $order->expects('payment_source')->andReturnNull(); + $order->expects('links')->andReturnNull(); $wcOrder = Mockery::mock(\WC_Order::class); $purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class); $purchaseUnit = Mockery::mock(PurchaseUnit::class); @@ -89,6 +90,11 @@ class OrderFactoryTest extends TestCase } else { $this->assertEquals($orderData->update_time, $order->update_time()->format(\DateTime::ISO8601)); } + if ( isset($orderData->links) ) { + $this->assertEquals($orderData->links, $order->links()); + } else { + $this->assertNull($order->links()); + } } public function dataForTestFromPayPalResponseTest() : array @@ -135,6 +141,20 @@ class OrderFactoryTest extends TestCase 'update_time' => '2005-09-15T15:52:01+0000', ], ], + 'with_links' => [ + (object) [ + 'id' => 'id', + 'purchase_units' => [new \stdClass(), new \stdClass()], + 'status' => OrderStatus::PAYER_ACTION_REQUIRED, + 'intent' => 'CAPTURE', + 'create_time' => '2005-08-15T15:52:01+0000', + 'update_time' => '2005-09-15T15:52:01+0000', + 'payer' => new \stdClass(), + 'links' => [ + (object) ['rel' => 'payer-action', 'href' => 'https://example.com/3ds'] + ], + ], + ], ]; } From cdc3dc722cc7eccf1e361c7ddf6b5e705a4e58e0 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 7 Jul 2025 01:58:35 +0200 Subject: [PATCH 65/65] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=203DS=20logic?= =?UTF-8?q?=20from=20global=20filter=20to=20AxoGateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/services.php | 4 +- modules/ppcp-axo/src/AxoModule.php | 56 ------------ modules/ppcp-axo/src/Gateway/AxoGateway.php | 98 ++++++++++++++++++--- 3 files changed, 88 insertions(+), 70 deletions(-) diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index a31587f15..2d35fd136 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -99,7 +99,9 @@ return array( $container->get( 'api.factory.shipping-preference' ), $container->get( 'wcgateway.transaction-url-provider' ), $container->get( 'settings.environment' ), - $container->get( 'woocommerce.logger.woocommerce' ) + $container->get( 'woocommerce.logger.woocommerce' ), + $container->get( 'wcgateway.builder.experience-context' ), + $container->get( 'settings.data.settings' ) ); }, diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 1ca53222a..1b9b6f59a 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -13,14 +13,12 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; -use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -369,60 +367,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); - // Trigger 3D Secure verification. - add_filter( - 'ppcp_create_order_request_body_data', - function( array $data, string $payment_method ) use ( $c ): array { - if ( ! $c->get( 'axo.uk.enabled' ) ) { - return $data; - } - - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); - - if ( $payment_method !== AxoGateway::ID ) { - return $data; - } - - if ( ! isset( $data['payment_source'] ) || ! isset( $data['payment_source']['card'] ) || ! is_object( $data['payment_source']['card'] ) ) { - return $data; - } - - $settings_model = $c->get( 'settings.data.settings' ); - assert( $settings_model instanceof SettingsModel ); - - $three_d_secure = $settings_model->get_three_d_secure_enum(); - $card = $data['payment_source']['card']; - - if ( - $three_d_secure === 'SCA_ALWAYS' - || $three_d_secure === 'SCA_WHEN_REQUIRED' - ) { - $card->attributes = array( - 'verification' => array( - 'method' => $three_d_secure, - ), - ); - - $experience_context_builder = $c->get( 'wcgateway.builder.experience-context' ); - assert( $experience_context_builder instanceof ExperienceContextBuilder ); - - $data['experience_context'] = $experience_context_builder - ->with_endpoint_return_urls() - ->with_current_brand_name() - ->with_current_locale() - ->build()->to_array(); - - $data['transaction_context'] = array( - 'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ), - ); - } - - return $data; - }, - 10, - 2 - ); return true; } diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php index 54418f19b..445f41380 100644 --- a/modules/ppcp-axo/src/Gateway/AxoGateway.php +++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php @@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; @@ -32,6 +33,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait; use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration; +use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use DomainException; /** @@ -133,6 +135,20 @@ class AxoGateway extends WC_Payment_Gateway { */ protected $session_handler; + /** + * The experience context builder. + * + * @var ExperienceContextBuilder + */ + protected $experience_context_builder; + + /** + * The settings model. + * + * @var SettingsModel + */ + protected $settings_model; + /** * AXOGateway constructor. * @@ -149,6 +165,8 @@ class AxoGateway extends WC_Payment_Gateway { * @param TransactionUrlProvider $transaction_url_provider The transaction url provider. * @param Environment $environment The environment. * @param LoggerInterface $logger The logger. + * @param ExperienceContextBuilder $experience_context_builder The experience context builder. + * @param SettingsModel $settings_model The settings model. */ public function __construct( SettingsRenderer $settings_renderer, @@ -163,17 +181,21 @@ class AxoGateway extends WC_Payment_Gateway { ShippingPreferenceFactory $shipping_preference_factory, TransactionUrlProvider $transaction_url_provider, Environment $environment, - LoggerInterface $logger + LoggerInterface $logger, + ExperienceContextBuilder $experience_context_builder, + SettingsModel $settings_model ) { $this->id = self::ID; - $this->settings_renderer = $settings_renderer; - $this->ppcp_settings = $ppcp_settings; - $this->dcc_configuration = $dcc_configuration; - $this->wcgateway_module_url = $wcgateway_module_url; - $this->session_handler = $session_handler; - $this->order_processor = $order_processor; - $this->card_icons = $card_icons; + $this->settings_renderer = $settings_renderer; + $this->ppcp_settings = $ppcp_settings; + $this->dcc_configuration = $dcc_configuration; + $this->wcgateway_module_url = $wcgateway_module_url; + $this->session_handler = $session_handler; + $this->order_processor = $order_processor; + $this->card_icons = $card_icons; + $this->experience_context_builder = $experience_context_builder; + $this->settings_model = $settings_model; $this->method_title = __( 'Fastlane Debit & Credit Cards', 'woocommerce-paypal-payments' ); $this->method_description = __( 'Fastlane accelerates the checkout experience for guest shoppers and autofills their details so they can pay in seconds. When enabled, Fastlane is presented as the default payment method for guests.', 'woocommerce-paypal-payments' ); @@ -244,7 +266,7 @@ class AxoGateway extends WC_Payment_Gateway { // phpcs:ignore WordPress.Security.NonceVerification.Missing $axo_nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) ); - // phpcs:ignore WordPress.Security.NonceVerification.Missing + // phpcs:ignore WordPress.Security.NonceVerification.Recommended $token_param = wc_clean( wp_unslash( $_GET['token'] ?? '' ) ); if ( empty( $axo_nonce ) && ! empty( $token_param ) ) { @@ -434,9 +456,8 @@ class AxoGateway extends WC_Payment_Gateway { 'checkout' ); - $payment_source_properties = (object) array( - 'single_use_token' => $payment_token, - ); + // Build payment source with 3DS verification if needed. + $payment_source_properties = $this->build_payment_source_properties( $payment_token ); $payment_source = new PaymentSource( 'card', @@ -448,12 +469,63 @@ class AxoGateway extends WC_Payment_Gateway { $shipping_preference, null, self::ID, - array(), + $this->build_order_data(), $payment_source, $wc_order ); } + /** + * Build payment source properties. + * + * @param string $payment_token The payment token. + * @return object The payment source properties. + */ + protected function build_payment_source_properties( string $payment_token ): object { + $properties = array( + 'single_use_token' => $payment_token, + ); + + $three_d_secure = $this->settings_model->get_three_d_secure_enum(); + + if ( 'SCA_ALWAYS' === $three_d_secure || 'SCA_WHEN_REQUIRED' === $three_d_secure ) { + $properties['attributes'] = array( + 'verification' => array( + 'method' => $three_d_secure, + ), + ); + } + + return (object) $properties; + } + + /** + * Build additional order data for experience context and 3DS verification. + * + * @return array The order data. + */ + protected function build_order_data(): array { + $data = array(); + + $experience_context = $this->experience_context_builder + ->with_endpoint_return_urls() + ->with_current_brand_name() + ->with_current_locale() + ->build(); + + $data['experience_context'] = $experience_context->to_array(); + + $three_d_secure = $this->settings_model->get_three_d_secure_enum(); + + if ( $three_d_secure === 'SCA_ALWAYS' || $three_d_secure === 'SCA_WHEN_REQUIRED' ) { + $data['transaction_context'] = array( + 'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ), + ); + } + + return $data; + } + /** * Returns the icons of the gateway. *