From c3040eb3475f304f095ae87115eff0e8f49779b5 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 3 Feb 2025 12:56:28 +0100 Subject: [PATCH 01/37] Add is new merchant flag option --- modules.php | 4 ++-- woocommerce-paypal-payments.php | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/modules.php b/modules.php index 21a4e645e..c4886bbc3 100644 --- a/modules.php +++ b/modules.php @@ -93,8 +93,8 @@ return function ( string $root_dir ): iterable { if ( apply_filters( 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', - getenv( 'PCP_SETTINGS_ENABLED' ) === '1' - ) ) { + getenv( 'PCP_SETTINGS_ENABLED' ) !== '0' + ) && get_option('woocommerce-ppcp-is-new-merchant') === '1') { $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); } diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index d9e1a2277..9d29f92c6 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -229,4 +229,11 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); return class_exists( 'woocommerce' ); } + // Set new merchant flag on plugin install. + add_action( 'woocommerce_paypal_payments_gateway_migrate', function() { + $legacy_settings = get_option(Settings::KEY); + if(! $legacy_settings) { + update_option('woocommerce-ppcp-is-new-merchant', true); + } + }); } )(); From 62bf7b76581a2107ce362f3d5b8453e0fc469802 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 18 Feb 2025 18:36:38 +0100 Subject: [PATCH 02/37] =?UTF-8?q?=F0=9F=8E=A8=20Address=20code=20style=20p?= =?UTF-8?q?roblems=20(phpcs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules.php b/modules.php index c4886bbc3..a63e9c38a 100644 --- a/modules.php +++ b/modules.php @@ -91,10 +91,13 @@ 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 = '0' !== getenv( 'PCP_SETTINGS_ENABLED' ); + if ( apply_filters( 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', - getenv( 'PCP_SETTINGS_ENABLED' ) !== '0' - ) && get_option('woocommerce-ppcp-is-new-merchant') === '1') { + $show_new_ux || $preview_new_ux + ) ) { $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); } From cb4221ed14081598c02875c03b0f856da9d72998 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 18 Feb 2025 18:55:12 +0100 Subject: [PATCH 03/37] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20the=20UX?= =?UTF-8?q?=20decision=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- woocommerce-paypal-payments.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 9d29f92c6..37a075c87 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -229,11 +229,21 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); return class_exists( 'woocommerce' ); } - // Set new merchant flag on plugin install. - add_action( 'woocommerce_paypal_payments_gateway_migrate', function() { - $legacy_settings = get_option(Settings::KEY); - if(! $legacy_settings) { - update_option('woocommerce-ppcp-is-new-merchant', true); + add_action( + 'woocommerce_paypal_payments_gateway_migrate', + /** + * Set new merchant flag on plugin install. + * + * When installing the plugin for the first time, we direct the user to + * the new UI without a data migration, and fully hide the legacy UI. + * + * @param string|false $version String with previous installed plugin version. + * Boolean false on first installation on a new site. + */ + static function ( $version ) { + if ( ! $version ) { + update_option( 'woocommerce-ppcp-is-new-merchant', '1' ); + } } - }); + ); } )(); From af9c8eca3dbf7b31b0ad974421c4595bf9c7e74d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 18 Feb 2025 19:09:24 +0100 Subject: [PATCH 04/37] =?UTF-8?q?=F0=9F=90=9B=20Fix=20incorrect=20result?= =?UTF-8?q?=20of=20the=20opt-out=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This opt-out is only relevant for existing merchants. New merchants can skip this check and always see the new UI. --- modules/ppcp-settings/src/SettingsModule.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 1e6c1f818..0447d52bc 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -51,9 +51,19 @@ class SettingsModule implements ServiceModule, ExecutableModule { * Returns whether the old settings UI should be loaded. */ public static function should_use_the_old_ui() : bool { + // New merchants should never see the legacy UI. + $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' ); + + if ( $show_new_ux ) { + return false; + } + + // Existing merchants can opt-in to see the new 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', - get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === 'yes' + $opt_out_choice ); } From 720b6cf309172ba455fcd69f3b12640f9694304f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 24 Feb 2025 18:12:46 +0100 Subject: [PATCH 05/37] =?UTF-8?q?=F0=9F=93=9D=20Document=20authentication?= =?UTF-8?q?=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/authentication-flows.md | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 modules/ppcp-settings/docs/authentication-flows.md diff --git a/modules/ppcp-settings/docs/authentication-flows.md b/modules/ppcp-settings/docs/authentication-flows.md new file mode 100644 index 000000000..bf666726f --- /dev/null +++ b/modules/ppcp-settings/docs/authentication-flows.md @@ -0,0 +1,120 @@ +# Authentication Flows + +The settings UI offers two distinct authentication methods: + +- OAuth +- Direct API + +## OAuth + +This is the usual authentication UI for most users. It opens a "PayPal popup" with a login mask. +The authentication flow consists of **three steps**: + +- Generate a referral URL with a special token +- Translate a one-time OAuth secret into permanent API credentials +- Complete authentication by confirming the token from step 1 + +**Usage:** + +1. Available on the first onboarding page (for sandbox login), or on the last page of the onboarding wizard. +2. Authentication is initiated by clicking a "Connect" button which opens a popup with a PayPal login mask + - Sometimes the login opens in a new tab, mainly on Mac when using the browser in full-screen mode +3. After completing the login, the final page shows a "Return to your store" button; clicking that button closes the popup/tab and completes the authentication process + +**More details on what happens:** + +```mermaid +sequenceDiagram + autonumber + participant R as React API + participant S as PHP Server + + R->>S: Request partner referral URL + Note over S: Generate and store a one-time token + create participant W as WooCommerce API + S->>W: Request referral URL + destroy W + W->>S: Generate the full partner referral URL + S->>R: Return referral URL + create participant P as PayPal Popup + R->>P: Open PayPal popup, which was generated by WooCommerce APi + Note over P: Complete login inside Popup + P->>R: Call JS function with OAuth ID and shared secret + R->>S: Send OAuth data to REST endpoint + create participant PP as PayPal API + S->>PP: Request permanent credentials + PP->>S: Translate one-time secret to permanent credentials + destroy P + P->>R: Redirect browser tab with unique token + Note over R: App unmounts during redirect + + Note over S: During page load + Note over S: Verify token and finalize authentication + S->>PP: Request merchant details + destroy PP + PP->>S: Return merchant details (e.g. country) + Note over S: Render the settings page with React app + S->>R: Boot react app in "settings mode" +``` + +1. Authentication starts _before_ the "Connect" button is rendered, as we generate a one-time partner referral URL + - See `ConnectionUrlGenerator::generate()` + - This referral URL configures PayPal: Which items render inside the Popup? What is the "return + URL" for the final step? Is it a sandbox or live login? +2. _...The merchant completes the login or account creation flow inside the popup..._ +3. During page-load of the final confirmation page inside the popup: PayPal directly calls a JS function on the WooCommerce settings page, i.e. the popup communicates with the open WooCommerce tab. This JS function sends an oauth ID and shared secret (OTP) to a REST endpoint + - See `AuthenticatoinRestEndpoint::connect_oauth()` + - See `AuthenticationManager::authenticate_via_oauth()` → translates the one-time shared secret + into a permanent client secret + - At this stage, the authentication is _incomplete_, as some details are only provided by the + final step +4. When clicking the "Return to store" button, the popup closes and the WooCommerce settings page "reloads"; it's actually a _redirect_ which is initiated by PayPal and receives a unique token (which was generated by the `ConnectionUrlGenerator`) that is required to complete authentication. + - See `ConnectionListener::process()` + - See `AuthenticationManager::finish_oauth_authentication()` + - This listener runs on every wp-admin page load and bails if the required token is not present +5. After the final page reload, the React app directly enters "Settings mode" + +## Direct API + +This method is only available for business accounts, as it requires the merchant to create a PayPal REST app that's linked to their account. + +
+Setup the PayPal REST app + +1. Visit https://developer.paypal.com/ +2. In section "Apps & Credentials" click "Create App" +3. After the app is ready, it displays the `Client ID` and `Secret Key` values + +
+ +**Usage:** + +1. Available on the first onboarding screen, via the "See advanced options" form at the bottom of the page +2. Activate the "Manual Connection" toggle; then enter the `Client ID` and `Secret Key` and hit Enter + +**What happens:** + +```mermaid +sequenceDiagram + participant R as React + participant S as Server + participant P as PayPal API + + R->>S: Send credentials to REST endpoint + S->>P: Authenticate via Direct API + P->>S: Return authentication result + S->>P: Request merchant details + P->>S: Return merchant details (e.g. country) + + Note over S: Process authentication result + S->>R: Return authentication status + Note over R: Update UI to authenticated state
(no page reload) + +``` + +1. Client ID and Secret are sent to a REST endpoint of the plugin. The authentication happens on server-side. + - See `AuthenticatoinRestEndpoint::connect_direct()` + - See `AuthenticationManager::authenticate_via_direct_api()` +2. After authentication is completed, the merchant account is prepared on server side and a confirmation is returned to the React app. + - See `AuthenticationManager::update_connection_details()` → condition `is_merchant_connected()` +3. The React app directly switches to the "Settings mode" without a page reload. From 75f1ec34095bc22ef821d71f459c9321d570e12e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 24 Feb 2025 18:13:17 +0100 Subject: [PATCH 06/37] =?UTF-8?q?=F0=9F=8E=A8=20Code=20style=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DTO/MerchantConnectionDTO.php | 16 ++++++++-------- .../src/Service/AuthenticationManager.php | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php b/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php index ed9098b48..de97e3590 100644 --- a/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php +++ b/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php @@ -70,21 +70,21 @@ class MerchantConnectionDTO { /** * Constructor. * - * @param bool $is_sandbox Whether this connection is a sandbox account. - * @param string $client_id API client ID. - * @param string $client_secret API client secret. - * @param string $merchant_id PayPal's 13-character merchant ID. - * @param string $merchant_email Email address of the merchant account. + * @param bool $is_sandbox Whether this connection is a sandbox account. + * @param string $client_id API client ID. + * @param string $client_secret API client secret. + * @param string $merchant_id PayPal's 13-character merchant ID. + * @param string $merchant_email Email address of the merchant account. * @param string $merchant_country Merchant's country. - * @param string $seller_type Whether the merchant is a business or personal account. + * @param string $seller_type Whether the merchant is a business or personal account. */ public function __construct( bool $is_sandbox, string $client_id, string $client_secret, string $merchant_id, - string $merchant_email, - string $merchant_country, + string $merchant_email = '', + string $merchant_country = '', string $seller_type = SellerTypeEnum::UNKNOWN ) { $this->is_sandbox = $is_sandbox; diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 9e55ee838..41c25da13 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -84,13 +84,13 @@ class AuthenticationManager { /** * Constructor. * - * @param GeneralSettings $common_settings Data model that stores the connection details. - * @param EnvironmentConfig $connection_host API host for direct authentication. - * @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials. - * @param PartnerReferralsData $referrals_data Partner referrals data. - * @param ConnectionState $connection_state Connection state manager. + * @param GeneralSettings $common_settings Data model that stores the connection details. + * @param EnvironmentConfig $connection_host API host for direct authentication. + * @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials. + * @param PartnerReferralsData $referrals_data Partner referrals data. + * @param ConnectionState $connection_state Connection state manager. * @param PartnersEndpoint $partners_endpoint Partners endpoint. - * @param ?LoggerInterface $logger Logging instance. + * @param ?LoggerInterface $logger Logging instance. */ public function __construct( GeneralSettings $common_settings, From 73bad59a26bf7e7d65131390fa58f703cd1b84d5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 24 Feb 2025 18:22:05 +0100 Subject: [PATCH 07/37] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20seller-cou?= =?UTF-8?q?ntry=20detection=20to=20new=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/AuthenticationManager.php | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 41c25da13..dfed9310c 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -188,6 +188,7 @@ class AuthenticationManager { * PayPal account using a client ID and secret. * * Part of the "Direct Connection" (Manual Connection) flow. + * This connection type is only available to business merchants. * * @param bool $use_sandbox Whether to use the sandbox mode. * @param string $client_id The client ID. @@ -208,19 +209,13 @@ class AuthenticationManager { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( PayPalApiException $exception ) { - $seller_status = null; - } - $connection = new MerchantConnectionDTO( $use_sandbox, $client_id, $client_secret, $payee['merchant_id'], $payee['email_address'], - ! is_null( $seller_status ) ? $seller_status->country() : '', + '', SellerTypeEnum::BUSINESS ); @@ -332,13 +327,6 @@ class AuthenticationManager { $connection->seller_type = $seller_type; } - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( PayPalApiException $exception ) { - $seller_status = null; - } - $connection->merchant_country = ! is_null( $seller_status ) ? $seller_status->country() : ''; - $this->update_connection_details( $connection ); } @@ -449,6 +437,36 @@ class AuthenticationManager { ); } + /** + * Fetches additional details about the connected merchant from PayPal + * and stores them in the DB. + * + * This process only works after persisting basic connection details. + * + * @return void + */ + private function enrich_merchant_details() : void { + if ( ! $this->common_settings->is_merchant_connected() ) { + return; + } + + try { + $seller_status = $this->partners_endpoint->seller_status(); + + // Request the merchant details via a PayPal API request. + $connection = $this->common_settings->get_merchant_data(); + + // Enrich the connection details with additional details. + $connection->merchant_country = $seller_status->country(); + + // Persist the changes. + $this->common_settings->set_merchant_data( $connection ); + $this->common_settings->save(); + } catch ( PayPalApiException $exception ) { + $this->logger->warning( 'Could not determine merchant country' ); + } + } + /** * Stores the provided details in the data model. * @@ -470,6 +488,9 @@ class AuthenticationManager { // Update the connection status and set the environment flags. $this->connection_state->connect( $connection->is_sandbox ); + // At this point, we can use the PayPal API to get more details about the seller. + $this->enrich_merchant_details(); + /** * Request to flush caches before authenticating the merchant, to * ensure the new merchant does not use stale data from previous From 65204a500342177b8b29495370305abf3c166fe2 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 24 Feb 2025 18:36:20 +0100 Subject: [PATCH 08/37] =?UTF-8?q?=F0=9F=92=A1=20Add=20documentation=20on?= =?UTF-8?q?=20next=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Service/AuthenticationManager.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index dfed9310c..1637c73a6 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -451,6 +451,7 @@ class AuthenticationManager { } try { + // TODO: this call only reliably works in the _next_ request, because in the current request the PartnersEndpoint instance might be initialized with an empty merchant_id. $seller_status = $this->partners_endpoint->seller_status(); // Request the merchant details via a PayPal API request. @@ -462,8 +463,8 @@ class AuthenticationManager { // Persist the changes. $this->common_settings->set_merchant_data( $connection ); $this->common_settings->save(); - } catch ( PayPalApiException $exception ) { - $this->logger->warning( 'Could not determine merchant country' ); + } catch ( Throwable $exception ) { + $this->logger->warning( 'Could not determine merchant country: ' . $exception->getMessage() ); } } From 79e4f9cdd49ac700f6cdca3dfe9fc22ab06f5c19 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 25 Feb 2025 17:22:59 +0100 Subject: [PATCH 09/37] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20the=20REST=20?= =?UTF-8?q?namespace=20a=20const=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to access the namespace from an external class; using a const makes this easy. The alternative would be passing a service-instance via DI, just to access the namespace --- .../src/Endpoint/AuthenticationRestEndpoint.php | 6 +++--- modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php | 6 +++--- .../ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php | 2 +- modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php | 2 +- .../ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php | 2 +- .../ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php | 4 ++-- .../src/Endpoint/PayLaterMessagingEndpoint.php | 4 ++-- modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php | 4 ++-- .../src/Endpoint/RefreshFeatureStatusEndpoint.php | 2 +- .../src/Endpoint/ResetDismissedTodosEndpoint.php | 2 +- modules/ppcp-settings/src/Endpoint/RestEndpoint.php | 4 +--- modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php | 2 +- modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php | 4 ++-- modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php | 6 +++--- .../ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php | 4 ++-- 15 files changed, 26 insertions(+), 28 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index d6499270b..1915d998f 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -87,7 +87,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/direct', array( 'methods' => WP_REST_Server::EDITABLE, @@ -125,7 +125,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/oauth', array( 'methods' => WP_REST_Server::EDITABLE, @@ -155,7 +155,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/authenticate/disconnect */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/disconnect', array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 8014e3817..bbd29c744 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -118,7 +118,7 @@ class CommonRestEndpoint extends RestEndpoint { * GET /wp-json/wc/v3/wc_paypal/common */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -134,7 +134,7 @@ class CommonRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, @@ -147,7 +147,7 @@ class CommonRestEndpoint extends RestEndpoint { * GET /wp-json/wc/v3/wc_paypal/common/merchant */ register_rest_route( - $this->namespace, + static::NAMESPACE, "/$this->rest_base/merchant", array( 'methods' => WP_REST_Server::READABLE, diff --git a/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php b/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php index 25d734b0a..a7290f136 100644 --- a/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php @@ -62,7 +62,7 @@ class CompleteOnClickEndpoint extends RestEndpoint { */ public function register_routes(): void { register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php index 025d606bd..f5c733e40 100644 --- a/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php @@ -64,7 +64,7 @@ class FeaturesRestEndpoint extends RestEndpoint { public function register_routes(): void { // GET /features - Get features list. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index c2b0e9ff3..b9972349f 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -60,7 +60,7 @@ class LoginLinkRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index b7a5b8023..bd5049333 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -101,7 +101,7 @@ class OnboardingRestEndpoint extends RestEndpoint { * GET /wp-json/wc/v3/wc_paypal/onboarding */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -117,7 +117,7 @@ class OnboardingRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php index 5713ce570..d8a322215 100644 --- a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php @@ -63,7 +63,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint { * GET wc/v3/wc_paypal/pay_later_messaging */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -76,7 +76,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint { * POST wc/v3/wc_paypal/pay_later_messaging */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index fb17a2877..6aa67f5b9 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -111,7 +111,7 @@ class PaymentRestEndpoint extends RestEndpoint { * GET wc/v3/wc_paypal/payment */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -131,7 +131,7 @@ class PaymentRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php index 3b17b84ed..a5be3f207 100644 --- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -87,7 +87,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/refresh-features */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php b/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php index b922127d2..5906836d0 100644 --- a/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php @@ -54,7 +54,7 @@ class ResetDismissedTodosEndpoint extends RestEndpoint { * POST wc/v3/wc_paypal/reset-dismissed-todos */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index e9e9948ab..f0cf1306a 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -18,10 +18,8 @@ use WP_REST_Response; abstract class RestEndpoint extends WC_REST_Controller { /** * Endpoint namespace. - * - * @var string */ - protected $namespace = 'wc/v3/wc_paypal'; + protected const NAMESPACE = 'wc/v3/wc_paypal'; /** * Verify access. diff --git a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php index 9f200ed99..06f639bba 100644 --- a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php @@ -109,7 +109,7 @@ class SettingsRestEndpoint extends RestEndpoint { * POST wc/v3/wc_paypal/settings */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( diff --git a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php index 450549e43..7e4057704 100644 --- a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php @@ -107,7 +107,7 @@ class StylingRestEndpoint extends RestEndpoint { * GET wc/v3/wc_paypal/styling */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -123,7 +123,7 @@ class StylingRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php index b1cd77b49..fa922f2f7 100644 --- a/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php @@ -88,7 +88,7 @@ class TodosRestEndpoint extends RestEndpoint { public function register_routes(): void { // GET/POST /todos - Get todos list and update dismissed todos. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( @@ -106,7 +106,7 @@ class TodosRestEndpoint extends RestEndpoint { // POST /todos/reset - Reset dismissed todos. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/reset', array( 'methods' => WP_REST_Server::EDITABLE, @@ -117,7 +117,7 @@ class TodosRestEndpoint extends RestEndpoint { // POST /todos/complete - Mark todo as completed on click. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/complete', array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php index 81e8f4335..6bf3c6352 100644 --- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -79,7 +79,7 @@ class WebhookSettingsEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/webhooks */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( @@ -100,7 +100,7 @@ class WebhookSettingsEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/webhooks/simulate */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/simulate', array( array( From 6dd0034c0a15a4bc09588bef4667e7f88456442a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 26 Feb 2025 10:35:43 +0100 Subject: [PATCH 10/37] =?UTF-8?q?=F0=9F=9A=A7=20non-working=20version=20of?= =?UTF-8?q?=20internal=20REST=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 11 +++- .../src/Endpoint/CommonRestEndpoint.php | 65 ++++++++++++++++++- .../src/Service/AuthenticationManager.php | 58 ++++++++--------- .../src/Service/InternalRestService.php | 57 ++++++++++++++++ 4 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 modules/ppcp-settings/src/Service/InternalRestService.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index d2dd9df97..edb8b37d6 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -49,6 +49,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; +use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -172,7 +173,10 @@ return array( return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); }, 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { - return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); + return new CommonRestEndpoint( + $container->get( 'settings.data.general' ), + $container->get( 'settings.service.rest-service' ) + ); }, 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { return new PaymentRestEndpoint( @@ -313,10 +317,13 @@ return array( $container->get( 'api.env.endpoint.login-seller' ), $container->get( 'api.repository.partner-referrals-data' ), $container->get( 'settings.connection-state' ), - $container->get( 'api.endpoint.partners' ), + $container->get( 'settings.service.rest-service' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'settings.service.rest-service' => static function ( ContainerInterface $container ) : InternalRestService { + return new InternalRestService(); + }, 'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer { return new DataSanitizer(); }, diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index bbd29c744..82aa06b29 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -13,6 +13,7 @@ use WP_REST_Server; use WP_REST_Response; use WP_REST_Request; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService; /** * REST controller for "common" settings, which are used and modified by @@ -22,6 +23,11 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; * internal data model. */ class CommonRestEndpoint extends RestEndpoint { + /** + * Full REST path to the merchant-details endpoint, relative to the namespace. + */ + protected const SELLER_ACCOUNT_PATH = 'common/seller-account'; + /** * The base path for this REST controller. * @@ -36,6 +42,13 @@ class CommonRestEndpoint extends RestEndpoint { */ protected GeneralSettings $settings; + /** + * Internal REST handler, used to authenticate internal requests. + * + * @var InternalRestService + */ + protected InternalRestService $rest_service; + /** * Field mapping for request to profile transformation. * @@ -104,10 +117,27 @@ class CommonRestEndpoint extends RestEndpoint { /** * Constructor. * - * @param GeneralSettings $settings The settings instance. + * @param GeneralSettings $settings The settings instance. + * @param InternalRestService $rest_service Internal REST handler, for authentication. */ - public function __construct( GeneralSettings $settings ) { - $this->settings = $settings; + public function __construct( GeneralSettings $settings, InternalRestService $rest_service ) { + $this->settings = $settings; + $this->rest_service = $rest_service; + } + + /** + * Returns the path to the "Get Seller Account Details" REST route. + * This is an internal route which is consumed by the plugin itself during onboarding. + * + * @param bool $full_route Whether to return the full endpoint path or just the route name. + * @return string The full path to the REST endpoint. + */ + public static function seller_account_route( bool $full_route = false ) : string { + if ( $full_route ) { + return '/' . static::NAMESPACE . '/' . self::SELLER_ACCOUNT_PATH; + } + + return self::SELLER_ACCOUNT_PATH; } /** @@ -155,6 +185,24 @@ class CommonRestEndpoint extends RestEndpoint { 'permission_callback' => array( $this, 'check_permission' ), ) ); + + /** + * GET /wp-json/wc/v3/wc_paypal/common/seller-account + */ + register_rest_route( + static::NAMESPACE, + self::seller_account_route(), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_seller_account_info' ), + 'permission_callback' => function ( WP_REST_Request $request ) { + $token = $request->get_header( 'X-Internal-Token' ); + $endpoint = self::seller_account_route(); + + return $this->rest_service->verify_token( $token, $endpoint ); + }, + ) + ); } /** @@ -205,6 +253,17 @@ class CommonRestEndpoint extends RestEndpoint { return $this->return_success( $js_data, $extra_data ); } + /** + * Requests details from the PayPal API. + * + * Used during onboarding to enrich the merchant details in the DB. + * + * @return WP_REST_Response Seller details, provided by PayPal's API. + */ + public function get_seller_account_info() : WP_REST_Response { + return $this->return_success( array( 'country' => 'XY' ) ); + } + /** * Appends the "merchant" attribute to the extra_data collection, which * contains details about the merchant's PayPal account, like the merchant ID. diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 1637c73a6..b48a7554a 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -27,6 +27,7 @@ use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; +use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; /** * Class that manages the connection to PayPal. @@ -75,22 +76,22 @@ class AuthenticationManager { private ConnectionState $connection_state; /** - * Partners endpoint. + * Internal REST service, to consume own REST handlers in a separate request. * - * @var PartnersEndpoint + * @var InternalRestService */ - private PartnersEndpoint $partners_endpoint; + private InternalRestService $rest_service; /** * Constructor. * - * @param GeneralSettings $common_settings Data model that stores the connection details. - * @param EnvironmentConfig $connection_host API host for direct authentication. - * @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials. - * @param PartnerReferralsData $referrals_data Partner referrals data. - * @param ConnectionState $connection_state Connection state manager. - * @param PartnersEndpoint $partners_endpoint Partners endpoint. - * @param ?LoggerInterface $logger Logging instance. + * @param GeneralSettings $common_settings Data model that stores the connection details. + * @param EnvironmentConfig $connection_host API host for direct authentication. + * @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials. + * @param PartnerReferralsData $referrals_data Partner referrals data. + * @param ConnectionState $connection_state Connection state manager. + * @param InternalRestService $rest_service Allows calling internal REST endpoints. + * @param ?LoggerInterface $logger Logging instance. */ public function __construct( GeneralSettings $common_settings, @@ -98,16 +99,16 @@ class AuthenticationManager { EnvironmentConfig $login_endpoint, PartnerReferralsData $referrals_data, ConnectionState $connection_state, - PartnersEndpoint $partners_endpoint, + InternalRestService $rest_service, ?LoggerInterface $logger = null ) { - $this->common_settings = $common_settings; - $this->connection_host = $connection_host; - $this->login_endpoint = $login_endpoint; - $this->referrals_data = $referrals_data; - $this->connection_state = $connection_state; - $this->partners_endpoint = $partners_endpoint; - $this->logger = $logger ?: new NullLogger(); + $this->common_settings = $common_settings; + $this->connection_host = $connection_host; + $this->login_endpoint = $login_endpoint; + $this->referrals_data = $referrals_data; + $this->connection_state = $connection_state; + $this->rest_service = $rest_service; + $this->logger = $logger ?: new NullLogger(); } /** @@ -281,17 +282,10 @@ class AuthenticationManager { */ $connection = $this->common_settings->get_merchant_data(); - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( PayPalApiException $exception ) { - $seller_status = null; - } - - $connection->is_sandbox = $use_sandbox; - $connection->client_id = $credentials['client_id']; - $connection->client_secret = $credentials['client_secret']; - $connection->merchant_id = $credentials['merchant_id']; - $connection->merchant_country = ! is_null( $seller_status ) ? $seller_status->country() : ''; + $connection->is_sandbox = $use_sandbox; + $connection->client_id = $credentials['client_id']; + $connection->client_secret = $credentials['client_secret']; + $connection->merchant_id = $credentials['merchant_id']; $this->update_connection_details( $connection ); } @@ -451,14 +445,14 @@ class AuthenticationManager { } try { - // TODO: this call only reliably works in the _next_ request, because in the current request the PartnersEndpoint instance might be initialized with an empty merchant_id. - $seller_status = $this->partners_endpoint->seller_status(); + $endpoint = CommonRestEndpoint::seller_account_route( true ); + $details = $this->rest_service->get_data( $endpoint ); // Request the merchant details via a PayPal API request. $connection = $this->common_settings->get_merchant_data(); // Enrich the connection details with additional details. - $connection->merchant_country = $seller_status->country(); + $connection->merchant_country = $details['country']; // Persist the changes. $this->common_settings->set_merchant_data( $connection ); diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php new file mode 100644 index 000000000..a83e5fdcf --- /dev/null +++ b/modules/ppcp-settings/src/Service/InternalRestService.php @@ -0,0 +1,57 @@ +generate_token( $endpoint ); + $rest_url = rest_url( $endpoint ); + + $response = wp_remote_get( + $rest_url, + array( + 'headers' => array( + 'X-Internal-Token' => $token, + 'Content-Type' => 'application/json', + ), + ) + ); + + if ( is_wp_error( $response ) ) { + return array(); + } + + $body = wp_remote_retrieve_body( $response ); + + try { + $json = json_decode( $body, true, 512, JSON_THROW_ON_ERROR ); + } catch ( Throwable $exception ) { + return array(); + } + + if ( ! $json || empty( $json['success'] ) ) { + return array(); + } + + return $json['data']; + } + + public function verify_token( string $token, string $endpoint ) : bool { + $expected_token = $this->generate_token( $endpoint ); + + return $expected_token === $token; + } + + private function generate_token( string $token_id ) : string { + return base64_encode( $token_id ); + } +} From 109e6517f0d79f0ed6a0a870228310a1a72c83d4 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 26 Feb 2025 15:19:42 +0100 Subject: [PATCH 11/37] Add can use Fastlane flag and pass it to welcome docs component --- .../js/Components/Screens/Onboarding/Steps/StepWelcome.js | 4 ++-- modules/ppcp-settings/resources/js/data/onboarding/reducer.js | 1 + modules/ppcp-settings/services.php | 4 +++- modules/ppcp-settings/src/Data/OnboardingProfile.php | 4 +++- modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php | 3 +++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js index 9d2bdacb2..7215570d8 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js @@ -12,7 +12,7 @@ import AdvancedOptionsForm from '../Components/AdvancedOptionsForm'; const StepWelcome = ( { setStep, currentStep } ) => { const { storeCountry } = CommonHooks.useWooSettings(); - const { canUseCardPayments } = OnboardingHooks.useFlags(); + const { canUseCardPayments, canUseFastlane } = OnboardingHooks.useFlags(); const nonAcdcIcons = [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ]; return ( @@ -54,7 +54,7 @@ const StepWelcome = ( { setStep, currentStep } ) => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 01a4d0422..6a647dbef 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -24,6 +24,7 @@ const defaultTransient = Object.freeze( { canUseCardPayments: false, canUseSubscriptions: false, shouldSkipPaymentMethods: false, + canUseFastlane: false, } ), } ); diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 8055f74fb..0f5e35f51 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -69,13 +69,15 @@ return array( $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' ) ->plugin_is_active(); $should_skip_payment_methods = class_exists( '\WC_Payments' ); + $can_use_fastlane = $container->get( 'axo.eligible' ); return new OnboardingProfile( $can_use_casual_selling, $can_use_vaulting, $can_use_card_payments, $can_use_subscriptions, - $should_skip_payment_methods + $should_skip_payment_methods, + $can_use_fastlane ); }, 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings { diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index aa1b6736b..dbdf8fac5 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -52,7 +52,8 @@ class OnboardingProfile extends AbstractDataModel { bool $can_use_vaulting = false, bool $can_use_card_payments = false, bool $can_use_subscriptions = false, - bool $should_skip_payment_methods = false + bool $should_skip_payment_methods = false, + bool $can_use_fastlane = false ) { parent::__construct(); @@ -61,6 +62,7 @@ class OnboardingProfile extends AbstractDataModel { $this->flags['can_use_card_payments'] = $can_use_card_payments; $this->flags['can_use_subscriptions'] = $can_use_subscriptions; $this->flags['should_skip_payment_methods'] = $should_skip_payment_methods; + $this->flags['can_use_fastlane'] = $can_use_fastlane; } /** diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 2572d9e6e..bef842144 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -83,6 +83,9 @@ class OnboardingRestEndpoint extends RestEndpoint { 'should_skip_payment_methods' => array( 'js_name' => 'shouldSkipPaymentMethods', ), + 'can_use_fastlane' => array( + 'js_name' => 'canUseFastlane', + ), ); /** From 7bf3e83d857dfdd3a4703b5da685fcf948a3a536 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 26 Feb 2025 15:46:18 +0100 Subject: [PATCH 12/37] Add Pay Later flag and pass it to welcome docs component --- .../js/Components/Screens/Onboarding/Steps/StepWelcome.js | 5 +++-- .../ppcp-settings/resources/js/data/onboarding/reducer.js | 1 + modules/ppcp-settings/services.php | 4 +++- modules/ppcp-settings/src/Data/OnboardingProfile.php | 8 ++++++-- .../ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php | 5 ++++- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js index 7215570d8..7d044256c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js @@ -12,7 +12,8 @@ import AdvancedOptionsForm from '../Components/AdvancedOptionsForm'; const StepWelcome = ( { setStep, currentStep } ) => { const { storeCountry } = CommonHooks.useWooSettings(); - const { canUseCardPayments, canUseFastlane } = OnboardingHooks.useFlags(); + const { canUseCardPayments, canUseFastlane, canUsePayLater } = + OnboardingHooks.useFlags(); const nonAcdcIcons = [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ]; return ( @@ -55,7 +56,7 @@ const StepWelcome = ( { setStep, currentStep } ) => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 6a647dbef..c6d729184 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -25,6 +25,7 @@ const defaultTransient = Object.freeze( { canUseSubscriptions: false, shouldSkipPaymentMethods: false, canUseFastlane: false, + canUsePayLater: false, } ), } ); diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 0f5e35f51..ca8b82756 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -70,6 +70,7 @@ return array( ->plugin_is_active(); $should_skip_payment_methods = class_exists( '\WC_Payments' ); $can_use_fastlane = $container->get( 'axo.eligible' ); + $can_use_pay_later = $container->get( 'button.helper.messages-apply' ); return new OnboardingProfile( $can_use_casual_selling, @@ -77,7 +78,8 @@ return array( $can_use_card_payments, $can_use_subscriptions, $should_skip_payment_methods, - $can_use_fastlane + $can_use_fastlane, + $can_use_pay_later->for_country() ); }, 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings { diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index dbdf8fac5..cf2817ede 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -44,6 +44,8 @@ class OnboardingProfile extends AbstractDataModel { * @param bool $can_use_card_payments Whether credit card payments are possible. * @param bool $can_use_subscriptions Whether WC Subscriptions plugin is active. * @param bool $should_skip_payment_methods Whether it should skip payment methods screen. + * @param bool $can_use_fastlane Whether it can use Fastlane or not. + * @param bool $can_use_pay_later Whether it can use PAy Later or not. * * @throws RuntimeException If the OPTION_KEY is not defined in the child class. */ @@ -53,7 +55,8 @@ class OnboardingProfile extends AbstractDataModel { bool $can_use_card_payments = false, bool $can_use_subscriptions = false, bool $should_skip_payment_methods = false, - bool $can_use_fastlane = false + bool $can_use_fastlane = false, + bool $can_use_pay_later = false ) { parent::__construct(); @@ -62,7 +65,8 @@ class OnboardingProfile extends AbstractDataModel { $this->flags['can_use_card_payments'] = $can_use_card_payments; $this->flags['can_use_subscriptions'] = $can_use_subscriptions; $this->flags['should_skip_payment_methods'] = $should_skip_payment_methods; - $this->flags['can_use_fastlane'] = $can_use_fastlane; + $this->flags['can_use_fastlane'] = $can_use_fastlane; + $this->flags['can_use_pay_later'] = $can_use_pay_later; } /** diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index bef842144..b767f3b3d 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -83,9 +83,12 @@ class OnboardingRestEndpoint extends RestEndpoint { 'should_skip_payment_methods' => array( 'js_name' => 'shouldSkipPaymentMethods', ), - 'can_use_fastlane' => array( + 'can_use_fastlane' => array( 'js_name' => 'canUseFastlane', ), + 'can_use_pay_later' => array( + 'js_name' => 'canUsePayLater', + ), ); /** From 4a3aaff0eb3be74fa46e2384ad758b2c759ea7e9 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Wed, 26 Feb 2025 18:52:04 +0400 Subject: [PATCH 13/37] Fix the naming of google and apple pay buttons mapping --- .../ppcp-compat/src/Settings/StylingSettingsMapHelper.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index bc0f1b8eb..fd7983d73 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -23,7 +23,7 @@ class StylingSettingsMapHelper { use ContextTrait; - protected const BUTTON_NAMES = array( 'googlepay', 'applepay', 'pay-later' ); + protected const BUTTON_NAMES = array( 'ppcp-googlepay', 'ppcp-applepay', 'pay-later' ); /** * Maps old setting keys to new setting style names. @@ -84,10 +84,10 @@ class StylingSettingsMapHelper { return $this->mapped_disabled_funding_value( $styling_models ); case 'googlepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'googlepay' ); + return $this->mapped_button_enabled_value( $styling_models, 'ppcp-googlepay' ); case 'applepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'applepay' ); + return $this->mapped_button_enabled_value( $styling_models, 'ppcp-applepay' ); case 'pay_later_button_enabled': return $this->mapped_button_enabled_value( $styling_models, 'pay-later' ); From f302025ca8f42ef1b3faa97778f67c765cc6f8e8 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 26 Feb 2025 17:28:36 +0100 Subject: [PATCH 14/37] Disable save PayPal and Venmo checkbox when not approved for reference transactions --- .../Settings/Components/Settings/Blocks/SavePaymentMethods.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js index 9b77076e2..0a744cc4d 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js @@ -40,6 +40,7 @@ const SavePaymentMethods = () => { ) } value={ savePaypalAndVenmo } onChange={ setSavePaypalAndVenmo } + disabled={ ! savePaypalAndVenmo } /> Date: Wed, 26 Feb 2025 17:37:20 +0100 Subject: [PATCH 15/37] Uncheck and disable toggle if merchant is not approved for reference transactions --- .../Components/Settings/Blocks/SavePaymentMethods.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js index 0a744cc4d..e1e15670a 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js @@ -3,6 +3,7 @@ import { __, sprintf } from '@wordpress/i18n'; import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock'; import { ControlToggleButton } from '../../../../../ReusableComponents/Controls'; import { SettingsHooks } from '../../../../../../data'; +import { useMerchantInfo } from '../../../../../../data/common/hooks'; const SavePaymentMethods = () => { const { @@ -12,6 +13,8 @@ const SavePaymentMethods = () => { setSaveCardDetails, } = SettingsHooks.useSettings(); + const { features } = useMerchantInfo(); + return ( { 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' ) } - value={ savePaypalAndVenmo } + value={ + features.save_paypal_and_venmo.enabled + ? savePaypalAndVenmo + : false + } onChange={ setSavePaypalAndVenmo } - disabled={ ! savePaypalAndVenmo } + disabled={ ! features.save_paypal_and_venmo.enabled } /> Date: Thu, 27 Feb 2025 10:14:17 +0100 Subject: [PATCH 16/37] Remove target PPFrame --- .../Components/Screens/Onboarding/Components/ConnectionButton.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js index 29434f344..55932143c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -32,7 +32,6 @@ const ButtonOrPlaceholder = ( { if ( href ) { buttonProps.href = href; - buttonProps.target = 'PPFrame'; buttonProps[ 'data-paypal-button' ] = 'true'; buttonProps[ 'data-paypal-onboard-button' ] = 'true'; } From aea44dc3f87b9f6152dd92d733c2ecc3a570ea9e Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Thu, 27 Feb 2025 12:54:04 +0100 Subject: [PATCH 17/37] Fix typo --- modules/ppcp-settings/src/Data/OnboardingProfile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index cf2817ede..6c41a1718 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -45,7 +45,7 @@ class OnboardingProfile extends AbstractDataModel { * @param bool $can_use_subscriptions Whether WC Subscriptions plugin is active. * @param bool $should_skip_payment_methods Whether it should skip payment methods screen. * @param bool $can_use_fastlane Whether it can use Fastlane or not. - * @param bool $can_use_pay_later Whether it can use PAy Later or not. + * @param bool $can_use_pay_later Whether it can use Pay Later or not. * * @throws RuntimeException If the OPTION_KEY is not defined in the child class. */ From e83dba1506d45717f300d4ab4486f7e4d4923269 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 27 Feb 2025 18:17:37 +0400 Subject: [PATCH 18/37] Pass the PaymentSettings model instance. We need to pass the PaymentSettings model instance to use it in some helpers. Once the new settings module is permanently enabled, this model can be passed as a dependency to the appropriate helper classes. For now, we must pass it this way to avoid errors when the new settings module is disabled. --- modules/ppcp-compat/services.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index e8ffe6adb..d8afe54f1 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -172,6 +172,16 @@ return array( $container->get( 'settings.data.settings' ), $subscription_map_helper->map() ), + /** + * We need to pass the PaymentSettings model instance to use it in some helpers. + * Once the new settings module is permanently enabled, + * this model can be passed as a dependency to the appropriate helper classes. + * For now, we must pass it this way to avoid errors when the new settings module is disabled. + */ + new SettingsMap( + $container->get( 'settings.data.payment' ), + array() + ), ); }, 'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper { From b74cfba72bed72bddc4567ce93ff22e606dbcac6 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 27 Feb 2025 18:18:37 +0400 Subject: [PATCH 19/37] Pass the PaymentSettings model instance to styling helper. Once the new settings module is permanently enabled, this model can be passed as a dependency to the styling helper class. For now, we must pass it this way to avoid errors when the new settings module is disabled. --- .../src/Settings/SettingsMapHelper.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php index 843b275c0..82507aed4 100644 --- a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php @@ -10,7 +10,9 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; @@ -169,7 +171,11 @@ class SettingsMapHelper { switch ( true ) { case $model instanceof StylingSettings: - return $this->styling_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); + return $this->styling_settings_map_helper->mapped_value( + $old_key, + $this->model_cache[ $model_id ], + $this->get_payment_settings_model() + ); case $model instanceof GeneralSettings: return $this->general_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); @@ -217,4 +223,23 @@ class SettingsMapHelper { } } } + + /** + * Retrieves the PaymentSettings model instance. + * + * Once the new settings module is permanently enabled, + * this model can be passed as a dependency to the appropriate helper classes. + * For now, we must pass it this way to avoid errors when the new settings module is disabled. + * + * @return PaymentSettings|null + */ + protected function get_payment_settings_model() : ?AbstractDataModel { + foreach ( $this->settings_map as $settings_map_instance ) { + if ( $settings_map_instance->get_model() instanceof PaymentSettings ) { + return $settings_map_instance->get_model(); + } + } + + return null; + } } From e913f60885df953579b52a317bffa96aebe94bb2 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 27 Feb 2025 18:33:48 +0400 Subject: [PATCH 20/37] Use the Payment setting model to check if the venmo gateway is enabled. Once the new settings module is permanently enabled, this model can be passed as a dependency to the styling helper class. For now, we must pass it this way to avoid errors when the new settings module is disabled. --- .../src/Settings/StylingSettingsMapHelper.php | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index fd7983d73..f13be3ba4 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; /** @@ -66,10 +67,11 @@ class StylingSettingsMapHelper { * * @param string $old_key The key from the legacy settings. * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param PaymentSettings $payment_settings The payment settings model. * * @return mixed The value of the mapped setting, (null if not found). */ - public function mapped_value( string $old_key, array $styling_models ) { + public function mapped_value( string $old_key, array $styling_models, PaymentSettings $payment_settings ) { switch ( $old_key ) { case 'smart_button_locations': return $this->mapped_smart_button_locations_value( $styling_models ); @@ -81,7 +83,7 @@ class StylingSettingsMapHelper { return $this->mapped_pay_later_button_locations_value( $styling_models ); case 'disable_funding': - return $this->mapped_disabled_funding_value( $styling_models ); + return $this->mapped_disabled_funding_value( $styling_models, $payment_settings ); case 'googlepay_button_enabled': return $this->mapped_button_enabled_value( $styling_models, 'ppcp-googlepay' ); @@ -225,24 +227,23 @@ class StylingSettingsMapHelper { * Retrieves the mapped disabled funding value from the new settings. * * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param PaymentSettings $payment_settings The payment settings model. * @return array|null The list of disabled funding, or null if none are disabled. */ - protected function mapped_disabled_funding_value( array $styling_models ): ?array { + protected function mapped_disabled_funding_value( array $styling_models, PaymentSettings $payment_settings ): ?array { $disabled_funding = array(); $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; foreach ( $styling_models as $model ) { - if ( $model->location !== $current_context || in_array( 'venmo', $model->methods, true ) ) { - continue; + if ( $model->location === $current_context ) { + if ( ! in_array( 'venmo', $model->methods, true ) || ! $payment_settings->get_venmo_enabled() ) { + $disabled_funding[] = 'venmo'; + } } - - $disabled_funding[] = 'venmo'; - - return $disabled_funding; } - return null; + return $disabled_funding; } /** From ffbe9c08d5d8ce33f50aa0141b2e24c903063988 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 27 Feb 2025 13:58:44 +0100 Subject: [PATCH 21/37] =?UTF-8?q?=F0=9F=94=8A=20Add=20logging=20to=20the?= =?UTF-8?q?=20internal=20REST=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 4 +- .../src/Service/InternalRestService.php | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 47b7622eb..51426fa77 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -318,7 +318,9 @@ return array( ); }, 'settings.service.rest-service' => static function ( ContainerInterface $container ) : InternalRestService { - return new InternalRestService(); + return new InternalRestService( + $container->get( 'woocommerce.logger.woocommerce' ) + ); }, 'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer { return new DataSanitizer(); diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php index a83e5fdcf..286cfc5d6 100644 --- a/modules/ppcp-settings/src/Service/InternalRestService.php +++ b/modules/ppcp-settings/src/Service/InternalRestService.php @@ -10,15 +10,42 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Service; use Throwable; +use Psr\Log\LoggerInterface; class InternalRestService { + + /** + * Logger instance. + * + * In this case, the logger is quite important for debugging, because the main + * functionality of this class cannot be step-debugged using Xdebug: While + * a Xdebug session is active, the remote call to the current server is also + * blocked and will end in a timeout. + * + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * Constructor. + * + * @param LoggerInterface $logger Logger instance. + */ + public function __construct( LoggerInterface $logger ) { + $this->logger = $logger; + } + public function get_data( string $endpoint ) : array { $token = $this->generate_token( $endpoint ); $rest_url = rest_url( $endpoint ); $response = wp_remote_get( + $this->logger->info( "Calling internal REST endpoint: $rest_url" ); + + $response = wp_remote_request( $rest_url, array( + 'method' => 'GET', 'headers' => array( 'X-Internal-Token' => $token, 'Content-Type' => 'application/json', @@ -27,6 +54,8 @@ class InternalRestService { ); if ( is_wp_error( $response ) ) { + $this->logger->error( 'Internal REST error', array( 'response' => $response ) ); + return array(); } @@ -35,13 +64,25 @@ class InternalRestService { try { $json = json_decode( $body, true, 512, JSON_THROW_ON_ERROR ); } catch ( Throwable $exception ) { + $this->logger->error( + 'Internal REST error: Invalid JSON response', + array( + 'error' => $exception->getMessage(), + 'response_body' => $body, + ) + ); + return array(); } if ( ! $json || empty( $json['success'] ) ) { + $this->logger->error( 'Internal REST error: Invalid response', array( 'json' => $json ) ); + return array(); } + $this->logger->info( 'Internal REST success', array( 'data' => $json['data'] ) ); + return $json['data']; } From 7ac0a4f62b04c5aea00dbb91f2bb622f1ede5f82 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 27 Feb 2025 16:11:13 +0100 Subject: [PATCH 22/37] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Use=20WP=20authen?= =?UTF-8?q?tication=20cookies=20for=20REST=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/CommonRestEndpoint.php | 7 +-- .../src/Service/InternalRestService.php | 59 +++++++++++++++---- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 82aa06b29..f81e74d79 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -195,12 +195,7 @@ class CommonRestEndpoint extends RestEndpoint { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_seller_account_info' ), - 'permission_callback' => function ( WP_REST_Request $request ) { - $token = $request->get_header( 'X-Internal-Token' ); - $endpoint = self::seller_account_route(); - - return $this->rest_service->verify_token( $token, $endpoint ); - }, + 'permission_callback' => array( $this, 'check_permission' ), ) ); } diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php index 286cfc5d6..dfc8483b5 100644 --- a/modules/ppcp-settings/src/Service/InternalRestService.php +++ b/modules/ppcp-settings/src/Service/InternalRestService.php @@ -11,7 +11,15 @@ namespace WooCommerce\PayPalCommerce\Settings\Service; use Throwable; use Psr\Log\LoggerInterface; +use WP_Http_Cookie; +/** + * Consume internal REST endpoints from server-side. + * + * This service makes a real HTTP request to the endpoint, forwarding the + * authentication cookies of the current request to maintain the user session + * while invoking a completely isolated and freshly initialized server request. + */ class InternalRestService { /** @@ -35,11 +43,17 @@ class InternalRestService { $this->logger = $logger; } + /** + * Performs a REST call to the defined local REST endpoint. + * + * @param string $endpoint The endpoint for which the token is generated. + * @return mixed The REST response. + */ public function get_data( string $endpoint ) : array { - $token = $this->generate_token( $endpoint ); - $rest_url = rest_url( $endpoint ); + $rest_url = rest_url( $endpoint ); + $rest_nonce = wp_create_nonce( 'wp_rest' ); + $auth_cookies = $this->build_authentication_cookie(); - $response = wp_remote_get( $this->logger->info( "Calling internal REST endpoint: $rest_url" ); $response = wp_remote_request( @@ -47,9 +61,10 @@ class InternalRestService { array( 'method' => 'GET', 'headers' => array( - 'X-Internal-Token' => $token, - 'Content-Type' => 'application/json', + 'Content-Type' => 'application/json', + 'X-WP-Nonce' => $rest_nonce, ), + 'cookies' => $auth_cookies, ) ); @@ -86,13 +101,35 @@ class InternalRestService { return $json['data']; } - public function verify_token( string $token, string $endpoint ) : bool { - $expected_token = $this->generate_token( $endpoint ); + /** + * Generate the cookie collection with relevant WordPress authentication + * cookies, which allows us to extend the current user's session to the + * called REST endpoint. + * + * @return array A list of cookies that are required to authenticate the user. + */ + private function build_authentication_cookie() : array { + $cookies = array(); - return $expected_token === $token; - } + // Cookie names are defined in constants and can be changed by site owners. + $wp_cookie_constants = array( 'AUTH_COOKIE', 'SECURE_AUTH_COOKIE', 'LOGGED_IN_COOKIE' ); - private function generate_token( string $token_id ) : string { - return base64_encode( $token_id ); + foreach ( $wp_cookie_constants as $cookie_const ) { + $cookie_name = (string) constant( $cookie_const ); + + if ( ! isset( $_COOKIE[ $cookie_name ] ) ) { + continue; + } + + $cookies[] = new WP_Http_Cookie( + array( + 'name' => $cookie_name, + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + 'value' => wp_unslash( $_COOKIE[ $cookie_name ] ), + ) + ); + } + + return $cookies; } } From 61323d5cc5b1cead4e9038f7242e90fb762c8bb7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 27 Feb 2025 16:13:23 +0100 Subject: [PATCH 23/37] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20the=20main?= =?UTF-8?q?=20REST=20service=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Service/AuthenticationManager.php | 2 +- modules/ppcp-settings/src/Service/InternalRestService.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index b48a7554a..950dcad06 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -446,7 +446,7 @@ class AuthenticationManager { try { $endpoint = CommonRestEndpoint::seller_account_route( true ); - $details = $this->rest_service->get_data( $endpoint ); + $details = $this->rest_service->get_response( $endpoint ); // Request the merchant details via a PayPal API request. $connection = $this->common_settings->get_merchant_data(); diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php index dfc8483b5..66e055de9 100644 --- a/modules/ppcp-settings/src/Service/InternalRestService.php +++ b/modules/ppcp-settings/src/Service/InternalRestService.php @@ -49,7 +49,7 @@ class InternalRestService { * @param string $endpoint The endpoint for which the token is generated. * @return mixed The REST response. */ - public function get_data( string $endpoint ) : array { + public function get_response( string $endpoint ) { $rest_url = rest_url( $endpoint ); $rest_nonce = wp_create_nonce( 'wp_rest' ); $auth_cookies = $this->build_authentication_cookie(); From 447666f9e0a7dbf55d0ef6d3ce1de2c8ed29520f Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Thu, 27 Feb 2025 19:26:42 +0400 Subject: [PATCH 24/37] Use the Payment setting model to check if the gateway is enabled. Once the new settings module is permanently enabled, this model can be passed as a dependency to the styling helper class. For now, we must pass it this way to avoid errors when the new settings module is disabled. --- .../src/Settings/StylingSettingsMapHelper.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index f13be3ba4..fa894bca4 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -86,13 +86,13 @@ class StylingSettingsMapHelper { return $this->mapped_disabled_funding_value( $styling_models, $payment_settings ); case 'googlepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'ppcp-googlepay' ); + return $this->mapped_button_enabled_value( $styling_models, 'ppcp-googlepay', $payment_settings ); case 'applepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'ppcp-applepay' ); + return $this->mapped_button_enabled_value( $styling_models, 'ppcp-applepay', $payment_settings ); case 'pay_later_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'pay-later' ); + return $this->mapped_button_enabled_value( $styling_models, 'pay-later', $payment_settings ); default: foreach ( $this->locations_map() as $old_location_name => $new_location_name ) { @@ -251,14 +251,15 @@ class StylingSettingsMapHelper { * * @param LocationStylingDTO[] $styling_models The list of location styling models. * @param string $button_name The button name (see {@link self::BUTTON_NAMES}). + * @param PaymentSettings $payment_settings The payment settings model. * @return int The enabled (1) or disabled (0) state. * @throws RuntimeException If an invalid button name is provided. */ - protected function mapped_button_enabled_value( array $styling_models, string $button_name ): ?int { + protected function mapped_button_enabled_value( array $styling_models, string $button_name, PaymentSettings $payment_settings ): ?int { if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) { throw new RuntimeException( 'Wrong button name is provided.' ); } - +var_dump($payment_settings->is_method_enabled($button_name)); $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; From 39394e864ac374001aa29d6089fadeee256d6102 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 27 Feb 2025 17:11:39 +0100 Subject: [PATCH 25/37] =?UTF-8?q?=E2=9C=A8=20Finish=20new=20REST=20endpoin?= =?UTF-8?q?t=20to=20provide=20real=20country?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 2 +- .../src/Endpoint/CommonRestEndpoint.php | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 51426fa77..8f35492f8 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -171,7 +171,7 @@ return array( 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { return new CommonRestEndpoint( $container->get( 'settings.data.general' ), - $container->get( 'settings.service.rest-service' ) + $container->get( 'api.endpoint.partners' ) ); }, 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index f81e74d79..323d10762 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -14,6 +14,7 @@ use WP_REST_Response; use WP_REST_Request; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; /** * REST controller for "common" settings, which are used and modified by @@ -43,11 +44,11 @@ class CommonRestEndpoint extends RestEndpoint { protected GeneralSettings $settings; /** - * Internal REST handler, used to authenticate internal requests. + * The Partners-Endpoint instance to request seller details from PayPal's API. * - * @var InternalRestService + * @var PartnersEndpoint */ - protected InternalRestService $rest_service; + protected PartnersEndpoint $partners_endpoint; /** * Field mapping for request to profile transformation. @@ -117,12 +118,12 @@ class CommonRestEndpoint extends RestEndpoint { /** * Constructor. * - * @param GeneralSettings $settings The settings instance. - * @param InternalRestService $rest_service Internal REST handler, for authentication. + * @param GeneralSettings $settings The settings instance. + * @param PartnersEndpoint $partners_endpoint Partners-API to get merchant details from PayPal. */ - public function __construct( GeneralSettings $settings, InternalRestService $rest_service ) { - $this->settings = $settings; - $this->rest_service = $rest_service; + public function __construct( GeneralSettings $settings, PartnersEndpoint $partners_endpoint ) { + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; } /** @@ -256,7 +257,9 @@ class CommonRestEndpoint extends RestEndpoint { * @return WP_REST_Response Seller details, provided by PayPal's API. */ public function get_seller_account_info() : WP_REST_Response { - return $this->return_success( array( 'country' => 'XY' ) ); + $seller_status = $this->partners_endpoint->seller_status(); + + return $this->return_success( array( 'country' => $seller_status->country() ) ); } /** From a1cbabf84cd2c3457de526c46da891bc9b0e37d7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 27 Feb 2025 17:13:09 +0100 Subject: [PATCH 26/37] =?UTF-8?q?=F0=9F=8E=A8=20Slightly=20improve=20code?= =?UTF-8?q?=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/AuthenticationManager.php | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 950dcad06..4badb92d7 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -447,19 +447,20 @@ class AuthenticationManager { try { $endpoint = CommonRestEndpoint::seller_account_route( true ); $details = $this->rest_service->get_response( $endpoint ); - - // Request the merchant details via a PayPal API request. - $connection = $this->common_settings->get_merchant_data(); - - // Enrich the connection details with additional details. - $connection->merchant_country = $details['country']; - - // Persist the changes. - $this->common_settings->set_merchant_data( $connection ); - $this->common_settings->save(); } catch ( Throwable $exception ) { $this->logger->warning( 'Could not determine merchant country: ' . $exception->getMessage() ); + return; } + + // Request the merchant details via a PayPal API request. + $connection = $this->common_settings->get_merchant_data(); + + // Enrich the connection details with additional details. + $connection->merchant_country = $details['country']; + + // Persist the changes. + $this->common_settings->set_merchant_data( $connection ); + $this->common_settings->save(); } /** From db3fa08e7efa6138ae3c6ad69921501083fbde9b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 27 Feb 2025 17:21:07 +0100 Subject: [PATCH 27/37] =?UTF-8?q?=E2=9C=A8=20Add=20a=20fallback=20value=20?= =?UTF-8?q?for=20the=20merchant=20country?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Data/GeneralSettings.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/ppcp-settings/src/Data/GeneralSettings.php b/modules/ppcp-settings/src/Data/GeneralSettings.php index 7953d6d95..5c816c4cb 100644 --- a/modules/ppcp-settings/src/Data/GeneralSettings.php +++ b/modules/ppcp-settings/src/Data/GeneralSettings.php @@ -250,6 +250,11 @@ class GeneralSettings extends AbstractDataModel { * @return string */ public function get_merchant_country() : string { + // When we don't know the merchant's real country, we assume it's the Woo store-country. + if ( empty( $this->data['merchant_country'] ) ) { + return $this->woo_settings['country']; + } + return $this->data['merchant_country']; } } From ca870ecf6f891511a827fe2c5369b738ffa3e6ef Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 15:49:42 +0400 Subject: [PATCH 28/37] Use the Payment setting model to check if the gateway is enabled. Once the new settings module is permanently enabled, this model can be passed as a dependency to the styling helper class. For now, we must pass it this way to avoid errors when the new settings module is disabled. --- .../src/Settings/StylingSettingsMapHelper.php | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index fa894bca4..079029ce8 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; @@ -259,19 +260,37 @@ class StylingSettingsMapHelper { if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) { throw new RuntimeException( 'Wrong button name is provided.' ); } -var_dump($payment_settings->is_method_enabled($button_name)); + $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; foreach ( $styling_models as $model ) { - if ( ! $model->enabled - || $model->location !== $current_context - || ! in_array( $button_name, $model->methods, true ) - ) { - continue; + if ( $model->enabled && $model->location === $current_context ) { + if ( in_array( $button_name, $model->methods, true ) && $payment_settings->is_method_enabled( $button_name ) ) { + return 1; + } } + } - return 1; + if ( $current_context === 'classic_checkout' ) { + /** + * Outputs an inline CSS style that hides the Google Pay gateway (on Classic Checkout) + * In case if the button is disabled from the styling settings but the gateway itself is enabled. + * + * @return void + */ + add_action( + 'woocommerce_paypal_payments_checkout_button_render', + static function (): void { + ?> + + Date: Fri, 28 Feb 2025 15:50:31 +0400 Subject: [PATCH 29/37] just return true when is the old settings --- modules/ppcp-settings/src/Data/PaymentSettings.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/ppcp-settings/src/Data/PaymentSettings.php b/modules/ppcp-settings/src/Data/PaymentSettings.php index 52ac3e419..7af4b2aa1 100644 --- a/modules/ppcp-settings/src/Data/PaymentSettings.php +++ b/modules/ppcp-settings/src/Data/PaymentSettings.php @@ -105,6 +105,12 @@ class PaymentSettings extends AbstractDataModel { return $this->get_paylater_enabled(); default: + if ( + ! did_filter( 'woocommerce_payment_gateways' ) + || doing_filter( 'woocommerce_payment_gateways' ) + ) { + return true; + } $gateway = $this->get_gateway( $method_id ); if ( $gateway ) { From d83b57b887bea4798d9a327045363705692cb987 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 15:51:39 +0400 Subject: [PATCH 30/37] Check if the button is enabled before rendering on block pages --- .../ppcp-googlepay/resources/js/boot-block.js | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/boot-block.js b/modules/ppcp-googlepay/resources/js/boot-block.js index 8c63bba14..22f681db7 100644 --- a/modules/ppcp-googlepay/resources/js/boot-block.js +++ b/modules/ppcp-googlepay/resources/js/boot-block.js @@ -81,22 +81,23 @@ const GooglePayComponent = ( { isEditing, buttonAttributes } ) => { }; const features = [ 'products' ]; - -registerExpressPaymentMethod( { - name: buttonData.id, - title: `PayPal - ${ buttonData.title }`, - description: __( - 'Eligible users will see the PayPal button.', - 'woocommerce-paypal-payments' - ), - gatewayId: 'ppcp-gateway', - label:
, - content: , - edit: , - ariaLabel: buttonData.title, - canMakePayment: () => buttonData.enabled, - supports: { - features, - style: [ 'height', 'borderRadius' ], - }, -} ); +if ( buttonConfig?.is_enabled ) { + registerExpressPaymentMethod( { + name: buttonData.id, + title: `PayPal - ${ buttonData.title }`, + description: __( + 'Eligible users will see the PayPal button.', + 'woocommerce-paypal-payments' + ), + gatewayId: 'ppcp-gateway', + label:
, + content: , + edit: , + ariaLabel: buttonData.title, + canMakePayment: () => buttonData.enabled, + supports: { + features, + style: [ 'height', 'borderRadius' ], + }, + } ); +} From 860372fb7cb50606481f0e1af4a83190c9bd2e5c Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 15:53:32 +0400 Subject: [PATCH 31/37] Use the constants from the gateways instead of hard-coding the names --- .../ppcp-compat/src/Settings/StylingSettingsMapHelper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index 079029ce8..acb723b34 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; @@ -25,7 +26,7 @@ class StylingSettingsMapHelper { use ContextTrait; - protected const BUTTON_NAMES = array( 'ppcp-googlepay', 'ppcp-applepay', 'pay-later' ); + protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID, 'pay-later' ); /** * Maps old setting keys to new setting style names. @@ -87,10 +88,10 @@ class StylingSettingsMapHelper { return $this->mapped_disabled_funding_value( $styling_models, $payment_settings ); case 'googlepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'ppcp-googlepay', $payment_settings ); + return $this->mapped_button_enabled_value( $styling_models, GooglePayGateway::ID, $payment_settings ); case 'applepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'ppcp-applepay', $payment_settings ); + return $this->mapped_button_enabled_value( $styling_models, ApplePayGateway::ID, $payment_settings ); case 'pay_later_button_enabled': return $this->mapped_button_enabled_value( $styling_models, 'pay-later', $payment_settings ); From 88c0ce6ee3248a85ba4aa78cc5fe91cf197be27d Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 16:10:22 +0400 Subject: [PATCH 32/37] Fix the tests --- .../src/Settings/SettingsMapHelper.php | 2 +- .../src/Settings/StylingSettingsMapHelper.php | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php index 82507aed4..667eb44a2 100644 --- a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php @@ -231,7 +231,7 @@ class SettingsMapHelper { * this model can be passed as a dependency to the appropriate helper classes. * For now, we must pass it this way to avoid errors when the new settings module is disabled. * - * @return PaymentSettings|null + * @return AbstractDataModel|null */ protected function get_payment_settings_model() : ?AbstractDataModel { foreach ( $this->settings_map as $settings_map_instance ) { diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index acb723b34..0fd6af183 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -13,6 +13,7 @@ use RuntimeException; use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; @@ -67,13 +68,13 @@ class StylingSettingsMapHelper { /** * Retrieves the value of a mapped key from the new settings. * - * @param string $old_key The key from the legacy settings. - * @param LocationStylingDTO[] $styling_models The list of location styling models. - * @param PaymentSettings $payment_settings The payment settings model. + * @param string $old_key The key from the legacy settings. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param AbstractDataModel|null $payment_settings The payment settings model. * * @return mixed The value of the mapped setting, (null if not found). */ - public function mapped_value( string $old_key, array $styling_models, PaymentSettings $payment_settings ) { + public function mapped_value( string $old_key, array $styling_models, ?AbstractDataModel $payment_settings ) { switch ( $old_key ) { case 'smart_button_locations': return $this->mapped_smart_button_locations_value( $styling_models ); @@ -228,14 +229,19 @@ class StylingSettingsMapHelper { /** * Retrieves the mapped disabled funding value from the new settings. * - * @param LocationStylingDTO[] $styling_models The list of location styling models. - * @param PaymentSettings $payment_settings The payment settings model. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param AbstractDataModel|null $payment_settings The payment settings model. * @return array|null The list of disabled funding, or null if none are disabled. */ - protected function mapped_disabled_funding_value( array $styling_models, PaymentSettings $payment_settings ): ?array { + protected function mapped_disabled_funding_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?array { + if ( is_null( $payment_settings ) ) { + return null; + } + $disabled_funding = array(); $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; + assert( $payment_settings instanceof PaymentSettings ); foreach ( $styling_models as $model ) { if ( $model->location === $current_context ) { From cf30631c19721f7c36af241f9b07d8d3bc146d8a Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 16:24:41 +0400 Subject: [PATCH 33/37] Fix the tests --- .../src/Settings/StylingSettingsMapHelper.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index 0fd6af183..42a810670 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -257,19 +257,24 @@ class StylingSettingsMapHelper { /** * Retrieves the mapped enabled or disabled button value from the new settings. * - * @param LocationStylingDTO[] $styling_models The list of location styling models. - * @param string $button_name The button name (see {@link self::BUTTON_NAMES}). - * @param PaymentSettings $payment_settings The payment settings model. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param string $button_name The button name (see {@link self::BUTTON_NAMES}). + * @param AbstractDataModel|null $payment_settings The payment settings model. * @return int The enabled (1) or disabled (0) state. * @throws RuntimeException If an invalid button name is provided. */ - protected function mapped_button_enabled_value( array $styling_models, string $button_name, PaymentSettings $payment_settings ): ?int { + protected function mapped_button_enabled_value( array $styling_models, string $button_name, ?AbstractDataModel $payment_settings ): ?int { + if ( is_null( $payment_settings ) ) { + return null; + } + if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) { throw new RuntimeException( 'Wrong button name is provided.' ); } $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; + assert( $payment_settings instanceof PaymentSettings ); foreach ( $styling_models as $model ) { if ( $model->enabled && $model->location === $current_context ) { From 04053078116208f68c861c7d49939166c753a4f9 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 17:47:17 +0400 Subject: [PATCH 34/37] Add Psalm stubs for `doing_filter` & `did_filter` --- .psalm/stubs.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 56ba72451..27484ec28 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -94,6 +94,45 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = return 0; } +/** + * Retrieves the number of times a filter has been applied during the current request. + * + * @since 6.1.0 + * + * @global int[] $wp_filters Stores the number of times each filter was triggered. + * + * @param string $hook_name The name of the filter hook. + * @return int The number of times the filter hook has been applied. + */ +function did_filter( $hook_name ) { + return 0; +} + +/** + * Returns whether or not a filter hook is currently being processed. + * + * The function current_filter() only returns the most recent filter being executed. + * did_filter() returns the number of times a filter has been applied during + * the current request. + * + * This function allows detection for any filter currently being executed + * (regardless of whether it's the most recent filter to fire, in the case of + * hooks called from hook callbacks) to be verified. + * + * @since 3.9.0 + * + * @see current_filter() + * @see did_filter() + * @global string[] $wp_current_filter Current filter. + * + * @param string|null $hook_name Optional. Filter hook to check. Defaults to null, + * which checks if any filter is currently being run. + * @return bool Whether the filter is currently in the stack. + */ +function doing_filter( $hook_name = null ) { + return false; +} + /** * HTML API: WP_HTML_Tag_Processor class */ From e06ea0036f46273e56e557eb7e5d318f81a36e5d Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 19:46:31 +0400 Subject: [PATCH 35/37] Fix the new settings module enabled logic. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit instead of `$preview_new_ux = '0' !== getenv( 'PCP_SETTINGS_ENABLED' );` we should use `$preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' );` becuase the module will be always enabled unless you set `PCP_SETTINGS_ENABLED = '0'` but by default the `PCP_SETTINGS_ENABLED` is `false` because it doesn’t exist. --- modules.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules.php b/modules.php index a63e9c38a..0d65644f2 100644 --- a/modules.php +++ b/modules.php @@ -92,7 +92,7 @@ return function ( string $root_dir ): iterable { } $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' ); - $preview_new_ux = '0' !== getenv( 'PCP_SETTINGS_ENABLED' ); + $preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' ); if ( apply_filters( 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', From c8d9a142ea1555a2e8e7b8afbf4779c0479f9e1c Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 19:47:03 +0400 Subject: [PATCH 36/37] Pass a boolean to the map to check if the new settings module is enabled --- modules/ppcp-compat/services.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index d8afe54f1..e523f10ff 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -190,7 +190,8 @@ return array( $container->get( 'compat.settings.styling_map_helper' ), $container->get( 'compat.settings.settings_tab_map_helper' ), $container->get( 'compat.settings.subscription_map_helper' ), - $container->get( 'compat.settings.general_map_helper' ) + $container->get( 'compat.settings.general_map_helper' ), + $container->get( 'wcgateway.settings.admin-settings-enabled' ) ); }, 'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper { From 7dc2fef1da72bf0348f5dce6b6b470228f4b2937 Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 28 Feb 2025 19:48:03 +0400 Subject: [PATCH 37/37] If the new settings module is not enabled, return `null` This will force the `Settings` old class to always fallback to the old settings value --- .../src/Settings/SettingsMapHelper.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php index 667eb44a2..6d3f5d5b4 100644 --- a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php @@ -74,6 +74,13 @@ class SettingsMapHelper { */ protected GeneralSettingsMapHelper $general_settings_map_helper; + /** + * Whether the new settings module is enabled. + * + * @var bool + */ + protected bool $new_settings_module_enabled; + /** * Constructor. * @@ -82,6 +89,7 @@ class SettingsMapHelper { * @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings. * @param SubscriptionSettingsMapHelper $subscription_map_helper A helper for mapping old and new subscription settings. * @param GeneralSettingsMapHelper $general_settings_map_helper A helper for mapping old and new general settings. + * @param bool $new_settings_module_enabled Whether the new settings module is enabled. * @throws RuntimeException When an old key has multiple mappings. */ public function __construct( @@ -89,7 +97,8 @@ class SettingsMapHelper { StylingSettingsMapHelper $styling_settings_map_helper, SettingsTabMapHelper $settings_tab_map_helper, SubscriptionSettingsMapHelper $subscription_map_helper, - GeneralSettingsMapHelper $general_settings_map_helper + GeneralSettingsMapHelper $general_settings_map_helper, + bool $new_settings_module_enabled ) { $this->validate_settings_map( $settings_map ); $this->settings_map = $settings_map; @@ -97,6 +106,7 @@ class SettingsMapHelper { $this->settings_tab_map_helper = $settings_tab_map_helper; $this->subscription_map_helper = $subscription_map_helper; $this->general_settings_map_helper = $general_settings_map_helper; + $this->new_settings_module_enabled = $new_settings_module_enabled; } /** @@ -126,6 +136,10 @@ class SettingsMapHelper { * @return mixed|null The value of the mapped setting, or null if not found. */ public function mapped_value( string $old_key ) { + if ( ! $this->new_settings_module_enabled ) { + return null; + } + $this->ensure_map_initialized(); if ( ! isset( $this->key_to_model[ $old_key ] ) ) { return null; @@ -149,6 +163,10 @@ class SettingsMapHelper { * @return bool True if the key exists in the new settings, false otherwise. */ public function has_mapped_key( string $old_key ) : bool { + if ( ! $this->new_settings_module_enabled ) { + return false; + } + $this->ensure_map_initialized(); return isset( $this->key_to_model[ $old_key ] );