From a5bbee307d732573eabbf39f906c9f1f5b56765a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 16:04:36 +0100 Subject: [PATCH 01/57] =?UTF-8?q?=F0=9F=9A=9A=20Move=20ajax=20handler=20to?= =?UTF-8?q?=20separate=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 2 +- .../src/{Endpoint => Ajax}/SwitchSettingsUiEndpoint.php | 4 ++-- modules/ppcp-settings/src/SettingsModule.php | 2 +- modules/ppcp-uninstall/services.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename modules/ppcp-settings/src/{Endpoint => Ajax}/SwitchSettingsUiEndpoint.php (94%) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 349c13350..f1c1730c4 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; -use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; +use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; diff --git a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php similarity index 94% rename from modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php rename to modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php index 244c26dfe..04a6cc86e 100644 --- a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Ajax/SwitchSettingsUiEndpoint.php @@ -1,13 +1,13 @@ Date: Tue, 17 Dec 2024 16:08:09 +0100 Subject: [PATCH 02/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adjust=20service=20n?= =?UTF-8?q?ame=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 2 +- modules/ppcp-settings/src/SettingsModule.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index f1c1730c4..c785111aa 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -192,7 +192,7 @@ return array( return $generators; }, - 'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { + 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { return new SwitchSettingsUiEndpoint( $container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'button.request-data' ), diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 3197b1cba..931a9782e 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -86,7 +86,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); - $endpoint = $container->get( 'settings.switch-ui.endpoint' ) ? $container->get( 'settings.switch-ui.endpoint' ) : null; + $endpoint = $container->get( 'settings.ajax.switch_ui' ) ? $container->get( 'settings.ajax.switch_ui' ) : null; assert( $endpoint instanceof SwitchSettingsUiEndpoint ); add_action( From a2221e52337ea7f90ec9d22ecb42f4c0cce3018d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 19:34:22 +0100 Subject: [PATCH 03/57] =?UTF-8?q?=E2=9C=A8=20Introduce=20new=20connection?= =?UTF-8?q?=20manager=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 9 + .../ppcp-settings/src/Data/CommonSettings.php | 14 + .../src/Service/ConnectionManager.php | 242 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 modules/ppcp-settings/src/Service/ConnectionManager.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index c785111aa..1397b137b 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; +use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -192,6 +193,14 @@ return array( return $generators; }, + 'settings.service.connection_manager' => static function ( ContainerInterface $container ) : ConnectionManager { + return new ConnectionManager( + $container->get( 'settings.data.common' ), + $container->get( 'api.paypal-host-production' ), + $container->get( 'api.paypal-host-sandbox' ), + $container->get( 'woocommerce.logger.woocommerce' ), + ); + }, 'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { return new SwitchSettingsUiEndpoint( $container->get( 'woocommerce.logger.woocommerce' ), diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 1894255ff..0935734b8 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -169,6 +169,20 @@ class CommonSettings extends AbstractDataModel { $this->data['merchant_connected'] = true; } + /** + * Reset all connection details to the initial, disconnected state. + * + * @return void + */ + public function reset_merchant_data() : void { + $defaults = $this->get_defaults(); + + $this->data['sandbox_merchant'] = $defaults['sandbox_merchant']; + $this->data['merchant_id'] = $defaults['merchant_id']; + $this->data['merchant_email'] = $defaults['merchant_email']; + $this->data['merchant_connected'] = $defaults['merchant_connected']; + } + /** * Whether the currently connected merchant is a sandbox account. * diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php new file mode 100644 index 000000000..1cffb155c --- /dev/null +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -0,0 +1,242 @@ + + */ + private array $connection_hosts; + + /** + * Constructor. + * + * @param CommonSettings $common_settings Data model that stores the connection details. + * @param string $live_host The API host for the live mode. + * @param string $sandbox_host The API host for the sandbox mode. + * @param LoggerInterface $logger Logging instance. + */ + public function __construct( CommonSettings $common_settings, string $live_host, string $sandbox_host, LoggerInterface $logger ) { + $this->common_settings = $common_settings; + $this->logger = $logger; + $this->connection_hosts = array( + 'live' => $live_host, + 'sandbox' => $sandbox_host, + ); + } + + /** + * Returns details about the currently connected merchant. + * + * @return array + */ + public function get_account_details() : array { + return array( + 'is_sandbox' => $this->common_settings->is_sandbox_merchant(), + 'is_connected' => $this->common_settings->is_merchant_connected(), + 'merchant_id' => $this->common_settings->get_merchant_id(), + 'merchant_email' => $this->common_settings->get_merchant_email(), + ); + } + + /** + * Removes any connection details we currently have stored. + * + * @return void + */ + public function disconnect() : void { + $this->logger->info( 'Disconnecting merchant from PayPal...' ); + + $this->common_settings->reset_merchant_data(); + $this->common_settings->save(); + } + + /** + * Checks if the provided ID and secret have a valid format. + * + * On failure, an Exception is thrown, while a successful check does not + * generate any return value. + * + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @return void + * @throws RuntimeException When invalid client ID or secret provided. + */ + public function validate_id_and_secret( string $client_id, string $client_secret ) : void { + if ( empty( $client_id ) ) { + throw new RuntimeException( 'No client ID provided.' ); + } + + if ( false === preg_match( '/^A[\w-]{79}$/', $client_secret ) ) { + throw new RuntimeException( 'Invalid client ID provided.' ); + } + + if ( empty( $client_secret ) ) { + throw new RuntimeException( 'No client secret provided.' ); + } + } + + /** + * Disconnects the current merchant, and then attempts to connect to a + * PayPal account using a client ID and secret. + * + * @param bool $use_sandbox Whether to use the sandbox mode. + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @return void + * @throws RuntimeException When failed to retrieve payee. + */ + public function connect_via_secret( bool $use_sandbox, string $client_id, string $client_secret ) : void { + $this->disconnect(); + + $this->logger->info( + 'Attempting manual connection to PayPal...', + array( + 'sandbox' => $use_sandbox, + 'client_id' => $client_id, + ) + ); + + $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); + + $this->update_connection_details( $use_sandbox, $payee['merchant_id'], $payee['email_address'] ); + } + + + // ---------------------------------------------------------------------------- + // Internal helper methods + + + /** + * Returns the API host for the relevant environment. + * + * @param bool $for_sandbox Whether to return the sandbox API host. + * @return string + */ + private function get_host( bool $for_sandbox = false ) : string { + return $for_sandbox ? $this->connection_hosts['sandbox'] : $this->connection_hosts['live']; + } + + /** + * Retrieves the payee object with the merchant data by creating a minimal PayPal order. + * + * @param string $client_id The client ID. + * @param string $client_secret The client secret. + * @param bool $use_sandbox Whether to use the sandbox mode. + * + * @return array Payee details, containing 'merchant_id' and 'merchant_email' keys. + * @throws RuntimeException When failed to retrieve payee. + */ + private function request_payee( + string $client_id, + string $client_secret, + bool $use_sandbox + ) : array { + $host = $this->get_host( $use_sandbox ); + + $bearer = new PayPalBearer( + new InMemoryCache(), + $host, + $client_id, + $client_secret, + $this->logger, + null + ); + + $orders = new Orders( + $host, + $bearer, + $this->logger + ); + + $request_body = array( + 'intent' => 'CAPTURE', + 'purchase_units' => array( + array( + 'amount' => array( + 'currency_code' => 'USD', + 'value' => 1.0, + ), + ), + ), + ); + + $response = $orders->create( $request_body ); + $body = json_decode( $response['body'] ); + + $order_id = $body->id; + + $order_response = $orders->order( $order_id ); + $order_body = json_decode( $order_response['body'] ); + + $pu = $order_body->purchase_units[0]; + $payee = $pu->payee; + + if ( ! is_object( $payee ) ) { + throw new RuntimeException( 'Payee not found.' ); + } + if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) { + throw new RuntimeException( 'Payee info not found.' ); + } + + return array( + 'merchant_id' => $payee->merchant_id, + 'email_address' => $payee->email_address, + ); + } + + /** + * Stores the provided details in the data model. + * + * @param bool $is_sandbox Whether the details are for a sandbox account. + * @param string $merchant_id PayPal's internal merchant ID. + * @param string $merchant_email Email address associated with the PayPal account. + * @return void + */ + private function update_connection_details( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + $this->logger->info( + 'Updating connection details', + array( + 'sandbox' => $is_sandbox, + 'merchant_id' => $merchant_id, + 'merchant_email' => $merchant_email, + ) + ); + + $this->common_settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->common_settings->save(); + } +} From 45db5abb34422dc25b656bcc975742fb1455bde0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 17 Dec 2024 19:35:43 +0100 Subject: [PATCH 04/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20new=20connecti?= =?UTF-8?q?on=20manager=20in=20REST=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 5 +- .../Endpoint/ConnectManualRestEndpoint.php | 161 +++--------------- 2 files changed, 24 insertions(+), 142 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 1397b137b..6b676850d 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -81,10 +81,7 @@ return array( }, 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint { return new ConnectManualRestEndpoint( - $container->get( 'api.paypal-host-production' ), - $container->get( 'api.paypal-host-sandbox' ), - $container->get( 'woocommerce.logger.woocommerce' ), - $container->get( 'settings.data.general' ) + $container->get( 'settings.service.connection_manager' ), ); }, 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint { diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php index 7046342a2..efeda393d 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php @@ -20,33 +20,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; /** * REST controller for connection via manual credentials input. */ class ConnectManualRestEndpoint extends RestEndpoint { - - /** - * The API host for the live mode. - * - * @var string - */ - private string $live_host; - - /** - * The API host for the sandbox mode. - * - * @var string - */ - private string $sandbox_host; - - /** - * The logger. - * - * @var LoggerInterface - */ - private $logger; - /** * The base path for this REST controller. * @@ -54,13 +33,6 @@ class ConnectManualRestEndpoint extends RestEndpoint { */ protected $rest_base = 'connect_manual'; - /** - * Settings instance. - * - * @var GeneralSettings - */ - private $settings = null; - /** * Field mapping for request. * @@ -81,24 +53,27 @@ class ConnectManualRestEndpoint extends RestEndpoint { ), ); + /** + * Defines the JSON response format (when connection was successful). + * + * @var array + */ + private array $response_map = array( + 'merchant_id' => array( + 'js_name' => 'merchantId', + ), + 'merchant_email' => array( + 'js_name' => 'email', + ), + ); + /** * ConnectManualRestEndpoint constructor. * - * @param string $live_host The API host for the live mode. - * @param string $sandbox_host The API host for the sandbox mode. - * @param LoggerInterface $logger The logger. - * @param GeneralSettings $settings Settings instance. + * @param ConnectionManager $connection_manager The connection manager. */ - public function __construct( - string $live_host, - string $sandbox_host, - LoggerInterface $logger, - GeneralSettings $settings - ) { - $this->live_host = $live_host; - $this->sandbox_host = $sandbox_host; - $this->logger = $logger; - $this->settings = $settings; + public function __construct( ConnectionManager $connection_manager ) { + $this->connection_manager = $connection_manager; } /** @@ -133,106 +108,16 @@ class ConnectManualRestEndpoint extends RestEndpoint { $client_secret = $data['client_secret'] ?? ''; $use_sandbox = (bool) ( $data['use_sandbox'] ?? false ); - if ( empty( $client_id ) || empty( $client_secret ) ) { - return $this->return_error( 'No client ID or secret provided.' ); - } - try { - $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); + $this->connection_manager->validate_id_and_secret( $client_id, $client_secret ); + $this->connection_manager->connect_via_secret( $use_sandbox, $client_id, $client_secret ); } catch ( Exception $exception ) { return $this->return_error( $exception->getMessage() ); } - if ( $use_sandbox ) { - $this->settings->set_is_sandbox( true ); - $this->settings->set_sandbox_client_id( $client_id ); - $this->settings->set_sandbox_client_secret( $client_secret ); - $this->settings->set_sandbox_merchant_id( $payee->merchant_id ); - $this->settings->set_sandbox_merchant_email( $payee->email_address ); - } else { - $this->settings->set_is_sandbox( false ); - $this->settings->set_live_client_id( $client_id ); - $this->settings->set_live_client_secret( $client_secret ); - $this->settings->set_live_merchant_id( $payee->merchant_id ); - $this->settings->set_live_merchant_email( $payee->email_address ); - } - $this->settings->save(); + $account = $this->connection_manager->get_account_details(); + $response = $this->sanitize_for_javascript( $this->response_map, $account ); - return $this->return_success( - array( - 'merchantId' => $payee->merchant_id, - 'email' => $payee->email_address, - ) - ); - } - - /** - * Retrieves the payee object with the merchant data - * by creating a minimal PayPal order. - * - * @throws Exception When failed to retrieve payee. - * - * phpcs:disable Squiz.Commenting - * phpcs:disable Generic.Commenting - * - * @param string $client_secret The client secret. - * @param bool $use_sandbox Whether to use the sandbox mode. - * @param string $client_id The client ID. - * - * @return stdClass The payee object. - */ - private function request_payee( - string $client_id, - string $client_secret, - bool $use_sandbox - ) : stdClass { - - $host = $use_sandbox ? $this->sandbox_host : $this->live_host; - - $bearer = new PayPalBearer( - new InMemoryCache(), - $host, - $client_id, - $client_secret, - $this->logger, - null - ); - - $orders = new Orders( - $host, - $bearer, - $this->logger - ); - - $request_body = array( - 'intent' => 'CAPTURE', - 'purchase_units' => array( - array( - 'amount' => array( - 'currency_code' => 'USD', - 'value' => 1.0, - ), - ), - ), - ); - - $response = $orders->create( $request_body ); - $body = json_decode( $response['body'] ); - - $order_id = $body->id; - - $order_response = $orders->order( $order_id ); - $order_body = json_decode( $order_response['body'] ); - - $pu = $order_body->purchase_units[0]; - $payee = $pu->payee; - if ( ! is_object( $payee ) ) { - throw new RuntimeException( 'Payee not found.' ); - } - if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) { - throw new RuntimeException( 'Payee info not found.' ); - } - - return $payee; + return $this->return_success( $response ); } } From 607021c9ec2cd76e6e3ca46cacd5e339e9a45754 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 18 Dec 2024 12:51:22 +0100 Subject: [PATCH 05/57] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unnecessary=20"us?= =?UTF-8?q?e"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php index 028740cb9..b2483160a 100644 --- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -12,7 +12,6 @@ namespace WooCommerce\PayPalCommerce\Settings\Service; use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; -use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; From da96c084abd12dd7a9a1fee02425014ae256a99e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 19 Dec 2024 13:23:40 +0100 Subject: [PATCH 06/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20the=20?= =?UTF-8?q?onboarding-url-generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 3 +- .../resources/js/data/common/actions.js | 14 ++++++++-- .../resources/js/data/common/controls.js | 28 ++++--------------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index ac08cdcf7..5c9c83fe5 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -20,8 +20,7 @@ export default { // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', - DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN', - DO_PRODUCTION_LOGIN: 'COMMON:DO_PRODUCTION_LOGIN', + DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index ccbf34ce0..e9d89e5f7 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -7,7 +7,7 @@ * @file */ -import { dispatch, select } from '@wordpress/data'; +import { select } from '@wordpress/data'; import ACTION_TYPES from './action-types'; import { STORE_NAME } from './constants'; @@ -151,7 +151,11 @@ export const persist = function* () { * @return {Action} The action. */ export const connectToSandbox = function* () { - return yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; + return yield { + type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, + environment: 'sandbox', + products: [ 'EXPRESS_CHECKOUT' ], + }; }; /** @@ -161,7 +165,11 @@ export const connectToSandbox = function* () { * @return {Action} The action. */ export const connectToProduction = function* ( products = [] ) { - return yield { type: ACTION_TYPES.DO_PRODUCTION_LOGIN, products }; + return yield { + type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, + environment: 'production', + products, + }; }; /** diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index a088660b9..28e93eaf4 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -31,33 +31,15 @@ export const controls = { } }, - async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() { + async [ ACTION_TYPES.DO_GENERATE_ONBOARDING_URL ]( { + products, + environment, + } ) { try { return apiFetch( { path: REST_CONNECTION_URL_PATH, method: 'POST', - data: { - environment: 'sandbox', - products: [ 'EXPRESS_CHECKOUT' ], // Sandbox always uses EXPRESS_CHECKOUT. - }, - } ); - } catch ( e ) { - return { - success: false, - error: e, - }; - } - }, - - async [ ACTION_TYPES.DO_PRODUCTION_LOGIN ]( { products } ) { - try { - return apiFetch( { - path: REST_CONNECTION_URL_PATH, - method: 'POST', - data: { - environment: 'production', - products, - }, + data: { environment, products }, } ); } catch ( e ) { return { From 1e26852aa1fe145bbcfe2f6b8ca47ee4b4067d95 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 19 Dec 2024 13:26:19 +0100 Subject: [PATCH 07/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20actions=20f?= =?UTF-8?q?or=20better=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/actions.js | 4 ++-- .../resources/js/data/common/hooks.js | 16 ++++++++-------- .../resources/js/hooks/useHandleConnections.js | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index e9d89e5f7..2b6dda353 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -150,7 +150,7 @@ export const persist = function* () { * * @return {Action} The action. */ -export const connectToSandbox = function* () { +export const sandboxOnboardingUrl = function* () { return yield { type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, environment: 'sandbox', @@ -164,7 +164,7 @@ export const connectToSandbox = function* () { * @param {string[]} products Which products/features to display in the ISU popup. * @return {Action} The action. */ -export const connectToProduction = function* ( products = [] ) { +export const productionOnboardingUrl = function* ( products = [] ) { return yield { type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, environment: 'production', diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 8eaaa3924..7057bbf2a 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -31,8 +31,8 @@ const useHooks = () => { setManualConnectionMode, setClientId, setClientSecret, - connectToSandbox, - connectToProduction, + sandboxOnboardingUrl, + productionOnboardingUrl, connectViaIdAndSecret, } = useDispatch( STORE_NAME ); @@ -77,8 +77,8 @@ const useHooks = () => { setClientSecret: ( value ) => { return savePersistent( setClientSecret, value ); }, - connectToSandbox, - connectToProduction, + sandboxOnboardingUrl, + productionOnboardingUrl, connectViaIdAndSecret, merchant, wooSettings, @@ -86,15 +86,15 @@ const useHooks = () => { }; export const useSandbox = () => { - const { isSandboxMode, setSandboxMode, connectToSandbox } = useHooks(); + const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks(); - return { isSandboxMode, setSandboxMode, connectToSandbox }; + return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl }; }; export const useProduction = () => { - const { connectToProduction } = useHooks(); + const { productionOnboardingUrl } = useHooks(); - return { connectToProduction }; + return { productionOnboardingUrl }; }; export const useManualConnection = () => { diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index d34e74f42..6686ab05f 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -118,11 +118,11 @@ const useConnectionAttempt = ( connectFn, errorMessage ) => { }; export const useSandboxConnection = () => { - const { connectToSandbox, isSandboxMode, setSandboxMode } = + const { sandboxOnboardingUrl, isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); const { withActivity } = CommonHooks.useBusyState(); const connectionAttempt = useConnectionAttempt( - connectToSandbox, + sandboxOnboardingUrl, MESSAGES.SANDBOX_ERROR ); @@ -142,11 +142,11 @@ export const useSandboxConnection = () => { }; export const useProductionConnection = () => { - const { connectToProduction } = CommonHooks.useProduction(); + const { productionOnboardingUrl } = CommonHooks.useProduction(); const { withActivity } = CommonHooks.useBusyState(); const products = OnboardingHooks.useDetermineProducts(); const connectionAttempt = useConnectionAttempt( - () => connectToProduction( products ), + () => productionOnboardingUrl( products ), MESSAGES.PRODUCTION_ERROR ); From 827e08d91fca63bce98edf023f9f5f82fbb24bb9 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 13:38:12 +0100 Subject: [PATCH 08/57] =?UTF-8?q?=E2=9C=A8=20Add=20a=20new=20local=20?= =?UTF-8?q?=E2=80=9CisBusy=E2=80=9D=20flag=20for=20busy-wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/ReusableComponents/BusyStateWrapper.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js index 959b71bfe..239a088b7 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/BusyStateWrapper.js @@ -24,6 +24,7 @@ const BusyContext = createContext( false ); * @param {boolean} props.busySpinner - Allows disabling the spinner in busy-state. * @param {string} props.className - Additional class names for the wrapper. * @param {Function} props.onBusy - Callback to process child props when busy. + * @param {boolean} props.isBusy - Optional. Additional condition to determine if the component is busy. */ const BusyStateWrapper = ( { children, @@ -31,11 +32,12 @@ const BusyStateWrapper = ( { busySpinner = true, className = '', onBusy = () => ( { disabled: true } ), + isBusy = false, } ) => { - const { isBusy } = CommonHooks.useBusyState(); + const { isBusy: globalIsBusy } = CommonHooks.useBusyState(); const hasBusyParent = useContext( BusyContext ); - const isBusyComponent = isBusy && enabled; + const isBusyComponent = ( isBusy || globalIsBusy ) && enabled; const showSpinner = busySpinner && isBusyComponent && ! hasBusyParent; const wrapperClassName = classNames( 'ppcp-r-busy-wrapper', className, { From 1c44a8105ba1975e22f53b07b1aa18f2e8f53783 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 13:38:38 +0100 Subject: [PATCH 09/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix=20the=20PayPal?= =?UTF-8?q?=20onboarding=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components/ConnectionButton.js | 82 +++++++--- .../js/hooks/useHandleConnections.js | 145 ++++++++---------- 2 files changed, 127 insertions(+), 100 deletions(-) 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 ad6a7dcef..214da3211 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 @@ -1,15 +1,59 @@ import { Button } from '@wordpress/components'; - +import { useEffect } from '@wordpress/element'; import classNames from 'classnames'; - -import { CommonHooks } from '../../../../data'; import { openSignup } from '../../../ReusableComponents/Icons'; -import { - useProductionConnection, - useSandboxConnection, -} from '../../../../hooks/useHandleConnections'; +import { useHandleOnboardingButton } from '../../../../hooks/useHandleConnections'; import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; +/** + * Button component that outputs a placeholder button when no onboardingUrl is present yet - the + * placeholder button looks identical to the working button, but has no href, target, or + * custom connection attributes. + * + * @param {Object} props + * @param {string} props.className + * @param {string} props.variant + * @param {boolean} props.showIcon + * @param {?string} props.href + * @param {Element} props.children + */ +const ButtonOrPlaceholder = ( { + className, + variant, + showIcon, + href, + children, +} ) => { + if ( ! href ) { + return ( + + ); + } + + return ( + + ); +}; + const ConnectionButton = ( { title, isSandbox = false, @@ -17,31 +61,29 @@ const ConnectionButton = ( { showIcon = true, className = '', } ) => { - const { handleSandboxConnect } = useSandboxConnection(); - const { handleProductionConnect } = useProductionConnection(); + const { onboardingUrl, scriptLoaded } = + useHandleOnboardingButton( isSandbox ); const buttonClassName = classNames( 'ppcp-r-connection-button', className, { 'sandbox-mode': isSandbox, 'live-mode': ! isSandbox, } ); - const handleConnectClick = async () => { - if ( isSandbox ) { - await handleSandboxConnect(); - } else { - await handleProductionConnect(); + useEffect( () => { + if ( scriptLoaded && onboardingUrl ) { + window.PAYPAL.apps.Signup.render(); } - }; + }, [ scriptLoaded, onboardingUrl ] ); return ( - - + ); }; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 6686ab05f..0bd14cc17 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -1,9 +1,9 @@ import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { CommonHooks, OnboardingHooks } from '../data'; -import { openPopup } from '../utils/window'; const MESSAGES = { CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ), @@ -35,32 +35,77 @@ const ACTIVITIES = { CONNECT_MANUAL: 'MANUAL_LOGIN', }; -const handlePopupWithCompletion = ( url, onError ) => { - return new Promise( ( resolve ) => { - const popup = openPopup( url ); +export const useHandleOnboardingButton = ( isSandbox ) => { + const { sandboxOnboardingUrl } = CommonHooks.useSandbox(); + const { productionOnboardingUrl } = CommonHooks.useProduction(); + const products = OnboardingHooks.useDetermineProducts(); + const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); + const [ scriptLoaded, setScriptLoaded ] = useState( false ); - if ( ! popup ) { - onError( MESSAGES.POPUP_BLOCKED ); - resolve( false ); + useEffect( () => { + const fetchOnboardingUrl = async () => { + let res; + if ( isSandbox ) { + res = await sandboxOnboardingUrl(); + } else { + res = await productionOnboardingUrl( products ); + } + + if ( res.success && res.data ) { + setOnboardingUrl( res.data ); + } else { + console.error( 'Failed to fetch onboarding URL' ); + } + }; + + fetchOnboardingUrl(); + }, [ isSandbox, productionOnboardingUrl, products, sandboxOnboardingUrl ] ); + + useEffect( () => { + /** + * The partner.js script initializes all onboarding buttons in the onload event. + * When no buttons are present, a JS error is displayed; i.e. we should load this script + * only when the button is ready (with a valid href and data-attributes). + */ + if ( ! onboardingUrl ) { return; } - // Check popup state every 500ms - const checkPopup = setInterval( () => { - if ( popup.closed ) { - clearInterval( checkPopup ); - resolve( true ); - } - }, 500 ); + const script = document.createElement( 'script' ); + script.id = 'partner-js'; + script.src = + 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js'; + script.onload = () => { + setScriptLoaded( true ); + }; + document.body.appendChild( script ); return () => { - clearInterval( checkPopup ); + /** + * When the component is unmounted, remove the partner.js script, as well as the + * dynamic scripts it loaded (signup-js and rampConfig-js) + * + * This is important, as the onboarding button is only initialized during the onload + * event of those scripts; i.e. we need to load the scripts again, when the button is + * rendered again. + */ + const onboardingScripts = [ + 'partner-js', + 'signup-js', + 'rampConfig-js', + ]; - if ( popup && ! popup.closed ) { - popup.close(); - } + onboardingScripts.forEach( ( id ) => { + const el = document.querySelector( `script[id="${ id }"]` ); + + if ( el?.parentNode ) { + el.parentNode.removeChild( el ); + } + } ); }; - } ); + }, [ onboardingUrl ] ); + + return { onboardingUrl, scriptLoaded }; }; const useConnectionBase = () => { @@ -92,75 +137,15 @@ const useConnectionBase = () => { }; }; -const useConnectionAttempt = ( connectFn, errorMessage ) => { - const { handleFailed, createErrorNotice, handleCompleted } = - useConnectionBase(); - - return async ( ...args ) => { - const res = await connectFn( ...args ); - - if ( ! res.success || ! res.data ) { - handleFailed( res, errorMessage ); - return false; - } - - const popupClosed = await handlePopupWithCompletion( - res.data, - createErrorNotice - ); - - if ( popupClosed ) { - await handleCompleted(); - } - - return popupClosed; - }; -}; - export const useSandboxConnection = () => { - const { sandboxOnboardingUrl, isSandboxMode, setSandboxMode } = - CommonHooks.useSandbox(); - const { withActivity } = CommonHooks.useBusyState(); - const connectionAttempt = useConnectionAttempt( - sandboxOnboardingUrl, - MESSAGES.SANDBOX_ERROR - ); - - const handleSandboxConnect = async () => { - return withActivity( - ACTIVITIES.CONNECT_SANDBOX, - 'Connecting to sandbox account', - connectionAttempt - ); - }; + const { isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); return { - handleSandboxConnect, isSandboxMode, setSandboxMode, }; }; -export const useProductionConnection = () => { - const { productionOnboardingUrl } = CommonHooks.useProduction(); - const { withActivity } = CommonHooks.useBusyState(); - const products = OnboardingHooks.useDetermineProducts(); - const connectionAttempt = useConnectionAttempt( - () => productionOnboardingUrl( products ), - MESSAGES.PRODUCTION_ERROR - ); - - const handleProductionConnect = async () => { - return withActivity( - ACTIVITIES.CONNECT_PRODUCTION, - 'Connecting to production account', - connectionAttempt - ); - }; - - return { handleProductionConnect }; -}; - export const useManualConnection = () => { const { handleFailed, handleCompleted, createErrorNotice } = useConnectionBase(); From 2848347537b9ec092289161b3b8f52415e56c189 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 13:49:46 +0100 Subject: [PATCH 10/57] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 4 -- .../Onboarding/Components/Navigation.js | 1 - .../resources/js/utils/window.js | 42 ------------------- 3 files changed, 47 deletions(-) delete mode 100644 modules/ppcp-settings/resources/js/utils/window.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 6aabd15fd..5685cf014 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -7,18 +7,15 @@ import { useMemo, useCallback, } from '@wordpress/element'; - import classNames from 'classnames'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; -import { CommonHooks } from '../../../../data'; import { useSandboxConnection, useManualConnection, } from '../../../../hooks/useHandleConnections'; - import ConnectionButton from './ConnectionButton'; import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; @@ -41,7 +38,6 @@ const AdvancedOptionsForm = () => { const [ clientValid, setClientValid ] = useState( false ); const [ secretValid, setSecretValid ] = useState( false ); - const { isBusy } = CommonHooks.useBusyState(); const { isSandboxMode, setSandboxMode } = useSandboxConnection(); const { handleConnectViaIdAndSecret, diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js index 3c12e1206..817b26f3e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js @@ -1,7 +1,6 @@ import { Button, Icon } from '@wordpress/components'; import { chevronLeft } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; - import classNames from 'classnames'; import { OnboardingHooks } from '../../../../data'; diff --git a/modules/ppcp-settings/resources/js/utils/window.js b/modules/ppcp-settings/resources/js/utils/window.js deleted file mode 100644 index 165874302..000000000 --- a/modules/ppcp-settings/resources/js/utils/window.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Opens the provided URL, preferably in a popup window. - * - * Popups are usually only supported on desktop devices, when the browser is not in fullscreen mode. - * - * @param {string} url - * @param {Object} options - * @param {string} options.name - * @param {number} options.width - * @param {number} options.height - * @param {boolean} options.resizeable - * @return {null|Window} Popup window instance, or null. - */ -export const openPopup = ( - url, - { name = '_blank', width = 450, height = 720, resizeable = false } = {} -) => { - width = Math.max( 100, Math.min( window.screen.width - 40, width ) ); - height = Math.max( 100, Math.min( window.screen.height - 40, height ) ); - - const left = ( window.screen.width - width ) / 2; - const top = ( window.screen.height - height ) / 2; - - const features = [ - `width=${ width }`, - `height=${ height }`, - `left=${ left }`, - `top=${ top }`, - `resizable=${ resizeable ? 'yes' : 'no' }`, - `scrollbars=yes`, - `status=no`, - ]; - - const popup = window.open( url, name, features.join( ',' ) ); - - if ( popup && ! popup.closed ) { - popup.focus(); - return popup; - } - - return null; -}; From 2b5555c96247db39b66b64377b466738ef0bca69 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 13:54:25 +0100 Subject: [PATCH 11/57] =?UTF-8?q?=E2=9E=95=20Add=20the=20@wordpress/icons?= =?UTF-8?q?=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/package.json | 1 + modules/ppcp-settings/yarn.lock | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/modules/ppcp-settings/package.json b/modules/ppcp-settings/package.json index 47e69347c..5cdb7be5c 100644 --- a/modules/ppcp-settings/package.json +++ b/modules/ppcp-settings/package.json @@ -9,6 +9,7 @@ "devDependencies": { "@wordpress/data": "^10.10.0", "@wordpress/data-controls": "^4.10.0", + "@wordpress/icons": "^10.14.0", "@wordpress/scripts": "^30.3.0", "classnames": "^2.5.1" }, diff --git a/modules/ppcp-settings/yarn.lock b/modules/ppcp-settings/yarn.lock index 6b623d53a..62a5e4ac9 100644 --- a/modules/ppcp-settings/yarn.lock +++ b/modules/ppcp-settings/yarn.lock @@ -2919,6 +2919,15 @@ sprintf-js "^1.1.1" tannin "^1.2.0" +"@wordpress/icons@^10.14.0": + version "10.14.0" + resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-10.14.0.tgz#a27298b438653a9a502eb4ee3b02b42ce516da2e" + integrity sha512-4S1AaBeqvTpsTC23y0+4WPiSyz7j+b7vJ4vQ4nqnPeBF7ZeC8J/UXWQnEuKY38n8TiutXljgagkEqGNC9pF2Mw== + dependencies: + "@babel/runtime" "7.25.7" + "@wordpress/element" "*" + "@wordpress/primitives" "*" + "@wordpress/is-shallow-equal@*": version "5.11.0" resolved "https://registry.yarnpkg.com/@wordpress/is-shallow-equal/-/is-shallow-equal-5.11.0.tgz#2f273d6d4de24a66a7a8316b770cf832d22bfc37" @@ -2968,6 +2977,15 @@ resolved "https://registry.yarnpkg.com/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz#6b3f9aa7e2698c0d78e644037c6778b5c1da12ce" integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw== +"@wordpress/primitives@*": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-4.14.0.tgz#1769f45bc541fd48be2d57626a9f6bdece39942a" + integrity sha512-IZibRVbvWoIQ+uynH0N5bmfWz83hD8lJj6jJFhSFuALK+4U5mRGg6tl0ZV0YllR6cjheD9UhTmfrAcOx+gQAjA== + dependencies: + "@babel/runtime" "7.25.7" + "@wordpress/element" "*" + clsx "^2.1.1" + "@wordpress/priority-queue@*": version "3.11.0" resolved "https://registry.yarnpkg.com/@wordpress/priority-queue/-/priority-queue-3.11.0.tgz#01e1570a7a29372bb1d07cd22fd9cbc5b5d03b09" @@ -3976,6 +3994,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" From 5b80e00c54c94f5ddb49945b6b2b6d530bc7ba77 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 13:54:46 +0100 Subject: [PATCH 12/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20the=20Con?= =?UTF-8?q?nectionButton=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components/ConnectionButton.js | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) 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 214da3211..e100881b8 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 @@ -24,34 +24,21 @@ const ButtonOrPlaceholder = ( { href, children, } ) => { - if ( ! href ) { - return ( - - ); + const buttonProps = { + className, + variant, + icon: showIcon ? openSignup : null, + }; + + if ( href ) { + buttonProps.href = href; + buttonProps.target = 'PPFrame'; + buttonProps[ 'data-paypal-button' ] = 'true'; + buttonProps[ 'data-paypal-onboard-complete' ] = 'onOnboardComplete'; + buttonProps[ 'data-paypal-onboard-button' ] = 'true'; } - return ( - - ); + return ; }; const ConnectionButton = ( { From 62dda0da3c9f165c8f74839efe1f0dadbf505a28 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 13:57:49 +0100 Subject: [PATCH 13/57] =?UTF-8?q?=E2=9C=A8=20Add=20Vaulting=20to=20onboard?= =?UTF-8?q?ing=20products?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/onboarding/selectors.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index 2e0953437..9f3a7f35d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -52,9 +52,6 @@ export const determineProducts = ( state ) => { * The store uses the Express-checkout product. */ derivedProducts.push( 'EXPRESS_CHECKOUT' ); - - // TODO: Add the "BCDC" product/feature - // Requirement: "EXPRESS_CHECKOUT with BCDC" } else { /** * Branch 3: Merchant is business, and can use CC payments. @@ -64,8 +61,7 @@ export const determineProducts = ( state ) => { } if ( canUseVaulting ) { - // TODO: Add the "Vaulting" product/feature - // Requirement: "... with Vault" + derivedProducts.push( 'ADVANCED_VAULTING' ); } return derivedProducts; From 69f6cc2e7359154331f4d7f9f2fed61ced61bcf0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 20 Dec 2024 16:09:54 +0100 Subject: [PATCH 14/57] =?UTF-8?q?=E2=9C=A8=20First=20steps=20to=20implemen?= =?UTF-8?q?t=20merchant=20authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components/ConnectionButton.js | 23 +++++++-- .../js/hooks/useHandleConnections.js | 51 +++++++++++++++++-- 2 files changed, 66 insertions(+), 8 deletions(-) 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 e100881b8..7cbc504e0 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 @@ -34,7 +34,6 @@ const ButtonOrPlaceholder = ( { buttonProps.href = href; buttonProps.target = 'PPFrame'; buttonProps[ 'data-paypal-button' ] = 'true'; - buttonProps[ 'data-paypal-onboard-complete' ] = 'onOnboardComplete'; buttonProps[ 'data-paypal-onboard-button' ] = 'true'; } @@ -48,18 +47,34 @@ const ConnectionButton = ( { showIcon = true, className = '', } ) => { - const { onboardingUrl, scriptLoaded } = - useHandleOnboardingButton( isSandbox ); + const { + onboardingUrl, + scriptLoaded, + setCompleteHandler, + removeCompleteHandler, + } = useHandleOnboardingButton( isSandbox ); const buttonClassName = classNames( 'ppcp-r-connection-button', className, { 'sandbox-mode': isSandbox, 'live-mode': ! isSandbox, } ); + const environment = isSandbox ? 'sandbox' : 'production'; useEffect( () => { if ( scriptLoaded && onboardingUrl ) { window.PAYPAL.apps.Signup.render(); + setCompleteHandler( environment ); } - }, [ scriptLoaded, onboardingUrl ] ); + + return () => { + removeCompleteHandler(); + }; + }, [ + scriptLoaded, + onboardingUrl, + environment, + setCompleteHandler, + removeCompleteHandler, + ] ); return ( diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 0bd14cc17..9d860e1c8 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -1,10 +1,13 @@ import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; -import { useState, useEffect } from '@wordpress/element'; +import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { CommonHooks, OnboardingHooks } from '../data'; +const PAYPAL_PARTNER_SDK_URL = + 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js'; + const MESSAGES = { CONNECTED: __( 'Connected to PayPal', 'woocommerce-paypal-payments' ), POPUP_BLOCKED: __( @@ -41,6 +44,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const products = OnboardingHooks.useDetermineProducts(); const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ scriptLoaded, setScriptLoaded ] = useState( false ); + const timerRef = useRef( null ); useEffect( () => { const fetchOnboardingUrl = async () => { @@ -73,8 +77,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const script = document.createElement( 'script' ); script.id = 'partner-js'; - script.src = - 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js'; + script.src = PAYPAL_PARTNER_SDK_URL; script.onload = () => { setScriptLoaded( true ); }; @@ -105,7 +108,47 @@ export const useHandleOnboardingButton = ( isSandbox ) => { }; }, [ onboardingUrl ] ); - return { onboardingUrl, scriptLoaded }; + const setCompleteHandler = useCallback( ( environment ) => { + const onComplete = ( authCode, shareId ) => { + // TODO -- finish this! + console.log( + `${ environment }-boarding complete - AUTH: `, + authCode + ); + console.log( + `${ environment }-boarding complete - SHARE:`, + shareId + ); + }; + + const addHandler = () => { + const MiniBrowser = window.PAYPAL?.apps?.Signup?.MiniBrowser; + if ( ! MiniBrowser || MiniBrowser.onOnboardComplete ) { + return; + } + + MiniBrowser.onOnboardComplete = onComplete; + }; + + // Ensure the onComplete handler is not removed by a PayPal init script. + timerRef.current = setInterval( addHandler, 250 ); + }, [] ); + + const removeCompleteHandler = useCallback( () => { + if ( timerRef.current ) { + clearInterval( timerRef.current ); + timerRef.current = null; + } + + delete window.PAYPAL?.apps?.Signup?.MiniBrowser?.onOnboardComplete; + }, [] ); + + return { + onboardingUrl, + scriptLoaded, + setCompleteHandler, + removeCompleteHandler, + }; }; const useConnectionBase = () => { From f9809bdc5312b61c49edf8f4b4ef22d1edb4f766 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 12:26:40 +0100 Subject: [PATCH 15/57] =?UTF-8?q?=F0=9F=9A=A7=20Rename=20ConnectManual=20e?= =?UTF-8?q?ndpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a preparation to include the UI login authentication in the same endpoint as the manual connection logic --- .../resources/js/data/common/constants.js | 4 ++-- modules/ppcp-settings/services.php | 6 +++--- ...RestEndpoint.php => AuthenticationRestEndpoint.php} | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) rename modules/ppcp-settings/src/Endpoint/{ConnectManualRestEndpoint.php => AuthenticationRestEndpoint.php} (91%) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index c67b1fef0..e51e311a5 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -38,11 +38,11 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; * REST path to perform the manual connection check, using client ID and secret, * * Used by: Controls - * See: ConnectManualRestEndpoint.php + * See: AuthenticateRestEndpoint.php * * @type {string} */ -export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual'; +export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/authenticate'; /** * REST path to generate an ISU URL for the PayPal-login. diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 6b676850d..7dde67edd 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -14,7 +14,7 @@ use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; -use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; @@ -79,8 +79,8 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint { - return new ConnectManualRestEndpoint( + 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint { + return new AuthenticationRestEndpoint( $container->get( 'settings.service.connection_manager' ), ); }, diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php similarity index 91% rename from modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php rename to modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index efeda393d..f6310588e 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -1,6 +1,6 @@ Date: Thu, 2 Jan 2025 12:33:58 +0100 Subject: [PATCH 16/57] =?UTF-8?q?=F0=9F=92=A1=20Explain=20diff=20between?= =?UTF-8?q?=20LoginLink=20&=20Authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/AuthenticationRestEndpoint.php | 9 ++++++++- .../ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index f6310588e..037add444 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -23,7 +23,14 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; /** - * REST controller for connection to a PayPal merchant account. + * REST controller for authenticating and connecting to a PayPal merchant account. + * + * This endpoint is responsible for verifying credentials and establishing + * a connection, regardless of whether they are provided via: + * 1. Direct login (clientId + secret) + * 2. UI-driven login (sharedId + authCode) + * + * It handles the actual authentication process after the login URL has been used. */ class AuthenticationRestEndpoint extends RestEndpoint { /** diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index 8ed204383..9ce844f6d 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -15,7 +15,14 @@ use WP_REST_Request; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; /** - * REST controller that generates merchant login URLs. + * REST controller that generates merchant login URLs for PayPal. + * + * This endpoint is responsible solely for generating a URL that initiates + * the PayPal login flow. It does not handle the authentication itself. + * + * The generated URL is typically used to redirect merchants to PayPal's login page. + * After successful login, the authentication process is completed via the + * AuthenticationRestEndpoint. */ class LoginLinkRestEndpoint extends RestEndpoint { /** From 5195748e7607f4d8e06879a7c55031d45195cbe6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 12:58:33 +0100 Subject: [PATCH 17/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Slightly=20simplify?= =?UTF-8?q?=20REST=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoint/AuthenticationRestEndpoint.php | 2 -- .../src/Endpoint/CommonRestEndpoint.php | 24 +++++-------- .../src/Endpoint/LoginLinkRestEndpoint.php | 34 +++++++++---------- .../src/Endpoint/OnboardingRestEndpoint.php | 16 ++++----- .../Endpoint/RefreshFeatureStatusEndpoint.php | 12 +++---- 5 files changed, 36 insertions(+), 52 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 037add444..648da5d28 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -90,12 +90,10 @@ class AuthenticationRestEndpoint extends RestEndpoint { register_rest_route( $this->namespace, '/' . $this->rest_base, - array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'connect_manual' ), 'permission_callback' => array( $this, 'check_permission' ), - ), ) ); } diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 7524e7e31..90a1a5e8d 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -111,11 +111,9 @@ class CommonRestEndpoint extends RestEndpoint { $this->namespace, '/' . $this->rest_base, array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_details' ), - 'permission_callback' => array( $this, 'check_permission' ), - ), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_details' ), + 'permission_callback' => array( $this, 'check_permission' ), ) ); @@ -123,11 +121,9 @@ class CommonRestEndpoint extends RestEndpoint { $this->namespace, '/' . $this->rest_base, array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_details' ), - 'permission_callback' => array( $this, 'check_permission' ), - ), + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_details' ), + 'permission_callback' => array( $this, 'check_permission' ), ) ); @@ -135,11 +131,9 @@ class CommonRestEndpoint extends RestEndpoint { $this->namespace, "/$this->rest_base/merchant", array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_merchant_details' ), - 'permission_callback' => array( $this, 'check_permission' ), - ), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_merchant_details' ), + 'permission_callback' => array( $this, 'check_permission' ), ) ); } diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index 9ce844f6d..722a20be8 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -56,25 +56,23 @@ class LoginLinkRestEndpoint extends RestEndpoint { $this->namespace, '/' . $this->rest_base, array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'get_login_url' ), - 'permission_callback' => array( $this, 'check_permission' ), - 'args' => array( - 'environment' => array( - 'required' => true, - 'type' => 'string', - ), - 'products' => array( - 'required' => true, - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'sanitize_callback' => function ( $products ) { - return array_map( 'sanitize_text_field', $products ); - }, + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'get_login_url' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'environment' => array( + 'required' => true, + 'type' => 'string', + ), + 'products' => array( + 'required' => true, + 'type' => 'array', + 'items' => array( + 'type' => 'string', ), + 'sanitize_callback' => function ( $products ) { + return array_map( 'sanitize_text_field', $products ); + }, ), ), ) diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index d4273228f..018ab2dc2 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -101,11 +101,9 @@ class OnboardingRestEndpoint extends RestEndpoint { $this->namespace, '/' . $this->rest_base, array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_details' ), - 'permission_callback' => array( $this, 'check_permission' ), - ), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_details' ), + 'permission_callback' => array( $this, 'check_permission' ), ) ); @@ -113,11 +111,9 @@ class OnboardingRestEndpoint extends RestEndpoint { $this->namespace, '/' . $this->rest_base, array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_details' ), - 'permission_callback' => array( $this, 'check_permission' ), - ), + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_details' ), + 'permission_callback' => array( $this, 'check_permission' ), ) ); } diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php index d8fc2760e..dfbfc3a3a 100644 --- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -5,7 +5,7 @@ * @package WooCommerce\PayPalCommerce\Settings\Endpoint */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; @@ -87,11 +87,9 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { $this->namespace, '/' . $this->rest_base, array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'refresh_status' ), - 'permission_callback' => array( $this, 'check_permission' ), - ), + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'refresh_status' ), + 'permission_callback' => array( $this, 'check_permission' ), ) ); } @@ -102,7 +100,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { * @param WP_REST_Request $request Full data about the request. * @return WP_REST_Response */ - public function refresh_status( WP_REST_Request $request ): WP_REST_Response { + public function refresh_status( WP_REST_Request $request ) : WP_REST_Response { $now = time(); $last_request_time = $this->cache->get( self::CACHE_KEY ) ?: 0; $seconds_missing = $last_request_time + self::TIMEOUT - $now; From ff1df84ada6950fa32493b07d19eec457f72e2cf Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 14:33:42 +0100 Subject: [PATCH 18/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20serializati?= =?UTF-8?q?on=20logic=20in=20manual=20login=20REST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoint/AuthenticationRestEndpoint.php | 59 +++++++++---------- .../src/Endpoint/RestEndpoint.php | 18 +++--- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 648da5d28..f112f6905 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -40,26 +40,6 @@ class AuthenticationRestEndpoint extends RestEndpoint { */ protected $rest_base = 'authenticate'; - /** - * Field mapping for request. - * - * @var array - */ - private array $field_map = array( - 'client_id' => array( - 'js_name' => 'clientId', - 'sanitize' => 'sanitize_text_field', - ), - 'client_secret' => array( - 'js_name' => 'clientSecret', - 'sanitize' => 'sanitize_text_field', - ), - 'use_sandbox' => array( - 'js_name' => 'useSandbox', - 'sanitize' => 'to_boolean', - ), - ); - /** * Defines the JSON response format (when connection was successful). * @@ -90,10 +70,30 @@ class AuthenticationRestEndpoint extends RestEndpoint { register_rest_route( $this->namespace, '/' . $this->rest_base, - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'connect_manual' ), - 'permission_callback' => array( $this, 'check_permission' ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'connect_manual' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'clientId' => array( + 'requires' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'minLength' => 80, + 'maxLength' => 80, + ), + 'clientSecret' => array( + 'requires' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'useSandbox' => array( + 'requires' => false, + 'type' => 'boolean', + 'default' => false, + 'sanitize_callback' => array( $this, 'to_boolean' ), + ), + ), ) ); } @@ -104,14 +104,9 @@ class AuthenticationRestEndpoint extends RestEndpoint { * @param WP_REST_Request $request Full data about the request. */ public function connect_manual( WP_REST_Request $request ) : WP_REST_Response { - $data = $this->sanitize_for_wordpress( - $request->get_params(), - $this->field_map - ); - - $client_id = $data['client_id'] ?? ''; - $client_secret = $data['client_secret'] ?? ''; - $use_sandbox = (bool) ( $data['use_sandbox'] ?? false ); + $client_id = $request->get_param( 'clientId' ); + $client_secret = $request->get_param( 'clientSecret' ); + $use_sandbox = $request->get_param( 'useSandbox' ); try { $this->connection_manager->validate_id_and_secret( $client_id, $client_secret ); diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 76626ac0c..850a70f87 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -81,7 +81,7 @@ abstract class RestEndpoint extends WC_REST_Controller { } /** - * Sanitizes parameters based on a field mapping. + * Sanitizes and renames input parameters, based on a field mapping. * * This method iterates through a field map, applying sanitization methods * to the corresponding values in the input parameters array. @@ -122,7 +122,7 @@ abstract class RestEndpoint extends WC_REST_Controller { } /** - * Sanitizes data for JavaScript based on a field mapping. + * Sanitizes and renames data for JavaScript, based on a field mapping. * * This method transforms the input data array according to the provided field map, * renaming keys to their JavaScript equivalents as specified in the mapping. @@ -151,9 +151,9 @@ abstract class RestEndpoint extends WC_REST_Controller { } /** - * Convert a value to a boolean. + * Sanitation callback: Convert a value to a boolean. * - * @param mixed $value The value to convert. + * @param mixed $value The value to sanitize. * * @return bool|null The boolean value, or null if not set. */ @@ -162,13 +162,17 @@ abstract class RestEndpoint extends WC_REST_Controller { } /** - * Convert a value to a number. + * Sanitation callback: Convert a value to a number. * - * @param mixed $value The value to convert. + * @param mixed $value The value to sanitize. * * @return int|float|null The numeric value, or null if not set. */ protected function to_number( $value ) { - return $value !== null ? ( is_numeric( $value ) ? $value + 0 : null ) : null; + if ( $value !== null ) { + $value = is_numeric( $value ) ? $value + 0 : null; + } + + return $value; } } From ef0e7e756c9d303474d0564c36c850c77a9aaf83 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 14:35:24 +0100 Subject: [PATCH 19/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20REST=20endp?= =?UTF-8?q?oint=20for=20manual=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/constants.js | 5 +++-- .../resources/js/data/common/controls.js | 4 ++-- .../src/Endpoint/AuthenticationRestEndpoint.php | 11 +++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index e51e311a5..a94a62b33 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -35,14 +35,15 @@ export const REST_HYDRATE_MERCHANT_PATH = '/wc/v3/wc_paypal/common/merchant'; export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; /** - * REST path to perform the manual connection check, using client ID and secret, + * REST path to perform the manual connection authentication, using client ID and secret. * * Used by: Controls * See: AuthenticateRestEndpoint.php * * @type {string} */ -export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/authenticate'; +export const REST_DIRECT_AUTHENTICATION_PATH = + '/wc/v3/wc_paypal/authenticate/direct'; /** * REST path to generate an ISU URL for the PayPal-login. diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 28e93eaf4..ffbc736b6 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -11,7 +11,7 @@ import apiFetch from '@wordpress/api-fetch'; import { REST_PERSIST_PATH, - REST_MANUAL_CONNECTION_PATH, + REST_DIRECT_AUTHENTICATION_PATH, REST_CONNECTION_URL_PATH, REST_HYDRATE_MERCHANT_PATH, REST_REFRESH_FEATURES_PATH, @@ -56,7 +56,7 @@ export const controls = { } ) { try { return await apiFetch( { - path: REST_MANUAL_CONNECTION_PATH, + path: REST_DIRECT_AUTHENTICATION_PATH, method: 'POST', data: { clientId, diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index f112f6905..05abff8ee 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -69,10 +69,10 @@ class AuthenticationRestEndpoint extends RestEndpoint { public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->rest_base, + '/' . $this->rest_base . '/direct', array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'connect_manual' ), + 'callback' => array( $this, 'connect_direct' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'clientId' => array( @@ -99,11 +99,14 @@ class AuthenticationRestEndpoint extends RestEndpoint { } /** - * Retrieves merchantId and email. + * Direct login: Retrieves merchantId and email using clientId and clientSecret. + * + * This is the "Manual Login" logic, when a merchant already knows their + * API credentials. * * @param WP_REST_Request $request Full data about the request. */ - public function connect_manual( WP_REST_Request $request ) : WP_REST_Response { + public function connect_direct( WP_REST_Request $request ) : WP_REST_Response { $client_id = $request->get_param( 'clientId' ); $client_secret = $request->get_param( 'clientSecret' ); $use_sandbox = $request->get_param( 'useSandbox' ); From 565ee96bb6b463e10ea1e37126ec9efa071e84a0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 14:43:33 +0100 Subject: [PATCH 20/57] =?UTF-8?q?=F0=9F=9A=A7=20First=20steps=20for=20the?= =?UTF-8?q?=20final=20ISU=20authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/constants.js | 10 ++++ .../Endpoint/AuthenticationRestEndpoint.php | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index a94a62b33..853683555 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -45,6 +45,16 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; export const REST_DIRECT_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/direct'; +/** + * REST path to perform the ISU authentication check, using shared ID and authCode. + * + * Used by: Controls + * See: AuthenticateRestEndpoint.php + * + * @type {string} + */ +export const REST_ISU_AUTHENTICATION_PATH = '/wc/v3/wc_paypal/authenticate/isu'; + /** * REST path to generate an ISU URL for the PayPal-login. * diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 05abff8ee..39ea8f398 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -96,6 +96,34 @@ class AuthenticationRestEndpoint extends RestEndpoint { ), ) ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/isu', + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'connect_isu' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'sharedId' => array( + 'requires' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'authCode' => array( + 'requires' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'useSandbox' => array( + 'requires' => false, + 'type' => 'boolean', + 'default' => false, + 'sanitize_callback' => array( $this, 'to_boolean' ), + ), + ), + ) + ); } /** @@ -123,4 +151,22 @@ class AuthenticationRestEndpoint extends RestEndpoint { return $this->return_success( $response ); } + + /** + * ISU login: Retrieves clientId and clientSecret using a sharedId and authCode. + * + * This is the final step in the UI-driven login via the ISU popup, which + * is triggered by the LoginLinkRestEndpoint URL. + * + * @param WP_REST_Request $request Full data about the request. + */ + public function connect_isu( WP_REST_Request $request ) : WP_REST_Response { + $shared_id = $request->get_param( 'sharedId' ); + $auth_code = $request->get_param( 'authCode' ); + $use_sandbox = $request->get_param( 'useSandbox' ); + + // TODO. + + return $this->return_error( 'NOT IMPLEMENTED' ); + } } From 084327c635cfe9a644d3f31fc1e41f08542dcf46 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 17:43:02 +0100 Subject: [PATCH 21/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20ambgious=20?= =?UTF-8?q?hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two different hooks and one store property shared the same name. This commit resolves the ambiguity and makes all names unique --- .../Screens/Onboarding/Components/AdvancedOptionsForm.js | 4 ++-- modules/ppcp-settings/resources/js/data/common/hooks.js | 2 +- .../ppcp-settings/resources/js/hooks/useHandleConnections.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 5685cf014..0c5f1cebe 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -14,7 +14,7 @@ import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; import { useSandboxConnection, - useManualConnection, + useDirectAuthentication, } from '../../../../hooks/useHandleConnections'; import ConnectionButton from './ConnectionButton'; import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; @@ -47,7 +47,7 @@ const AdvancedOptionsForm = () => { setClientId, clientSecret, setClientSecret, - } = useManualConnection(); + } = useDirectAuthentication(); const refClientId = useRef( null ); const refClientSecret = useRef( null ); diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 7057bbf2a..cfbf6adcc 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -97,7 +97,7 @@ export const useProduction = () => { return { productionOnboardingUrl }; }; -export const useManualConnection = () => { +export const useAuthentication = () => { const { isManualConnectionMode, setManualConnectionMode, diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 9d860e1c8..09fc22787 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -189,7 +189,7 @@ export const useSandboxConnection = () => { }; }; -export const useManualConnection = () => { +export const useDirectAuthentication = () => { const { handleFailed, handleCompleted, createErrorNotice } = useConnectionBase(); const { withActivity } = CommonHooks.useBusyState(); @@ -201,7 +201,7 @@ export const useManualConnection = () => { setClientId, clientSecret, setClientSecret, - } = CommonHooks.useManualConnection(); + } = CommonHooks.useAuthentication(); const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { return withActivity( From 605275626891a8ad98c7e6b169e0d0c0b070dfb5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 17:46:22 +0100 Subject: [PATCH 22/57] =?UTF-8?q?=F0=9F=9A=A7=20Prepare=20the=20ISU=20logi?= =?UTF-8?q?n=20completion=20JS=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/hooks/useHandleConnections.js | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 09fc22787..3c425f9c9 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -35,6 +35,7 @@ const MESSAGES = { const ACTIVITIES = { CONNECT_SANDBOX: 'ISU_LOGIN_SANDBOX', CONNECT_PRODUCTION: 'ISU_LOGIN_PRODUCTION', + CONNECT_ISU: 'ISU_LOGIN', CONNECT_MANUAL: 'MANUAL_LOGIN', }; @@ -42,6 +43,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const { sandboxOnboardingUrl } = CommonHooks.useSandbox(); const { productionOnboardingUrl } = CommonHooks.useProduction(); const products = OnboardingHooks.useDetermineProducts(); + const { withActivity } = CommonHooks.useBusyState(); const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ scriptLoaded, setScriptLoaded ] = useState( false ); const timerRef = useRef( null ); @@ -108,31 +110,49 @@ export const useHandleOnboardingButton = ( isSandbox ) => { }; }, [ onboardingUrl ] ); - const setCompleteHandler = useCallback( ( environment ) => { - const onComplete = ( authCode, shareId ) => { - // TODO -- finish this! - console.log( - `${ environment }-boarding complete - AUTH: `, - authCode - ); - console.log( - `${ environment }-boarding complete - SHARE:`, - shareId - ); - }; + const setCompleteHandler = useCallback( + ( environment ) => { + const onComplete = async ( authCode, shareId ) => { + /** + * Until now, the full page is blocked by PayPal's semi-transparent, black overlay. + * But at this point, the overlay is removed, while we process the sharedId and + * authCode via a REST call. + * + * Note: The REST response is irrelevant, since PayPal will most likely refresh this + * frame before the REST endpoint returns a value. Using "withActivity" is more of a + * visual cue to the user that something is still processing in the background. + */ + await withActivity( + ACTIVITIES.CONNECT_ISU, + 'Validating the connection details', + async () => { + // TODO -- finish this! + console.log( + `${ environment }-boarding complete - AUTH: `, + authCode + ); + console.log( + `${ environment }-boarding complete - SHARE:`, + shareId + ); + } + ); + }; - const addHandler = () => { - const MiniBrowser = window.PAYPAL?.apps?.Signup?.MiniBrowser; - if ( ! MiniBrowser || MiniBrowser.onOnboardComplete ) { - return; - } + const addHandler = () => { + const MiniBrowser = window.PAYPAL?.apps?.Signup?.MiniBrowser; + if ( ! MiniBrowser || MiniBrowser.onOnboardComplete ) { + return; + } - MiniBrowser.onOnboardComplete = onComplete; - }; + MiniBrowser.onOnboardComplete = onComplete; + }; - // Ensure the onComplete handler is not removed by a PayPal init script. - timerRef.current = setInterval( addHandler, 250 ); - }, [] ); + // Ensure the onComplete handler is not removed by a PayPal init script. + timerRef.current = setInterval( addHandler, 250 ); + }, + [ withActivity ] + ); const removeCompleteHandler = useCallback( () => { if ( timerRef.current ) { From 69d169533d2742c1978ef29669e8257cf99e03f0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 17:59:33 +0100 Subject: [PATCH 23/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Unify=20function-=20?= =?UTF-8?q?and=20hook=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/Onboarding/Components/AdvancedOptionsForm.js | 6 +++--- modules/ppcp-settings/resources/js/data/common/actions.js | 4 ++-- modules/ppcp-settings/resources/js/data/common/hooks.js | 8 ++++---- .../resources/js/hooks/useHandleConnections.js | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 0c5f1cebe..3ac56cc65 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -40,7 +40,7 @@ const AdvancedOptionsForm = () => { const { isSandboxMode, setSandboxMode } = useSandboxConnection(); const { - handleConnectViaIdAndSecret, + handleDirectAuthentication, isManualConnectionMode, setManualConnectionMode, clientId, @@ -83,10 +83,10 @@ const AdvancedOptionsForm = () => { const handleManualConnect = useCallback( () => - handleConnectViaIdAndSecret( { + handleDirectAuthentication( { validation: validateManualConnectionForm, } ), - [ handleConnectViaIdAndSecret, validateManualConnectionForm ] + [ handleDirectAuthentication, validateManualConnectionForm ] ); useEffect( () => { diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 2b6dda353..a19697e16 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -173,11 +173,11 @@ export const productionOnboardingUrl = function* ( products = [] ) { }; /** - * Side effect. Initiates a manual connection attempt using the provided client ID and secret. + * Side effect. Initiates a direct connection attempt using the provided client ID and secret. * * @return {Action} The action. */ -export const connectViaIdAndSecret = function* () { +export const connectViaSecret = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index cfbf6adcc..9e8101a76 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -33,7 +33,7 @@ const useHooks = () => { setClientSecret, sandboxOnboardingUrl, productionOnboardingUrl, - connectViaIdAndSecret, + connectViaSecret, } = useDispatch( STORE_NAME ); // Transient accessors. @@ -79,7 +79,7 @@ const useHooks = () => { }, sandboxOnboardingUrl, productionOnboardingUrl, - connectViaIdAndSecret, + connectViaSecret, merchant, wooSettings, }; @@ -105,7 +105,7 @@ export const useAuthentication = () => { setClientId, clientSecret, setClientSecret, - connectViaIdAndSecret, + connectViaSecret, } = useHooks(); return { @@ -115,7 +115,7 @@ export const useAuthentication = () => { setClientId, clientSecret, setClientSecret, - connectViaIdAndSecret, + connectViaSecret, }; }; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 3c425f9c9..2d140e6d0 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -214,7 +214,7 @@ export const useDirectAuthentication = () => { useConnectionBase(); const { withActivity } = CommonHooks.useBusyState(); const { - connectViaIdAndSecret, + connectViaSecret, isManualConnectionMode, setManualConnectionMode, clientId, @@ -223,7 +223,7 @@ export const useDirectAuthentication = () => { setClientSecret, } = CommonHooks.useAuthentication(); - const handleConnectViaIdAndSecret = async ( { validation } = {} ) => { + const handleDirectAuthentication = async ( { validation } = {} ) => { return withActivity( ACTIVITIES.CONNECT_MANUAL, 'Connecting manually via Client ID and Secret', @@ -237,7 +237,7 @@ export const useDirectAuthentication = () => { } } - const res = await connectViaIdAndSecret(); + const res = await connectViaSecret(); if ( res.success ) { await handleCompleted(); @@ -251,7 +251,7 @@ export const useDirectAuthentication = () => { }; return { - handleConnectViaIdAndSecret, + handleDirectAuthentication, isManualConnectionMode, setManualConnectionMode, clientId, From 1246a02f07c88b2faed1263a4504d04f8bea8ed7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 18:19:41 +0100 Subject: [PATCH 24/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adjust=20naming=20to?= =?UTF-8?q?=20new=20=E2=80=9Cauthentication=E2=80=9D=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/action-types.js | 2 +- modules/ppcp-settings/resources/js/data/common/actions.js | 2 +- modules/ppcp-settings/resources/js/data/common/controls.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 5c9c83fe5..60db34c30 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -19,7 +19,7 @@ export default { // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', - DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', + DO_MANUAL_AUTHENTICATION: 'COMMON:DO_MANUAL_AUTHENTICATION', DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES', diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index a19697e16..80edd13dc 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -182,7 +182,7 @@ export const connectViaSecret = function* () { yield select( STORE_NAME ).persistentData(); return yield { - type: ACTION_TYPES.DO_MANUAL_CONNECTION, + type: ACTION_TYPES.DO_MANUAL_AUTHENTICATION, clientId, clientSecret, useSandbox, diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index ffbc736b6..075dfebe3 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -49,7 +49,7 @@ export const controls = { } }, - async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( { + async [ ACTION_TYPES.DO_MANUAL_AUTHENTICATION ]( { clientId, clientSecret, useSandbox, From 0d5832aa8b945796c44be2465db4ae435a4cfe6d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 18:22:09 +0100 Subject: [PATCH 25/57] =?UTF-8?q?=E2=9C=A8=20Add=20React=20logic=20for=20f?= =?UTF-8?q?inal=20ISU=20authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 1 + .../resources/js/data/common/actions.js | 28 +++++++++++++++++++ .../resources/js/data/common/controls.js | 24 ++++++++++++++++ .../resources/js/data/common/hooks.js | 4 +++ .../js/hooks/useHandleConnections.js | 15 ++++------ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 60db34c30..229a01c6d 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -20,6 +20,7 @@ export default { // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', DO_MANUAL_AUTHENTICATION: 'COMMON:DO_MANUAL_AUTHENTICATION', + DO_ISU_AUTHENTICATION: 'COMMON:DO_ISU_AUTHENTICATION', DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES', diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 80edd13dc..dbf27a174 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -189,6 +189,34 @@ export const connectViaSecret = function* () { }; }; +/** + * Side effect. Completes the ISU login by authenticating the user via the one time sharedId and + * authCode provided by PayPal. + * + * This action accepts parameters instead of fetching data from the Redux state because all + * parameters are dynamically generated during the authentication process, and not managed by our + * Redux store. + * + * @param {string} sharedId - One-time authentication ID that PayPal "shares" with us. + * @param {string} authCode - Matching one-time authentication code to validate the login. + * @param {string} environment - [production|sandbox]. + * @return {Action} The action. + */ +export const connectViaAuthCode = function* ( + sharedId, + authCode, + environment +) { + const useSandbox = 'sandbox' === environment; + + return yield { + type: ACTION_TYPES.DO_ISU_AUTHENTICATION, + sharedId, + authCode, + useSandbox, + }; +}; + /** * Side effect. Clears and refreshes the merchant data via a REST request. * diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 075dfebe3..870bca49d 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -15,6 +15,7 @@ import { REST_CONNECTION_URL_PATH, REST_HYDRATE_MERCHANT_PATH, REST_REFRESH_FEATURES_PATH, + REST_ISU_AUTHENTICATION_PATH, } from './constants'; import ACTION_TYPES from './action-types'; @@ -72,6 +73,29 @@ export const controls = { } }, + async [ ACTION_TYPES.DO_ISU_AUTHENTICATION ]( { + sharedId, + authCode, + useSandbox, + } ) { + try { + return await apiFetch( { + path: REST_ISU_AUTHENTICATION_PATH, + method: 'POST', + data: { + sharedId, + authCode, + useSandbox, + }, + } ); + } catch ( e ) { + return { + success: false, + error: e, + }; + } + }, + async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() { try { return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } ); diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 9e8101a76..32ab33e6f 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -34,6 +34,7 @@ const useHooks = () => { sandboxOnboardingUrl, productionOnboardingUrl, connectViaSecret, + connectViaAuthCode, } = useDispatch( STORE_NAME ); // Transient accessors. @@ -80,6 +81,7 @@ const useHooks = () => { sandboxOnboardingUrl, productionOnboardingUrl, connectViaSecret, + connectViaAuthCode, merchant, wooSettings, }; @@ -106,6 +108,7 @@ export const useAuthentication = () => { clientSecret, setClientSecret, connectViaSecret, + connectViaAuthCode, } = useHooks(); return { @@ -116,6 +119,7 @@ export const useAuthentication = () => { clientSecret, setClientSecret, connectViaSecret, + connectViaAuthCode, }; }; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 2d140e6d0..6ad4c8a90 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -44,6 +44,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const { productionOnboardingUrl } = CommonHooks.useProduction(); const products = OnboardingHooks.useDetermineProducts(); const { withActivity } = CommonHooks.useBusyState(); + const { connectViaAuthCode } = CommonHooks.useAuthentication(); const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ scriptLoaded, setScriptLoaded ] = useState( false ); const timerRef = useRef( null ); @@ -112,7 +113,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const setCompleteHandler = useCallback( ( environment ) => { - const onComplete = async ( authCode, shareId ) => { + const onComplete = async ( authCode, sharedId ) => { /** * Until now, the full page is blocked by PayPal's semi-transparent, black overlay. * But at this point, the overlay is removed, while we process the sharedId and @@ -126,14 +127,10 @@ export const useHandleOnboardingButton = ( isSandbox ) => { ACTIVITIES.CONNECT_ISU, 'Validating the connection details', async () => { - // TODO -- finish this! - console.log( - `${ environment }-boarding complete - AUTH: `, - authCode - ); - console.log( - `${ environment }-boarding complete - SHARE:`, - shareId + await connectViaAuthCode( + authCode, + sharedId, + environment ); } ); From de3ab2c479473df048a41af6120bfc2a9ecc7edf Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 18:22:53 +0100 Subject: [PATCH 26/57] =?UTF-8?q?=F0=9F=92=AC=20Add=20missing=20prefix=20t?= =?UTF-8?q?o=20an=20unrelated=20action-type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/action-types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 229a01c6d..d607dc96e 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -23,5 +23,5 @@ export default { DO_ISU_AUTHENTICATION: 'COMMON:DO_ISU_AUTHENTICATION', DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', - DO_REFRESH_FEATURES: 'DO_REFRESH_FEATURES', + DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES', }; From 143bcd75a11e4105563240ce8fc7b50ce68dfabe Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 18:52:47 +0100 Subject: [PATCH 27/57] =?UTF-8?q?=F0=9F=92=A1=20Add=20comments=20to=20docu?= =?UTF-8?q?ment=20auth=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Service/ConnectionManager.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index 1cffb155c..1554d5045 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -87,6 +87,8 @@ class ConnectionManager { /** * Checks if the provided ID and secret have a valid format. * + * Part of the "Direct Connection" (Manual Connection) flow. + * * On failure, an Exception is thrown, while a successful check does not * generate any return value. * @@ -113,6 +115,8 @@ class ConnectionManager { * Disconnects the current merchant, and then attempts to connect to a * PayPal account using a client ID and secret. * + * Part of the "Direct Connection" (Manual Connection) flow. + * * @param bool $use_sandbox Whether to use the sandbox mode. * @param string $client_id The client ID. * @param string $client_secret The client secret. From 0cc451b66b907026bbd9c2f6ba924d3fc0dc3b0e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 18:54:03 +0100 Subject: [PATCH 28/57] =?UTF-8?q?=F0=9F=9A=A7=20Start=20with=20server-side?= =?UTF-8?q?=20ISU=20authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoint/AuthenticationRestEndpoint.php | 12 ++++- .../src/Service/ConnectionManager.php | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 39ea8f398..22cd6f472 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -165,8 +165,16 @@ class AuthenticationRestEndpoint extends RestEndpoint { $auth_code = $request->get_param( 'authCode' ); $use_sandbox = $request->get_param( 'useSandbox' ); - // TODO. + try { + $this->connection_manager->validate_id_and_auth_code( $shared_id, $auth_code ); + $this->connection_manager->connect_via_auth_code( $use_sandbox, $shared_id, $auth_code ); + } catch ( Exception $exception ) { + return $this->return_error( $exception->getMessage() ); + } - return $this->return_error( 'NOT IMPLEMENTED' ); + $account = $this->connection_manager->get_account_details(); + $response = $this->sanitize_for_javascript( $this->response_map, $account ); + + return $this->return_success( $response ); } } diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index 1554d5045..dbbf0d8fc 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -140,6 +140,57 @@ class ConnectionManager { } + /** + * Checks if the provided ID and auth-code have a valid format. + * + * Part of the "ISU Connection" (login via Popup) flow. + * + * On failure, an Exception is thrown, while a successful check does not + * generate any return value. Note, that we did not find official documentation + * on those values, so we only check if they are non-empty strings. + * + * @param string $shared_id The shared onboarding ID. + * @param string $auth_code The authorization code. + * @return void + * @throws RuntimeException When invalid shared ID or auth provided. + */ + public function validate_id_and_auth_code( string $shared_id, string $auth_code ) : void { + if ( empty( $shared_id ) ) { + throw new RuntimeException( 'No onboarding ID provided.' ); + } + + if ( empty( $auth_code ) ) { + throw new RuntimeException( 'No authorization code provided.' ); + } + } + + /** + * Disconnects the current merchant, and then attempts to connect to a + * PayPal account the onboarding ID and authorization ID. + * + * Part of the "ISU Connection" (login via Popup) flow. + * + * @param bool $use_sandbox Whether to use the sandbox mode. + * @param string $shared_id The shared onboarding ID. + * @param string $auth_code The authorization code. + * @return void + * @throws RuntimeException When failed to retrieve payee. + */ + public function connect_via_auth_code( bool $use_sandbox, string $shared_id, string $auth_code ) : void { + $this->disconnect(); + + $this->logger->info( + 'Attempting ISU login to PayPal...', + array( + 'sandbox' => $use_sandbox, + 'shared_id' => $shared_id, + ) + ); + + // TODO ... + } + + // ---------------------------------------------------------------------------- // Internal helper methods From 67a3fe034bc43332a320f4a00f0e8e97cdb37061 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 20:39:16 +0100 Subject: [PATCH 29/57] =?UTF-8?q?=F0=9F=90=9B=20Fix=20REST=20argument=20sa?= =?UTF-8?q?nitation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/AuthenticationRestEndpoint.php | 13 ++++++------- modules/ppcp-settings/src/Endpoint/RestEndpoint.php | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 22cd6f472..f3c26c15f 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -76,19 +76,19 @@ class AuthenticationRestEndpoint extends RestEndpoint { 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'clientId' => array( - 'requires' => true, + 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'minLength' => 80, 'maxLength' => 80, ), 'clientSecret' => array( - 'requires' => true, + 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'useSandbox' => array( - 'requires' => false, + 'required' => false, 'type' => 'boolean', 'default' => false, 'sanitize_callback' => array( $this, 'to_boolean' ), @@ -106,19 +106,18 @@ class AuthenticationRestEndpoint extends RestEndpoint { 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'sharedId' => array( - 'requires' => true, + 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'authCode' => array( - 'requires' => true, + 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'useSandbox' => array( - 'requires' => false, + 'default' => 0, 'type' => 'boolean', - 'default' => false, 'sanitize_callback' => array( $this, 'to_boolean' ), ), ), diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 850a70f87..6f1eb0e4f 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -157,7 +157,7 @@ abstract class RestEndpoint extends WC_REST_Controller { * * @return bool|null The boolean value, or null if not set. */ - protected function to_boolean( $value ) : ?bool { + public function to_boolean( $value ) : ?bool { return $value !== null ? (bool) $value : null; } @@ -168,7 +168,7 @@ abstract class RestEndpoint extends WC_REST_Controller { * * @return int|float|null The numeric value, or null if not set. */ - protected function to_number( $value ) { + public function to_number( $value ) { if ( $value !== null ) { $value = is_numeric( $value ) ? $value + 0 : null; } From e2b169aff49d1c69175e0fbd3e994a1bb74d6548 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 2 Jan 2025 20:40:11 +0100 Subject: [PATCH 30/57] =?UTF-8?q?=F0=9F=9A=A7=20Authentication=20progress,?= =?UTF-8?q?=20still=20non-functional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 4 + .../src/Service/ConnectionManager.php | 107 +++++++++++++++--- 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 7dde67edd..9bdc289c9 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; +use WooCommerce\PayPalCommerce\Settings\Service\EnvironmentConfig; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -195,6 +196,9 @@ return array( $container->get( 'settings.data.common' ), $container->get( 'api.paypal-host-production' ), $container->get( 'api.paypal-host-sandbox' ), + $container->get( 'api.endpoint.login-seller-production' ), + $container->get( 'api.endpoint.login-seller-sandbox' ), + $container->get( 'api.repository.partner-referrals-data' ), $container->get( 'woocommerce.logger.woocommerce' ), ); }, diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index dbbf0d8fc..c2d8292cc 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -11,10 +11,15 @@ namespace WooCommerce\PayPalCommerce\Settings\Service; use Psr\Log\LoggerInterface; use RuntimeException; +use JsonException; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; +use WooCommerce\WooCommerce\Logging\Logger\NullLogger; +use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; +use Automattic\Jetpack\Partner; /** * Class that manages the connection to PayPal. @@ -35,27 +40,58 @@ class ConnectionManager { private LoggerInterface $logger; /** - * Base URLs for the manual connection attempt. + * Base URLs for the manual connection attempt, by environment. * * @var array */ private array $connection_hosts; + /** + * Login API handler instances, by environment. + * + * @var array + */ + private array $login_endpoints; + + /** + * Onboarding referrals data. + * + * @var PartnerReferralsData + */ + private PartnerReferralsData $referrals_data; + /** * Constructor. * - * @param CommonSettings $common_settings Data model that stores the connection details. - * @param string $live_host The API host for the live mode. - * @param string $sandbox_host The API host for the sandbox mode. - * @param LoggerInterface $logger Logging instance. + * @param CommonSettings $common_settings Data model that stores the connection + * details. + * @param string $live_host The API host for the live mode. + * @param string $sandbox_host The API host for the sandbox mode. + * @param LoginSeller $live_login_endpoint API handler to fetch live-merchant + * credentials. + * @param LoginSeller $sandbox_login_endpoint API handler to fetch sandbox-merchant + * credentials. + * @param PartnerReferralsData $referrals_data Partner referrals data. + * @param ?LoggerInterface $logger Logging instance. */ - public function __construct( CommonSettings $common_settings, string $live_host, string $sandbox_host, LoggerInterface $logger ) { - $this->common_settings = $common_settings; - $this->logger = $logger; + public function __construct( + CommonSettings $common_settings, string $live_host, string $sandbox_host, + LoginSeller $live_login_endpoint, LoginSeller $sandbox_login_endpoint, + PartnerReferralsData $referrals_data, + ?LoggerInterface $logger = null + ) { + $this->common_settings = $common_settings; + $this->logger = $logger ?: new NullLogger(); + $this->connection_hosts = array( 'live' => $live_host, 'sandbox' => $sandbox_host, ); + $this->login_endpoints = array( + 'live' => $live_login_endpoint, + 'sandbox' => $sandbox_login_endpoint, + ); + $this->referrals_data = $referrals_data; } /** @@ -187,7 +223,10 @@ class ConnectionManager { ) ); - // TODO ... + $credentials = $this->get_credentials( $shared_id, $auth_code, $use_sandbox ); + + // TODO. + // $this->update_connection_details( $use_sandbox, $payee['merchant_id'], $payee['email_address'] ); } @@ -205,9 +244,21 @@ class ConnectionManager { return $for_sandbox ? $this->connection_hosts['sandbox'] : $this->connection_hosts['live']; } + /** + * Returns an API handler to fetch merchant credentials. + * + * @param bool $for_sandbox Whether to return the sandbox API handler. + * @return LoginSeller + */ + private function get_login_endpoint( bool $for_sandbox = false ) : LoginSeller { + return $for_sandbox ? $this->login_endpoints['sandbox'] : $this->login_endpoints['live']; + } + /** * Retrieves the payee object with the merchant data by creating a minimal PayPal order. * + * Part of the "Direct Connection" (Manual Connection) flow. + * * @param string $client_id The client ID. * @param string $client_secret The client secret. * @param bool $use_sandbox Whether to use the sandbox mode. @@ -249,13 +300,16 @@ class ConnectionManager { ), ); - $response = $orders->create( $request_body ); - $body = json_decode( $response['body'] ); + try { + $response = $orders->create( $request_body ); + $body = json_decode( $response['body'], false, 512, JSON_THROW_ON_ERROR ); + $order_id = $body->id; - $order_id = $body->id; - - $order_response = $orders->order( $order_id ); - $order_body = json_decode( $order_response['body'] ); + $order_response = $orders->order( $order_id ); + $order_body = json_decode( $order_response['body'], false, 512, JSON_THROW_ON_ERROR ); + } catch ( JsonException $exception ) { + throw new RuntimeException( 'Could not decode JSON response: ' . $exception->getMessage() ); + } $pu = $order_body->purchase_units[0]; $payee = $pu->payee; @@ -273,6 +327,28 @@ class ConnectionManager { ); } + /** + * Fetches merchant API credentials using a shared onboarding ID and + * authorization code. + * + * Part of the "ISU Connection" (login via Popup) flow. + * + * @param string $shared_id The shared onboarding ID. + * @param string $auth_code The authorization code. + * @param bool $use_sandbox Whether to use the sandbox mode. + * @return array + */ + private function get_credentials( string $shared_id, string $auth_code, bool $use_sandbox ) : array { + $login_handler = $this->get_login_endpoint( $use_sandbox ); + $nonce = $this->referrals_data->nonce(); + + // TODO. Always throws the exception "No token found." + $response = $login_handler->credentials_for( $shared_id, $auth_code, $nonce ); + + // TODO. + return (array) $response; + } + /** * Stores the provided details in the data model. * @@ -294,4 +370,5 @@ class ConnectionManager { $this->common_settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); $this->common_settings->save(); } + } From ed13064a169929d9cbd707bdcf7140b94d449c91 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 3 Jan 2025 12:12:25 +0100 Subject: [PATCH 31/57] =?UTF-8?q?=E2=9C=A8=20Create=20new=20environment-co?= =?UTF-8?q?nfig-container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Helper/EnvironmentConfig.php | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 modules/ppcp-wc-gateway/src/Helper/EnvironmentConfig.php diff --git a/modules/ppcp-wc-gateway/src/Helper/EnvironmentConfig.php b/modules/ppcp-wc-gateway/src/Helper/EnvironmentConfig.php new file mode 100644 index 000000000..1542de783 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Helper/EnvironmentConfig.php @@ -0,0 +1,79 @@ +production_value = $production_value; + $this->sandbox_value = $sandbox_value; + } + + /** + * Factory method to create a validated EnvironmentConfig. + * + * @template U + * @param string $data_type Expected type for the values (class name or primitive type). + * @param U $production_value Value for production environment. + * @param U $sandbox_value Value for the sandbox environment. + * @return self + */ + public static function create( string $data_type, $production_value, $sandbox_value ) : self { + assert( + gettype( $production_value ) === $data_type || $production_value instanceof $data_type, + "Production value must be of type '$data_type'" + ); + assert( + gettype( $sandbox_value ) === $data_type || $sandbox_value instanceof $data_type, + "Sandbox value must be of type '$data_type'" + ); + + return new self( $production_value, $sandbox_value ); + } + + /** + * Get the value for the specified environment. + * + * @param bool $for_sandbox Whether to get the sandbox value. + * @return T The value for the specified environment. + */ + public function get_value( bool $for_sandbox = false ) { + return $for_sandbox ? $this->sandbox_value : $this->production_value; + } +} From a1f80f1d3df4319091b94444ff17ddeda4895d57 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 3 Jan 2025 12:22:06 +0100 Subject: [PATCH 32/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20to=20use?= =?UTF-8?q?=20the=20EnvironmentConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/services.php | 17 ++++ modules/ppcp-settings/services.php | 6 +- .../src/Service/ConnectionManager.php | 78 ++++++------------- 3 files changed, 42 insertions(+), 59 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 280438b80..8665c3c75 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -79,6 +79,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; +use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; return array( 'api.host' => function( ContainerInterface $container ) : string { @@ -879,4 +880,20 @@ return array( 'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string { return CONNECT_WOO_SANDBOX_MERCHANT_ID; }, + 'api.env.paypal-host' => static function ( ContainerInterface $container ) : EnvironmentConfig { + /** @type EnvironmentConfig Configuration object */ + return EnvironmentConfig::create( + 'string', + $container->get( 'api.paypal-host-production' ), + $container->get( 'api.paypal-host-sandbox' ) + ); + }, + 'api.env.endpoint.login-seller' => static function ( ContainerInterface $container ) : EnvironmentConfig { + /** @type EnvironmentConfig Configuration object */ + return EnvironmentConfig::create( + LoginSeller::class, + $container->get( 'api.endpoint.login-seller-production' ), + $container->get( 'api.endpoint.login-seller-sandbox' ) + ); + }, ); diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index fc80dd6a0..98a37542b 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -201,10 +201,8 @@ return array( 'settings.service.connection_manager' => static function ( ContainerInterface $container ) : ConnectionManager { return new ConnectionManager( $container->get( 'settings.data.common' ), - $container->get( 'api.paypal-host-production' ), - $container->get( 'api.paypal-host-sandbox' ), - $container->get( 'api.endpoint.login-seller-production' ), - $container->get( 'api.endpoint.login-seller-sandbox' ), + $container->get( 'api.env.paypal-host' ), + $container->get( 'api.env.endpoint.login-seller' ), $container->get( 'api.repository.partner-referrals-data' ), $container->get( 'woocommerce.logger.woocommerce' ), ); diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index c2d8292cc..58303d2a6 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -9,17 +9,17 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Service; +use JsonException; use Psr\Log\LoggerInterface; use RuntimeException; -use JsonException; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; -use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; -use WooCommerce\WooCommerce\Logging\Logger\NullLogger; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; -use Automattic\Jetpack\Partner; +use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; +use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; +use WooCommerce\WooCommerce\Logging\Logger\NullLogger; /** * Class that manages the connection to PayPal. @@ -42,16 +42,16 @@ class ConnectionManager { /** * Base URLs for the manual connection attempt, by environment. * - * @var array + * @var EnvironmentConfig */ - private array $connection_hosts; + private EnvironmentConfig $connection_host; /** * Login API handler instances, by environment. * - * @var array + * @var EnvironmentConfig */ - private array $login_endpoints; + private EnvironmentConfig $login_endpoint; /** * Onboarding referrals data. @@ -63,35 +63,24 @@ class ConnectionManager { /** * Constructor. * - * @param CommonSettings $common_settings Data model that stores the connection - * details. - * @param string $live_host The API host for the live mode. - * @param string $sandbox_host The API host for the sandbox mode. - * @param LoginSeller $live_login_endpoint API handler to fetch live-merchant - * credentials. - * @param LoginSeller $sandbox_login_endpoint API handler to fetch sandbox-merchant - * credentials. - * @param PartnerReferralsData $referrals_data Partner referrals data. - * @param ?LoggerInterface $logger Logging instance. + * @param CommonSettings $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 ?LoggerInterface $logger Logging instance. */ public function __construct( - CommonSettings $common_settings, string $live_host, string $sandbox_host, - LoginSeller $live_login_endpoint, LoginSeller $sandbox_login_endpoint, + CommonSettings $common_settings, + EnvironmentConfig $connection_host, + EnvironmentConfig $login_endpoint, PartnerReferralsData $referrals_data, ?LoggerInterface $logger = null ) { $this->common_settings = $common_settings; + $this->connection_host = $connection_host; + $this->login_endpoint = $login_endpoint; + $this->referrals_data = $referrals_data; $this->logger = $logger ?: new NullLogger(); - - $this->connection_hosts = array( - 'live' => $live_host, - 'sandbox' => $sandbox_host, - ); - $this->login_endpoints = array( - 'live' => $live_login_endpoint, - 'sandbox' => $sandbox_login_endpoint, - ); - $this->referrals_data = $referrals_data; } /** @@ -226,7 +215,6 @@ class ConnectionManager { $credentials = $this->get_credentials( $shared_id, $auth_code, $use_sandbox ); // TODO. - // $this->update_connection_details( $use_sandbox, $payee['merchant_id'], $payee['email_address'] ); } @@ -234,26 +222,6 @@ class ConnectionManager { // Internal helper methods - /** - * Returns the API host for the relevant environment. - * - * @param bool $for_sandbox Whether to return the sandbox API host. - * @return string - */ - private function get_host( bool $for_sandbox = false ) : string { - return $for_sandbox ? $this->connection_hosts['sandbox'] : $this->connection_hosts['live']; - } - - /** - * Returns an API handler to fetch merchant credentials. - * - * @param bool $for_sandbox Whether to return the sandbox API handler. - * @return LoginSeller - */ - private function get_login_endpoint( bool $for_sandbox = false ) : LoginSeller { - return $for_sandbox ? $this->login_endpoints['sandbox'] : $this->login_endpoints['live']; - } - /** * Retrieves the payee object with the merchant data by creating a minimal PayPal order. * @@ -271,7 +239,7 @@ class ConnectionManager { string $client_secret, bool $use_sandbox ) : array { - $host = $this->get_host( $use_sandbox ); + $host = $this->connection_host->get_value( $use_sandbox ); $bearer = new PayPalBearer( new InMemoryCache(), @@ -339,10 +307,10 @@ class ConnectionManager { * @return array */ private function get_credentials( string $shared_id, string $auth_code, bool $use_sandbox ) : array { - $login_handler = $this->get_login_endpoint( $use_sandbox ); + $login_handler = $this->login_endpoint->get_value( $use_sandbox ); $nonce = $this->referrals_data->nonce(); - // TODO. Always throws the exception "No token found." + // TODO. Always throws the exception "No token found.". $response = $login_handler->credentials_for( $shared_id, $auth_code, $nonce ); // TODO. From 3f691bea012d327606e06032e204bd3a160cd81d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 3 Jan 2025 13:48:26 +0100 Subject: [PATCH 33/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20EnvironmentCon?= =?UTF-8?q?fig=20in=20ConnectionUrlGenerator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/services.php | 8 +++ .../resources/js/data/common/actions.js | 4 +- .../resources/js/data/common/controls.js | 4 +- modules/ppcp-settings/services.php | 32 +++-------- .../src/Endpoint/LoginLinkRestEndpoint.php | 36 +++++------- .../src/Service/ConnectionUrlGenerator.php | 55 +++++++------------ 6 files changed, 54 insertions(+), 85 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 8665c3c75..ab58194a7 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -896,4 +896,12 @@ return array( $container->get( 'api.endpoint.login-seller-sandbox' ) ); }, + 'api.env.endpoint.partner-referrals' => static function ( ContainerInterface $container ) : EnvironmentConfig { + /** @type EnvironmentConfig Configuration object */ + return EnvironmentConfig::create( + PartnerReferrals::class, + $container->get( 'api.endpoint.partner-referrals-production' ), + $container->get( 'api.endpoint.partner-referrals-sandbox' ) + ); + }, ); diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 83e252d92..a8291787b 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -153,7 +153,7 @@ export const persist = function* () { export const sandboxOnboardingUrl = function* () { return yield { type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, - environment: 'sandbox', + useSandbox: true, products: [ 'EXPRESS_CHECKOUT' ], }; }; @@ -167,7 +167,7 @@ export const sandboxOnboardingUrl = function* () { export const productionOnboardingUrl = function* ( products = [] ) { return yield { type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, - environment: 'production', + useSandbox: false, products, }; }; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index cbe851982..dac31147c 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -36,13 +36,13 @@ export const controls = { async [ ACTION_TYPES.DO_GENERATE_ONBOARDING_URL ]( { products, - environment, + useSandbox, } ) { try { return apiFetch( { path: REST_CONNECTION_URL_PATH, method: 'POST', - data: { environment, products }, + data: { useSandbox, products }, } ); } catch ( e ) { return { diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 98a37542b..f19dc750f 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -87,7 +87,7 @@ return array( }, 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint { return new LoginLinkRestEndpoint( - $container->get( 'settings.service.connection-url-generators' ), + $container->get( 'settings.service.connection-url-generator' ), ); }, 'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint { @@ -172,31 +172,13 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array { - // Define available environments. - $environments = array( - 'production' => array( - 'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-production' ), - ), - 'sandbox' => array( - 'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-sandbox' ), - ), + 'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator { + return new ConnectionUrlGenerator( + $container->get( 'api.env.endpoint.partner-referrals' ), + $container->get( 'api.repository.partner-referrals-data' ), + $container->get( 'settings.service.onboarding-url-manager' ), + $container->get( 'woocommerce.logger.woocommerce' ) ); - - $generators = array(); - - // Instantiate URL generators for each environment. - foreach ( $environments as $environment => $config ) { - $generators[ $environment ] = new ConnectionUrlGenerator( - $config['partner_referrals'], - $container->get( 'api.repository.partner-referrals-data' ), - $environment, - $container->get( 'settings.service.onboarding-url-manager' ), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - } - - return $generators; }, 'settings.service.connection_manager' => static function ( ContainerInterface $container ) : ConnectionManager { return new ConnectionManager( diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index 722a20be8..7ddee27a5 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -33,25 +33,25 @@ class LoginLinkRestEndpoint extends RestEndpoint { protected $rest_base = 'login_link'; /** - * Link generator list, with environment name as array key. + * Login-URL generator. * - * @var ConnectionUrlGenerator[] + * @var ConnectionUrlGenerator */ - protected array $url_generators; + protected ConnectionUrlGenerator $url_generator; /** * Constructor. * - * @param ConnectionUrlGenerator[] $url_generators Array of environment-specific URL generators. + * @param ConnectionUrlGenerator $url_generator Login-URL generator. */ - public function __construct( array $url_generators ) { - $this->url_generators = $url_generators; + public function __construct( ConnectionUrlGenerator $url_generator ) { + $this->url_generator = $url_generator; } /** * Configure REST API routes. */ - public function register_routes() { + public function register_routes() : void { register_rest_route( $this->namespace, '/' . $this->rest_base, @@ -60,11 +60,12 @@ class LoginLinkRestEndpoint extends RestEndpoint { 'callback' => array( $this, 'get_login_url' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( - 'environment' => array( - 'required' => true, - 'type' => 'string', + 'useSandbox' => array( + 'default' => 0, + 'type' => 'boolean', + 'sanitize_callback' => array( $this, 'to_boolean' ), ), - 'products' => array( + 'products' => array( 'required' => true, 'type' => 'array', 'items' => array( @@ -87,20 +88,11 @@ class LoginLinkRestEndpoint extends RestEndpoint { * @return WP_REST_Response The login URL or an error response. */ public function get_login_url( WP_REST_Request $request ) : WP_REST_Response { - $environment = $request->get_param( 'environment' ); + $use_sandbox = $request->get_param( 'useSandbox' ); $products = $request->get_param( 'products' ); - if ( ! isset( $this->url_generators[ $environment ] ) ) { - return new WP_REST_Response( - array( 'error' => 'Invalid environment specified.' ), - 400 - ); - } - - $url_generator = $this->url_generators[ $environment ]; - try { - $url = $url_generator->generate( $products ); + $url = $this->url_generator->generate( $products, $use_sandbox ); return $this->return_success( $url ); } catch ( \Exception $e ) { diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php index b2483160a..62ee92dff 100644 --- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; +use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; // TODO: Replace the OnboardingUrl with a new implementation for this module. use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; @@ -25,9 +26,9 @@ class ConnectionUrlGenerator { /** * The partner referrals endpoint. * - * @var PartnerReferrals + * @var EnvironmentConfig */ - protected PartnerReferrals $partner_referrals; + protected EnvironmentConfig $partner_referrals; /** * The default partner referrals data. @@ -43,13 +44,6 @@ class ConnectionUrlGenerator { */ protected OnboardingUrlManager $url_manager; - /** - * Which environment is used for the connection URL. - * - * @var string - */ - protected string $environment = ''; - /** * The logger * @@ -62,36 +56,23 @@ class ConnectionUrlGenerator { * * Initializes the cache and logger properties of the class. * - * @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation. + * @param EnvironmentConfig $partner_referrals PartnerReferrals for URL generation. * @param PartnerReferralsData $referrals_data Default partner referrals data. - * @param string $environment Environment that is used to generate the URL. - * ['production'|'sandbox']. * @param OnboardingUrlManager $url_manager Manages access to OnboardingUrl instances. * @param ?LoggerInterface $logger The logger object for logging messages. */ public function __construct( - PartnerReferrals $partner_referrals, + EnvironmentConfig $partner_referrals, PartnerReferralsData $referrals_data, - string $environment, OnboardingUrlManager $url_manager, ?LoggerInterface $logger = null ) { $this->partner_referrals = $partner_referrals; $this->referrals_data = $referrals_data; - $this->environment = $environment; $this->url_manager = $url_manager; $this->logger = $logger ?: new NullLogger(); } - /** - * Returns the environment for which the URL is being generated. - * - * @return string - */ - public function environment() : string { - return $this->environment; - } - /** * Generates a PayPal onboarding URL for merchant sign-up. * @@ -99,13 +80,14 @@ class ConnectionUrlGenerator { * It handles caching of the URL, generation of new URLs when necessary, * and works for both production and sandbox environments. * - * @param array $products An array of product identifiers to include in the sign-up process. - * These determine the PayPal onboarding experience. + * @param array $products An array of product identifiers to include in the sign-up process. + * These determine the PayPal onboarding experience. + * @param bool $use_sandbox Whether to generate a sandbox URL. * * @return string The generated PayPal onboarding URL. */ - public function generate( array $products = array() ) : string { - $cache_key = $this->cache_key( $products ); + public function generate( array $products = array(), bool $use_sandbox = false ) : string { + $cache_key = $this->cache_key( $products, $use_sandbox ); $user_id = get_current_user_id(); $onboarding_url = $this->url_manager->get( $cache_key, $user_id ); $cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key ); @@ -118,7 +100,7 @@ class ConnectionUrlGenerator { $this->logger->info( 'Generating onboarding URL for: ' . $cache_key ); - $url = $this->generate_new_url( $products, $onboarding_url, $cache_key ); + $url = $this->generate_new_url( $use_sandbox, $products, $onboarding_url, $cache_key ); if ( $url ) { $this->persist_url( $onboarding_url, $url ); @@ -130,15 +112,18 @@ class ConnectionUrlGenerator { /** * Generates a cache key from the environment and sorted product array. * - * @param array $products Product identifiers that are part of the cache key. + * @param array $products Product identifiers that are part of the cache key. + * @param bool $for_sandbox Whether the cache contains a sandbox URL. * * @return string The cache key, defining the product list and environment. */ - protected function cache_key( array $products = array() ) : string { + protected function cache_key( array $products, bool $for_sandbox ) : string { + $environment = $for_sandbox ? 'sandbox' : 'production'; + // Sort products alphabetically, to improve cache implementation. sort( $products ); - return $this->environment() . '-' . implode( '-', $products ); + return $environment . '-' . implode( '-', $products ); } /** @@ -167,13 +152,14 @@ class ConnectionUrlGenerator { /** * Generates a new URL. * + * @param bool $for_sandbox Whether to generate a sandbox URL. * @param array $products The products array. * @param OnboardingUrl $onboarding_url The OnboardingUrl object. * @param string $cache_key The cache key. * * @return string The generated URL or an empty string on failure. */ - protected function generate_new_url( array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string { + protected function generate_new_url( bool $for_sandbox, array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string { $query_args = array( 'displayMode' => 'minibrowser' ); $onboarding_url->init(); @@ -188,7 +174,8 @@ class ConnectionUrlGenerator { $data = $this->prepare_referral_data( $products, $onboarding_token ); try { - $url = $this->partner_referrals->signup_link( $data ); + $referral = $this->partner_referrals->get_value( $for_sandbox ); + $url = $referral->signup_link( $data ); } catch ( Exception $e ) { $this->logger->warning( 'Could not generate an onboarding URL for: ' . $cache_key ); From e4c80d7dbc4f8d1f3e9d08cf6fe92cc02d253745 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 3 Jan 2025 18:26:01 +0100 Subject: [PATCH 34/57] =?UTF-8?q?=E2=9C=A8=20Prepare=20authentication=20ho?= =?UTF-8?q?oks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/ConnectionManager.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index 58303d2a6..8ee63efea 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -11,7 +11,7 @@ namespace WooCommerce\PayPalCommerce\Settings\Service; use JsonException; use Psr\Log\LoggerInterface; -use RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; @@ -107,6 +107,12 @@ class ConnectionManager { $this->common_settings->reset_merchant_data(); $this->common_settings->save(); + + /** + * Broadcast, that the plugin disconnected from PayPal. This allows other + * modules to clean up merchant-related details, such as eligibility flags. + */ + do_action( 'woocommerce_paypal_payments_merchant_disconnected' ); } /** @@ -305,6 +311,7 @@ class ConnectionManager { * @param string $auth_code The authorization code. * @param bool $use_sandbox Whether to use the sandbox mode. * @return array + * @throws RuntimeException When failed to fetch credentials. */ private function get_credentials( string $shared_id, string $auth_code, bool $use_sandbox ) : array { $login_handler = $this->login_endpoint->get_value( $use_sandbox ); @@ -337,6 +344,12 @@ class ConnectionManager { $this->common_settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); $this->common_settings->save(); - } + /** + * Broadcast that the plugin connected to a new PayPal merchant account. + * This is the right time to initialize merchant relative flags for the + * first time. + */ + do_action( 'woocommerce_paypal_payments_authenticated_merchant' ); + } } From b86ae2b1c7f39e9fcf99bda9b773917e58177d9d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 3 Jan 2025 18:31:52 +0100 Subject: [PATCH 35/57] =?UTF-8?q?=F0=9F=93=9D=20Add=20notes=20on=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/ConnectionManager.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index 8ee63efea..96af4cb23 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -318,6 +318,21 @@ class ConnectionManager { $nonce = $this->referrals_data->nonce(); // TODO. Always throws the exception "No token found.". + /* + * Maybe the problem is not with the `credentials_for()` call, but something + * that is wrong with the ConnectionUrl? + * + * The LoginSellerEndpoint class uses the same code as this class: + >> $credentials = $endpoint->credentials_for( + >> $data['sharedId'], + >> $data['authCode'], + >> $this->partner_referrals_data->nonce() + >> ); + * + * PayPal's API response is 401, not because OAuth the onboarding ID and + * authorization code are invalid, but because they cannot be matched to + * this website/merchant. An intermediary step might be missing. + */ $response = $login_handler->credentials_for( $shared_id, $auth_code, $nonce ); // TODO. From ca2aa4eff11a948be19411fe7ef30eddc0f11c86 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 7 Jan 2025 17:29:53 +0100 Subject: [PATCH 36/57] =?UTF-8?q?=F0=9F=A9=B9=20Add=20missing=20dependency?= =?UTF-8?q?=20to=20useCallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/hooks/useHandleConnections.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 6ad4c8a90..b15c33b32 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -148,7 +148,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { // Ensure the onComplete handler is not removed by a PayPal init script. timerRef.current = setInterval( addHandler, 250 ); }, - [ withActivity ] + [ connectViaAuthCode, withActivity ] ); const removeCompleteHandler = useCallback( () => { From 4e7d89fd2626b39e893b0ad01c76a158b903958c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 13:21:30 +0100 Subject: [PATCH 37/57] =?UTF-8?q?=F0=9F=92=A1=20Improve=20a=20code=20comme?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index a8291787b..c73719186 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -197,8 +197,8 @@ export const connectViaSecret = function* () { * parameters are dynamically generated during the authentication process, and not managed by our * Redux store. * - * @param {string} sharedId - One-time authentication ID that PayPal "shares" with us. - * @param {string} authCode - Matching one-time authentication code to validate the login. + * @param {string} sharedId - OAuth client ID, provided via "sharedId" during onboarding. + * @param {string} authCode - OAuth authorization code provided during onboarding. * @param {string} environment - [production|sandbox]. * @return {Action} The action. */ From 650586ce7ca41c48d2ff269776a27614d15f4248 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 13:21:46 +0100 Subject: [PATCH 38/57] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=20authentication?= =?UTF-8?q?=20bug!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/hooks/useHandleConnections.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index b15c33b32..111139638 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -128,8 +128,8 @@ export const useHandleOnboardingButton = ( isSandbox ) => { 'Validating the connection details', async () => { await connectViaAuthCode( - authCode, sharedId, + authCode, environment ); } From d4fbde1b139f356183b5333d7833b8871696869b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 14:39:06 +0100 Subject: [PATCH 39/57] =?UTF-8?q?=F0=9F=9A=A7=20Clean=20up=20WIP=20code=20?= =?UTF-8?q?in=20ConnectionManager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/ManualConnectionForm.js | 0 .../Components/SandboxConnectionForm.js | 41 +++++++++++++++++++ .../src/Service/ConnectionManager.php | 27 ++++-------- 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js new file mode 100644 index 000000000..e69de29bb diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js new file mode 100644 index 000000000..5323fb311 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js @@ -0,0 +1,41 @@ +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; +import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; +import { __ } from '@wordpress/i18n'; +import ConnectionButton from './ConnectionButton'; +import { useSandboxConnection } from '../../../../hooks/useHandleConnections'; + +const SandboxLoginSection = () => { + const { isSandboxMode, setSandboxMode } = useSandboxConnection(); + + return ( + + + + + + ); +}; + +export default SandboxLoginSection; diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index 96af4cb23..b773919bb 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -202,8 +202,8 @@ class ConnectionManager { * Part of the "ISU Connection" (login via Popup) flow. * * @param bool $use_sandbox Whether to use the sandbox mode. - * @param string $shared_id The shared onboarding ID. - * @param string $auth_code The authorization code. + * @param string $shared_id The OAuth client ID. + * @param string $auth_code The OAuth authorization code. * @return void * @throws RuntimeException When failed to retrieve payee. */ @@ -317,26 +317,13 @@ class ConnectionManager { $login_handler = $this->login_endpoint->get_value( $use_sandbox ); $nonce = $this->referrals_data->nonce(); - // TODO. Always throws the exception "No token found.". - /* - * Maybe the problem is not with the `credentials_for()` call, but something - * that is wrong with the ConnectionUrl? - * - * The LoginSellerEndpoint class uses the same code as this class: - >> $credentials = $endpoint->credentials_for( - >> $data['sharedId'], - >> $data['authCode'], - >> $this->partner_referrals_data->nonce() - >> ); - * - * PayPal's API response is 401, not because OAuth the onboarding ID and - * authorization code are invalid, but because they cannot be matched to - * this website/merchant. An intermediary step might be missing. - */ $response = $login_handler->credentials_for( $shared_id, $auth_code, $nonce ); - // TODO. - return (array) $response; + return array( + 'client_id' => (string) ( $response->client_id ?? '' ), + 'client_secret' => (string) ( $response->client_secret ?? '' ), + 'merchant_id' => (string) ( $response->payer_id ?? '' ), + ); } /** From ac68aa79687e4e8384a7ec3696fd977f31606b7a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 14:41:27 +0100 Subject: [PATCH 40/57] =?UTF-8?q?=E2=9C=A8=20New=20Redux=20properties=20fo?= =?UTF-8?q?r=20manual=20connection=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/actions.js | 22 +++++++++++++++ .../resources/js/data/onboarding/hooks.js | 28 +++++++++++++++++++ .../resources/js/data/onboarding/reducer.js | 2 ++ 3 files changed, 52 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index dcf401995..e9bf8ed5f 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -47,6 +47,28 @@ export const setIsReady = ( isReady ) => ( { payload: { isReady }, } ); +/** + * Transient. Sets the "manualClientId" value. + * + * @param {string} manualClientId + * @return {Action} The action. + */ +export const setManualClientId = ( manualClientId ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { manualClientId }, +} ); + +/** + * Transient. Sets the "manualClientSecret" value. + * + * @param {string} manualClientSecret + * @return {Action} The action. + */ +export const setManualClientSecret = ( manualClientSecret ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { manualClientSecret }, +} ); + /** * Persistent.Set the "onboarding completed" flag which shows or hides the wizard. * diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index e8582821e..c4308c0fa 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -30,6 +30,8 @@ const useHooks = () => { setStep, setCompleted, setIsCasualSeller, + setManualClientId, + setManualClientSecret, setAreOptionalPaymentMethodsEnabled, setProducts, } = useDispatch( STORE_NAME ); @@ -43,6 +45,8 @@ const useHooks = () => { // Transient accessors. const isReady = useTransient( 'isReady' ); + const manualClientId = useTransient( 'manualClientId' ); + const manualClientSecret = useTransient( 'manualClientSecret' ); // Persistent accessors. const step = usePersistent( 'step' ); @@ -73,6 +77,14 @@ const useHooks = () => { setIsCasualSeller: ( value ) => { return savePersistent( setIsCasualSeller, value ); }, + manualClientId, + setManualClientId: ( value ) => { + return savePersistent( setManualClientId, value ); + }, + manualClientSecret, + setManualClientSecret: ( value ) => { + return savePersistent( setManualClientSecret, value ); + }, areOptionalPaymentMethodsEnabled, setAreOptionalPaymentMethodsEnabled: ( value ) => { return savePersistent( setAreOptionalPaymentMethodsEnabled, value ); @@ -88,6 +100,22 @@ const useHooks = () => { }; }; +export const useManualConnectionForm = () => { + const { + manualClientId, + setManualClientId, + manualClientSecret, + setManualClientSecret, + } = useHooks(); + + return { + manualClientId, + setManualClientId, + manualClientSecret, + setManualClientSecret, + }; +}; + export const useBusiness = () => { const { isCasualSeller, setIsCasualSeller } = useHooks(); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 2b16e2416..8d03f9fbf 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -14,6 +14,8 @@ import ACTION_TYPES from './action-types'; const defaultTransient = Object.freeze( { isReady: false, + manualClientId: '', + manualClientSecret: '', // Read only values, provided by the server. flags: Object.freeze( { From 1bf6e488a358b91e225a4981b214b7ff93937e7d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 14:59:59 +0100 Subject: [PATCH 41/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Apply=20more=20accur?= =?UTF-8?q?ate=20authentication=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - “authenticate” instead of “connect” - “withCredentials” instead of “direct” - “OAuth” instead of “ISU” --- .../resources/js/data/common/action-types.js | 4 ++-- .../resources/js/data/common/actions.js | 10 +++++----- .../resources/js/data/common/controls.js | 4 ++-- .../resources/js/data/common/hooks.js | 16 ++++++++-------- .../resources/js/hooks/useHandleConnections.js | 10 +++++----- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js index 76bfb66f0..8ae56b20c 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -19,8 +19,8 @@ export default { // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', - DO_MANUAL_AUTHENTICATION: 'COMMON:DO_MANUAL_AUTHENTICATION', - DO_ISU_AUTHENTICATION: 'COMMON:DO_ISU_AUTHENTICATION', + DO_DIRECT_API_AUTHENTICATION: 'COMMON:DO_DIRECT_API_AUTHENTICATION', + DO_OAUTH_AUTHENTICATION: 'COMMON:DO_OAUTH_AUTHENTICATION', DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL', DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT', DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES', diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index c73719186..f559d53c7 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -177,12 +177,12 @@ export const productionOnboardingUrl = function* ( products = [] ) { * * @return {Action} The action. */ -export const connectViaSecret = function* () { +export const authenticateWithCredentials = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); return yield { - type: ACTION_TYPES.DO_MANUAL_AUTHENTICATION, + type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION, clientId, clientSecret, useSandbox, @@ -197,12 +197,12 @@ export const connectViaSecret = function* () { * parameters are dynamically generated during the authentication process, and not managed by our * Redux store. * - * @param {string} sharedId - OAuth client ID, provided via "sharedId" during onboarding. + * @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID. * @param {string} authCode - OAuth authorization code provided during onboarding. * @param {string} environment - [production|sandbox]. * @return {Action} The action. */ -export const connectViaAuthCode = function* ( +export const authenticateWithOAuth = function* ( sharedId, authCode, environment @@ -210,7 +210,7 @@ export const connectViaAuthCode = function* ( const useSandbox = 'sandbox' === environment; return yield { - type: ACTION_TYPES.DO_ISU_AUTHENTICATION, + type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION, sharedId, authCode, useSandbox, diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index dac31147c..62d4a8f84 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -52,7 +52,7 @@ export const controls = { } }, - async [ ACTION_TYPES.DO_MANUAL_AUTHENTICATION ]( { + async [ ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION ]( { clientId, clientSecret, useSandbox, @@ -75,7 +75,7 @@ export const controls = { } }, - async [ ACTION_TYPES.DO_ISU_AUTHENTICATION ]( { + async [ ACTION_TYPES.DO_OAUTH_AUTHENTICATION ]( { sharedId, authCode, useSandbox, diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 715b42b65..844c375e4 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -32,8 +32,8 @@ const useHooks = () => { setClientSecret, sandboxOnboardingUrl, productionOnboardingUrl, - connectViaSecret, - connectViaAuthCode, + authenticateWithCredentials, + authenticateWithOAuth, startWebhookSimulation, checkWebhookSimulationState, } = useDispatch( STORE_NAME ); @@ -81,8 +81,8 @@ const useHooks = () => { }, sandboxOnboardingUrl, productionOnboardingUrl, - connectViaSecret, - connectViaAuthCode, + authenticateWithCredentials, + authenticateWithOAuth, merchant, wooSettings, webhooks, @@ -111,8 +111,8 @@ export const useAuthentication = () => { setClientId, clientSecret, setClientSecret, - connectViaSecret, - connectViaAuthCode, + authenticateWithCredentials, + authenticateWithOAuth, } = useHooks(); return { @@ -122,8 +122,8 @@ export const useAuthentication = () => { setClientId, clientSecret, setClientSecret, - connectViaSecret, - connectViaAuthCode, + authenticateWithCredentials, + authenticateWithOAuth, }; }; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 111139638..66be18d24 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -44,7 +44,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const { productionOnboardingUrl } = CommonHooks.useProduction(); const products = OnboardingHooks.useDetermineProducts(); const { withActivity } = CommonHooks.useBusyState(); - const { connectViaAuthCode } = CommonHooks.useAuthentication(); + const { authenticateWithOAuth } = CommonHooks.useAuthentication(); const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ scriptLoaded, setScriptLoaded ] = useState( false ); const timerRef = useRef( null ); @@ -127,7 +127,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { ACTIVITIES.CONNECT_ISU, 'Validating the connection details', async () => { - await connectViaAuthCode( + await authenticateWithOAuth( sharedId, authCode, environment @@ -148,7 +148,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { // Ensure the onComplete handler is not removed by a PayPal init script. timerRef.current = setInterval( addHandler, 250 ); }, - [ connectViaAuthCode, withActivity ] + [ authenticateWithOAuth, withActivity ] ); const removeCompleteHandler = useCallback( () => { @@ -211,7 +211,7 @@ export const useDirectAuthentication = () => { useConnectionBase(); const { withActivity } = CommonHooks.useBusyState(); const { - connectViaSecret, + authenticateWithCredentials, isManualConnectionMode, setManualConnectionMode, clientId, @@ -234,7 +234,7 @@ export const useDirectAuthentication = () => { } } - const res = await connectViaSecret(); + const res = await authenticateWithCredentials(); if ( res.success ) { await handleCompleted(); From 54b174e4470a465e00c3156a4f41dfcfb8428efd Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 15:01:40 +0100 Subject: [PATCH 42/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Decouple=20API=20aut?= =?UTF-8?q?hentication=20from=20Redux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/actions.js | 26 +++++++++------ .../js/hooks/useHandleConnections.js | 33 +++++++++++-------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index f559d53c7..91ef41cfc 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -175,12 +175,20 @@ export const productionOnboardingUrl = function* ( products = [] ) { /** * Side effect. Initiates a direct connection attempt using the provided client ID and secret. * + * This action accepts parameters instead of fetching data from the Redux state because the + * values (ID and secret) are not managed by a central redux store, but might come from private + * component state. + * + * @param {string} clientId - AP client ID (always 80-characters, starting with "A"). + * @param {string} clientSecret - API client secret. + * @param {boolean} useSandbox - Whether the credentials are for a sandbox account. * @return {Action} The action. */ -export const authenticateWithCredentials = function* () { - const { clientId, clientSecret, useSandbox } = - yield select( STORE_NAME ).persistentData(); - +export const authenticateWithCredentials = function* ( + clientId, + clientSecret, + useSandbox +) { return yield { type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION, clientId, @@ -197,18 +205,16 @@ export const authenticateWithCredentials = function* () { * parameters are dynamically generated during the authentication process, and not managed by our * Redux store. * - * @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID. - * @param {string} authCode - OAuth authorization code provided during onboarding. - * @param {string} environment - [production|sandbox]. + * @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID. + * @param {string} authCode - OAuth authorization code provided during onboarding. + * @param {boolean} useSandbox - Whether the credentials are for a sandbox account. * @return {Action} The action. */ export const authenticateWithOAuth = function* ( sharedId, authCode, - environment + useSandbox ) { - const useSandbox = 'sandbox' === environment; - return yield { type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION, sharedId, diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index 66be18d24..f6837c488 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -130,7 +130,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { await authenticateWithOAuth( sharedId, authCode, - environment + 'sandbox' === environment ); } ); @@ -214,27 +214,38 @@ export const useDirectAuthentication = () => { authenticateWithCredentials, isManualConnectionMode, setManualConnectionMode, - clientId, - setClientId, - clientSecret, - setClientSecret, } = CommonHooks.useAuthentication(); - const handleDirectAuthentication = async ( { validation } = {} ) => { + const handleDirectAuthentication = async ( connectionDetails ) => { return withActivity( ACTIVITIES.CONNECT_MANUAL, 'Connecting manually via Client ID and Secret', async () => { - if ( 'function' === typeof validation ) { + let data; + + if ( 'function' === typeof connectionDetails ) { try { - validation(); + data = connectionDetails(); } catch ( exception ) { createErrorNotice( exception.message ); return; } + } else if ( 'object' === typeof connectionDetails ) { + data = connectionDetails; } - const res = await authenticateWithCredentials(); + if ( ! data || ! data.clientId || ! data.clientSecret ) { + createErrorNotice( + 'Invalid connection details (clientID or clientSecret missing)' + ); + return; + } + + const res = await authenticateWithCredentials( + data.clientId, + data.clientSecret, + !! data.isSandbox + ); if ( res.success ) { await handleCompleted(); @@ -251,9 +262,5 @@ export const useDirectAuthentication = () => { handleDirectAuthentication, isManualConnectionMode, setManualConnectionMode, - clientId, - setClientId, - clientSecret, - setClientSecret, }; }; From 0752436f00dc6c16372e01b874ea124f4ef52d21 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 15:01:47 +0100 Subject: [PATCH 43/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Split=20the=20advanc?= =?UTF-8?q?ed=20form=20into=202=20sub-components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 199 +----------------- .../Components/ManualConnectionForm.js | 188 +++++++++++++++++ .../Components/SandboxConnectionForm.js | 9 +- 3 files changed, 197 insertions(+), 199 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js index 3ac56cc65..4d1891735 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/AdvancedOptionsForm.js @@ -1,204 +1,13 @@ -import { __, sprintf } from '@wordpress/i18n'; -import { Button, TextControl } from '@wordpress/components'; -import { - useRef, - useState, - useEffect, - useMemo, - useCallback, -} from '@wordpress/element'; -import classNames from 'classnames'; - -import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; -import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; -import { - useSandboxConnection, - useDirectAuthentication, -} from '../../../../hooks/useHandleConnections'; -import ConnectionButton from './ConnectionButton'; -import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; - -const FORM_ERRORS = { - noClientId: __( - 'Please enter your Client ID', - 'woocommerce-paypal-payments' - ), - noClientSecret: __( - 'Please enter your Secret Key', - 'woocommerce-paypal-payments' - ), - invalidClientId: __( - 'Please enter a valid Client ID', - 'woocommerce-paypal-payments' - ), -}; +import SandboxConnectionForm from './SandboxConnectionForm'; +import ManualConnectionForm from './ManualConnectionForm'; const AdvancedOptionsForm = () => { - const [ clientValid, setClientValid ] = useState( false ); - const [ secretValid, setSecretValid ] = useState( false ); - - const { isSandboxMode, setSandboxMode } = useSandboxConnection(); - const { - handleDirectAuthentication, - isManualConnectionMode, - setManualConnectionMode, - clientId, - setClientId, - clientSecret, - setClientSecret, - } = useDirectAuthentication(); - - const refClientId = useRef( null ); - const refClientSecret = useRef( null ); - - const validateManualConnectionForm = useCallback( () => { - const checks = [ - { - ref: refClientId, - valid: () => clientId, - errorMessage: FORM_ERRORS.noClientId, - }, - { - ref: refClientId, - valid: () => clientValid, - errorMessage: FORM_ERRORS.invalidClientId, - }, - { - ref: refClientSecret, - valid: () => clientSecret && secretValid, - errorMessage: FORM_ERRORS.noClientSecret, - }, - ]; - - for ( const { ref, valid, errorMessage } of checks ) { - if ( valid() ) { - continue; - } - - ref?.current?.focus(); - throw new Error( errorMessage ); - } - }, [ clientId, clientSecret, clientValid, secretValid ] ); - - const handleManualConnect = useCallback( - () => - handleDirectAuthentication( { - validation: validateManualConnectionForm, - } ), - [ handleDirectAuthentication, validateManualConnectionForm ] - ); - - useEffect( () => { - setClientValid( ! clientId || /^A[\w-]{79}$/.test( clientId ) ); - setSecretValid( clientSecret && clientSecret.length > 0 ); - }, [ clientId, clientSecret ] ); - - const clientIdLabel = useMemo( - () => - isSandboxMode - ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' ) - : __( 'Live Client ID', 'woocommerce-paypal-payments' ), - [ isSandboxMode ] - ); - - const secretKeyLabel = useMemo( - () => - isSandboxMode - ? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ) - : __( 'Live Secret Key', 'woocommerce-paypal-payments' ), - [ isSandboxMode ] - ); - - const advancedUsersDescription = sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, click here.', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input' - ); - return ( <> - - - - - + - ( { - disabled: true, - label: props.label + ' ...', - } ) } - > - - - { clientValid || ( -

- { FORM_ERRORS.invalidClientId } -

- ) } - - -
-
+ ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js index e69de29bb..ca0257159 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ManualConnectionForm.js @@ -0,0 +1,188 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from '@wordpress/element'; +import { Button, TextControl } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import classNames from 'classnames'; + +import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; +import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; +import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; +import { + useDirectAuthentication, + useSandboxConnection, +} from '../../../../hooks/useHandleConnections'; +import { OnboardingHooks } from '../../../../data'; + +const FORM_ERRORS = { + noClientId: __( + 'Please enter your Client ID', + 'woocommerce-paypal-payments' + ), + noClientSecret: __( + 'Please enter your Secret Key', + 'woocommerce-paypal-payments' + ), + invalidClientId: __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ), +}; + +const ManualConnectionForm = () => { + const [ clientValid, setClientValid ] = useState( false ); + const [ secretValid, setSecretValid ] = useState( false ); + const { isSandboxMode } = useSandboxConnection(); + const { + manualClientId, + setManualClientId, + manualClientSecret, + setManualClientSecret, + } = OnboardingHooks.useManualConnectionForm(); + const { + handleDirectAuthentication, + isManualConnectionMode, + setManualConnectionMode, + } = useDirectAuthentication(); + const refClientId = useRef( null ); + const refClientSecret = useRef( null ); + + // Form data validation and sanitation. + const getManualConnectionDetails = useCallback( () => { + const checks = [ + { + ref: refClientId, + valid: () => manualClientId, + errorMessage: FORM_ERRORS.noClientId, + }, + { + ref: refClientId, + valid: () => clientValid, + errorMessage: FORM_ERRORS.invalidClientId, + }, + { + ref: refClientSecret, + valid: () => manualClientSecret && secretValid, + errorMessage: FORM_ERRORS.noClientSecret, + }, + ]; + + for ( const { ref, valid, errorMessage } of checks ) { + if ( valid() ) { + continue; + } + + ref?.current?.focus(); + throw new Error( errorMessage ); + } + + return { + clientId: manualClientId, + clientSecret: manualClientSecret, + isSandbox: isSandboxMode, + }; + }, [ + manualClientId, + manualClientSecret, + isSandboxMode, + clientValid, + secretValid, + ] ); + + // On-the-fly form validation. + useEffect( () => { + setClientValid( + ! manualClientId || /^A[\w-]{79}$/.test( manualClientId ) + ); + setSecretValid( manualClientSecret && manualClientSecret.length > 0 ); + }, [ manualClientId, manualClientSecret ] ); + + // Environment-specific field labels. + const clientIdLabel = useMemo( + () => + isSandboxMode + ? __( 'Sandbox Client ID', 'woocommerce-paypal-payments' ) + : __( 'Live Client ID', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); + + const secretKeyLabel = useMemo( + () => + isSandboxMode + ? __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ) + : __( 'Live Secret Key', 'woocommerce-paypal-payments' ), + [ isSandboxMode ] + ); + + // Translations with placeholders. + const advancedUsersDescription = sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'For advanced users: Connect a custom PayPal REST app for full control over your integration. For more information on creating a PayPal REST application, click here.', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input' + ); + + // Button click handler. + const handleManualConnect = useCallback( + () => handleDirectAuthentication( getManualConnectionDetails ), + [ handleDirectAuthentication, getManualConnectionDetails ] + ); + + return ( + ( { + disabled: true, + label: props.label + ' ...', + } ) } + > + + + { clientValid || ( +

+ { FORM_ERRORS.invalidClientId } +

+ ) } + + +
+
+ ); +}; + +export default ManualConnectionForm; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js index 5323fb311..39b115b9d 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/SandboxConnectionForm.js @@ -1,10 +1,11 @@ +import { __ } from '@wordpress/i18n'; + import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; -import { __ } from '@wordpress/i18n'; -import ConnectionButton from './ConnectionButton'; import { useSandboxConnection } from '../../../../hooks/useHandleConnections'; +import ConnectionButton from './ConnectionButton'; -const SandboxLoginSection = () => { +const SandboxConnectionForm = () => { const { isSandboxMode, setSandboxMode } = useSandboxConnection(); return ( @@ -38,4 +39,4 @@ const SandboxLoginSection = () => { ); }; -export default SandboxLoginSection; +export default SandboxConnectionForm; From 6167955374d83802b339f3733310c38e25c12792 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 15:07:37 +0100 Subject: [PATCH 44/57] =?UTF-8?q?=F0=9F=94=A5=20Remove=20setters=20for=20c?= =?UTF-8?q?lientId/secret?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Those values should only be set by PHP after validating some authentication details --- .../resources/js/data/common/actions.js | 22 ------------------- .../resources/js/data/common/hooks.js | 20 ----------------- .../resources/js/data/common/reducer.js | 4 ++-- 3 files changed, 2 insertions(+), 44 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 91ef41cfc..0cdec5d31 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -112,28 +112,6 @@ export const setManualConnectionMode = ( useManualConnection ) => ( { payload: { useManualConnection }, } ); -/** - * Persistent. Changes the "client ID" value. - * - * @param {string} clientId - * @return {Action} The action. - */ -export const setClientId = ( clientId ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { clientId }, -} ); - -/** - * Persistent. Changes the "client secret" value. - * - * @param {string} clientSecret - * @return {Action} The action. - */ -export const setClientSecret = ( clientSecret ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { clientSecret }, -} ); - /** * Side effect. Saves the persistent details to the WP database. * diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 844c375e4..8c22494ca 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -28,8 +28,6 @@ const useHooks = () => { persist, setSandboxMode, setManualConnectionMode, - setClientId, - setClientSecret, sandboxOnboardingUrl, productionOnboardingUrl, authenticateWithCredentials, @@ -42,8 +40,6 @@ const useHooks = () => { const isReady = useTransient( 'isReady' ); // Persistent accessors. - const clientId = usePersistent( 'clientId' ); - const clientSecret = usePersistent( 'clientSecret' ); const isSandboxMode = usePersistent( 'useSandbox' ); const isManualConnectionMode = usePersistent( 'useManualConnection' ); const webhooks = usePersistent( 'webhooks' ); @@ -71,14 +67,6 @@ const useHooks = () => { setManualConnectionMode: ( state ) => { return savePersistent( setManualConnectionMode, state ); }, - clientId, - setClientId: ( value ) => { - return savePersistent( setClientId, value ); - }, - clientSecret, - setClientSecret: ( value ) => { - return savePersistent( setClientSecret, value ); - }, sandboxOnboardingUrl, productionOnboardingUrl, authenticateWithCredentials, @@ -107,10 +95,6 @@ export const useAuthentication = () => { const { isManualConnectionMode, setManualConnectionMode, - clientId, - setClientId, - clientSecret, - setClientSecret, authenticateWithCredentials, authenticateWithOAuth, } = useHooks(); @@ -118,10 +102,6 @@ export const useAuthentication = () => { return { isManualConnectionMode, setManualConnectionMode, - clientId, - setClientId, - clientSecret, - setClientSecret, authenticateWithCredentials, authenticateWithOAuth, }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 8b5cfb9b3..922db6985 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -22,6 +22,8 @@ const defaultTransient = Object.freeze( { isSandbox: false, id: '', email: '', + clientId: '', + clientSecret: '', } ), wooSettings: Object.freeze( { @@ -33,8 +35,6 @@ const defaultTransient = Object.freeze( { const defaultPersistent = Object.freeze( { useSandbox: false, useManualConnection: false, - clientId: '', - clientSecret: '', webhooks: [], } ); From bd7bbb36b27df85a643679182cb621a210ad36ad Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 15:19:50 +0100 Subject: [PATCH 45/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Prevent=20upading=20?= =?UTF-8?q?clientId/secret=20via=20REST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Data/CommonSettings.php | 4 ++-- .../src/Endpoint/CommonRestEndpoint.php | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 0935734b8..7d4c5a640 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -57,14 +57,14 @@ class CommonSettings extends AbstractDataModel { return array( 'use_sandbox' => false, 'use_manual_connection' => false, - 'client_id' => '', - 'client_secret' => '', // Details about connected merchant account. 'merchant_connected' => false, 'sandbox_merchant' => false, 'merchant_id' => '', 'merchant_email' => '', + 'client_id' => '', + 'client_secret' => '', ); } diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 74b66ff16..0be2b4ad3 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -50,14 +50,6 @@ class CommonRestEndpoint extends RestEndpoint { 'js_name' => 'useManualConnection', 'sanitize' => 'to_boolean', ), - 'client_id' => array( - 'js_name' => 'clientId', - 'sanitize' => 'sanitize_text_field', - ), - 'client_secret' => array( - 'js_name' => 'clientSecret', - 'sanitize' => 'sanitize_text_field', - ), 'webhooks' => array( 'js_name' => 'webhooks', ), @@ -81,6 +73,12 @@ class CommonRestEndpoint extends RestEndpoint { 'merchant_email' => array( 'js_name' => 'email', ), + 'client_id' => array( + 'js_name' => 'clientId', + ), + 'client_secret' => array( + 'js_name' => 'clientSecret', + ), ); /** From 08ff9e7380869d619e3ff55bdbbaa0ebaa249b28 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 16:15:49 +0100 Subject: [PATCH 46/57] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Introduce=20a=20D?= =?UTF-8?q?TO=20to=20hold=20merchent=20credentials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/DTO/MerchantConnectionDTO.php | 75 +++++++++++++++++++ .../ppcp-settings/src/Data/CommonSettings.php | 33 +++++--- .../src/Service/ConnectionManager.php | 42 ++++++++--- 3 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php diff --git a/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php b/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php new file mode 100644 index 000000000..f79a85d76 --- /dev/null +++ b/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php @@ -0,0 +1,75 @@ +is_sandbox = $is_sandbox; + $this->client_id = $client_id; + $this->client_secret = $client_secret; + $this->merchant_id = $merchant_id; + $this->merchant_email = $merchant_email; + } +} diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 7d4c5a640..47470e35e 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -10,6 +10,7 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Data; use RuntimeException; +use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; /** * Class CommonSettings @@ -41,11 +42,16 @@ class CommonSettings extends AbstractDataModel { * * @param string $country WooCommerce store country. * @param string $currency WooCommerce store currency. + * + * @throws RuntimeException When forgetting to define the OPTION_KEY in this class. */ public function __construct( string $country, string $currency ) { parent::__construct(); + $this->woo_settings['country'] = $country; $this->woo_settings['currency'] = $currency; + + $this->data['merchant_connected'] = $this->is_merchant_connected(); } /** @@ -154,19 +160,17 @@ class CommonSettings extends AbstractDataModel { /** * Setter to update details of the connected merchant account. * - * Those details cannot be changed individually. - * - * @param bool $is_sandbox Whether the details are for a sandbox account. - * @param string $merchant_id The merchant ID. - * @param string $merchant_email The merchant's email. + * @param MerchantConnectionDTO $connection Connection details. * * @return void */ - public function set_merchant_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { - $this->data['sandbox_merchant'] = $is_sandbox; - $this->data['merchant_id'] = sanitize_text_field( $merchant_id ); - $this->data['merchant_email'] = sanitize_email( $merchant_email ); - $this->data['merchant_connected'] = true; + public function set_merchant_data( MerchantConnectionDTO $connection ) : void { + $this->data['sandbox_merchant'] = $connection->is_sandbox; + $this->data['merchant_id'] = sanitize_text_field( $connection->merchant_id ); + $this->data['merchant_email'] = sanitize_email( $connection->merchant_email ); + $this->data['client_id'] = sanitize_text_field( $connection->client_id ); + $this->data['client_secret'] = sanitize_text_field( $connection->client_secret ); + $this->data['merchant_connected'] = $this->is_merchant_connected(); } /** @@ -180,7 +184,9 @@ class CommonSettings extends AbstractDataModel { $this->data['sandbox_merchant'] = $defaults['sandbox_merchant']; $this->data['merchant_id'] = $defaults['merchant_id']; $this->data['merchant_email'] = $defaults['merchant_email']; - $this->data['merchant_connected'] = $defaults['merchant_connected']; + $this->data['client_id'] = $defaults['client_id']; + $this->data['client_secret'] = $defaults['client_secret']; + $this->data['merchant_connected'] = false; } /** @@ -198,7 +204,10 @@ class CommonSettings extends AbstractDataModel { * @return bool */ public function is_merchant_connected() : bool { - return $this->data['merchant_connected'] && $this->data['merchant_id'] && $this->data['merchant_email']; + return $this->data['merchant_email'] + && $this->data['merchant_id'] + && $this->data['client_id'] + && $this->data['client_secret']; } /** diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/ConnectionManager.php index b773919bb..85d4af927 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/ConnectionManager.php @@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; +use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; /** * Class that manages the connection to PayPal. @@ -167,7 +168,15 @@ class ConnectionManager { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); - $this->update_connection_details( $use_sandbox, $payee['merchant_id'], $payee['email_address'] ); + $connection = new MerchantConnectionDTO( + $use_sandbox, + $client_id, + $client_secret, + $payee['merchant_id'], + $payee['email_address'] + ); + + $this->update_connection_details( $connection ); } @@ -220,7 +229,22 @@ class ConnectionManager { $credentials = $this->get_credentials( $shared_id, $auth_code, $use_sandbox ); - // TODO. + /** + * The merchant's email is set by `ConnectionListener`. That listener + * is invoked during the page reload, once the user clicks the blue + * "Return to Store" button in PayPal's login popup. + */ + $empty_email = ''; + + $connection = new MerchantConnectionDTO( + $use_sandbox, + $credentials['client_id'], + $credentials['client_secret'], + $credentials['merchant_id'], + $empty_email + ); + + $this->update_connection_details( $connection ); } @@ -329,22 +353,16 @@ class ConnectionManager { /** * Stores the provided details in the data model. * - * @param bool $is_sandbox Whether the details are for a sandbox account. - * @param string $merchant_id PayPal's internal merchant ID. - * @param string $merchant_email Email address associated with the PayPal account. + * @param MerchantConnectionDTO $connection Connection details to persist. * @return void */ - private function update_connection_details( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { + private function update_connection_details( MerchantConnectionDTO $connection ) : void { $this->logger->info( 'Updating connection details', - array( - 'sandbox' => $is_sandbox, - 'merchant_id' => $merchant_id, - 'merchant_email' => $merchant_email, - ) + (array) $connection ); - $this->common_settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->common_settings->set_merchant_data( $connection ); $this->common_settings->save(); /** From 33bd9ecce8ed5ece9880e2ddc6fe51f2467e7825 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 16:34:42 +0100 Subject: [PATCH 47/57] =?UTF-8?q?=E2=9C=A8=20Update=20merchant=20email=20a?= =?UTF-8?q?fter=20OAuth=20completion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Data/AbstractDataModel.php | 1 - .../ppcp-settings/src/Data/CommonSettings.php | 55 ++++++------------- .../src/Handler/ConnectionListener.php | 30 ++++++---- 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/modules/ppcp-settings/src/Data/AbstractDataModel.php b/modules/ppcp-settings/src/Data/AbstractDataModel.php index 780ad40bd..070015af2 100644 --- a/modules/ppcp-settings/src/Data/AbstractDataModel.php +++ b/modules/ppcp-settings/src/Data/AbstractDataModel.php @@ -122,5 +122,4 @@ abstract class AbstractDataModel { return $stripped_key ? "set_$stripped_key" : ''; } - } diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php index 47470e35e..8e118ab89 100644 --- a/modules/ppcp-settings/src/Data/CommonSettings.php +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -61,8 +61,8 @@ class CommonSettings extends AbstractDataModel { */ protected function get_defaults() : array { return array( - 'use_sandbox' => false, - 'use_manual_connection' => false, + 'use_sandbox' => false, // UI state, not a connection detail. + 'use_manual_connection' => false, // UI state, not a connection detail. // Details about connected merchant account. 'merchant_connected' => false, @@ -112,42 +112,6 @@ class CommonSettings extends AbstractDataModel { $this->data['use_manual_connection'] = $use_manual_connection; } - /** - * Gets the client ID. - * - * @return string - */ - public function get_client_id() : string { - return $this->data['client_id']; - } - - /** - * Sets the client ID. - * - * @param string $client_id The client ID. - */ - public function set_client_id( string $client_id ) : void { - $this->data['client_id'] = sanitize_text_field( $client_id ); - } - - /** - * Gets the client secret. - * - * @return string - */ - public function get_client_secret() : string { - return $this->data['client_secret']; - } - - /** - * Sets the client secret. - * - * @param string $client_secret The client secret. - */ - public function set_client_secret( string $client_secret ) : void { - $this->data['client_secret'] = sanitize_text_field( $client_secret ); - } - /** * Returns the list of read-only customization flags. * @@ -173,6 +137,21 @@ class CommonSettings extends AbstractDataModel { $this->data['merchant_connected'] = $this->is_merchant_connected(); } + /** + * Returns the full merchant connection DTO for the current connection. + * + * @return MerchantConnectionDTO All connection details. + */ + public function get_merchant_data() : MerchantConnectionDTO { + return new MerchantConnectionDTO( + $this->is_sandbox_merchant(), + $this->data['client_id'], + $this->data['client_secret'], + $this->data['merchant_id'], + $this->data['merchant_email'] + ); + } + /** * Reset all connection details to the initial, disconnected state. * diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php index a24a82231..5ce3f14d6 100644 --- a/modules/ppcp-settings/src/Handler/ConnectionListener.php +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -10,10 +10,12 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Handler; +use Psr\Log\LoggerInterface; +use RuntimeException; +use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; -use Psr\Log\LoggerInterface; /** * Provides a listener that handles merchant-connection requests. @@ -82,6 +84,8 @@ class ConnectionListener { * * @param int $user_id The current user ID. * @param array $request Request details to process. + * + * @throws RuntimeException If the merchant ID does not match the ID previously set via OAuth. */ public function process( int $user_id, array $request ) : void { $this->user_id = $user_id; @@ -102,11 +106,15 @@ class ConnectionListener { $this->logger->info( 'Found merchant data in request', $data ); - $this->store_data( - $data['is_sandbox'], - $data['merchant_id'], - $data['merchant_email'] - ); + $connection = $this->settings->get_merchant_data(); + + if ( $connection->merchant_id !== $data['merchant_id'] ) { + throw new RuntimeException( 'Unexpected merchant ID in request' ); + } + + $connection->merchant_email = $data['merchant_email']; + + $this->store_data( $connection ); } /** @@ -169,14 +177,12 @@ class ConnectionListener { /** * Persist the merchant details to the database. * - * @param bool $is_sandbox Whether the details are for a sandbox account. - * @param string $merchant_id The anonymized merchant ID. - * @param string $merchant_email The merchant's email. + * @param MerchantConnectionDTO $connection Merchant connection details to store. */ - protected function store_data( bool $is_sandbox, string $merchant_id, string $merchant_email ) : void { - $this->logger->info( "Save merchant details to the DB: $merchant_email ($merchant_id)" ); + protected function store_data( MerchantConnectionDTO $connection ) : void { + $this->logger->info( 'Save merchant details to the DB', (array) $connection ); - $this->settings->set_merchant_data( $is_sandbox, $merchant_id, $merchant_email ); + $this->settings->set_merchant_data( $connection ); $this->settings->save(); } From 6591889079ac0fd32315bb22c813b05a84c1d7fb Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 16:50:38 +0100 Subject: [PATCH 48/57] =?UTF-8?q?=F0=9F=9A=A7=20Refactor=20the=20eligible-?= =?UTF-8?q?feature=20REST=20response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/src/ApplepayModule.php | 10 +++------- modules/ppcp-googlepay/src/GooglepayModule.php | 10 +++------- .../Components/Screens/Overview/TabOverview.js | 4 ++-- .../resources/js/data/common/hooks.js | 10 +++++++++- .../resources/js/data/common/reducer.js | 18 +++++++++++++++++- .../resources/js/data/common/selectors.js | 6 +++++- .../src/Endpoint/CommonRestEndpoint.php | 10 ++++++---- .../ppcp-wc-gateway/src/WCGatewayModule.php | 12 ++++-------- 8 files changed, 49 insertions(+), 31 deletions(-) diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index aa9876069..dc7b3cf11 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -184,21 +184,17 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule add_filter( 'woocommerce_paypal_payments_rest_common_merchant_data', - function( array $merchant_data ) use ( $c ): array { - if ( ! isset( $merchant_data['features'] ) ) { - $merchant_data['features'] = array(); - } - + function( array $features ) use ( $c ): array { $product_status = $c->get( 'applepay.apple-product-status' ); assert( $product_status instanceof AppleProductStatus ); $apple_pay_enabled = $product_status->is_active(); - $merchant_data['features']['apple_pay'] = array( + $features['apple_pay'] = array( 'enabled' => $apple_pay_enabled, ); - return $merchant_data; + return $features; } ); diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index 01d5f8fae..dd7320011 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -234,21 +234,17 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul add_filter( 'woocommerce_paypal_payments_rest_common_merchant_data', - function ( array $merchant_data ) use ( $c ): array { - if ( ! isset( $merchant_data['features'] ) ) { - $merchant_data['features'] = array(); - } - + function ( array $features ) use ( $c ): array { $product_status = $c->get( 'googlepay.helpers.apm-product-status' ); assert( $product_status instanceof ApmProductStatus ); $google_pay_enabled = $product_status->is_active(); - $merchant_data['features']['google_pay'] = array( + $features['google_pay'] = array( 'enabled' => $google_pay_enabled, ); - return $merchant_data; + return $features; } ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js index 07e70efea..f64095a8f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabOverview.js @@ -16,11 +16,11 @@ const TabOverview = () => { const [ todosData, setTodosData ] = useState( todosDataDefault ); const [ isRefreshing, setIsRefreshing ] = useState( false ); - const { merchant } = useMerchantInfo(); + const { merchantFeatures } = useMerchantInfo(); const { refreshFeatureStatuses } = useDispatch( STORE_NAME ); const features = featuresDefault.map( ( feature ) => { - const merchantFeature = merchant?.features?.[ feature.id ]; + const merchantFeature = merchantFeatures?.[ feature.id ]; return { ...feature, enabled: merchantFeature?.enabled ?? false, diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 8c22494ca..4a2429575 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -47,10 +47,16 @@ const useHooks = () => { ( select ) => select( STORE_NAME ).merchant(), [] ); + + // Read-only properties. const wooSettings = useSelect( ( select ) => select( STORE_NAME ).wooSettings(), [] ); + const features = useSelect( + ( select ) => select( STORE_NAME ).features(), + [] + ); const savePersistent = async ( setter, value ) => { setter( value ); @@ -73,6 +79,7 @@ const useHooks = () => { authenticateWithOAuth, merchant, wooSettings, + features, webhooks, startWebhookSimulation, checkWebhookSimulationState, @@ -130,7 +137,7 @@ export const useWebhooks = () => { }; }; export const useMerchantInfo = () => { - const { merchant } = useHooks(); + const { merchant, features } = useHooks(); const { refreshMerchantData } = useDispatch( STORE_NAME ); const verifyLoginStatus = useCallback( async () => { @@ -146,6 +153,7 @@ export const useMerchantInfo = () => { return { merchant, // Merchant details + features, // Eligible merchant features verifyLoginStatus, // Callback }; }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 922db6985..af5aece23 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -30,6 +30,21 @@ const defaultTransient = Object.freeze( { storeCountry: '', storeCurrency: '', } ), + + features: Object.freeze( { + save_paypal_and_venmo: { + enabled: false, + }, + advanced_credit_and_debit_cards: { + enabled: false, + }, + apple_pay: { + enabled: false, + }, + google_pay: { + enabled: false, + }, + } ), } ); const defaultPersistent = Object.freeze( { @@ -83,13 +98,14 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( { ...state, merchant: Object.freeze( { ...defaultTransient.merchant } ), + features: Object.freeze( { ...defaultTransient.features } ), } ), [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { const newState = setPersistent( state, payload.data ); // Populate read-only properties. - [ 'wooSettings', 'merchant' ].forEach( ( key ) => { + [ 'wooSettings', 'merchant', 'features' ].forEach( ( key ) => { if ( ! payload[ key ] ) { return; } diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 96393942a..0e0ec781e 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -16,7 +16,7 @@ export const persistentData = ( state ) => { }; export const transientData = ( state ) => { - const { data, merchant, wooSettings, ...transientState } = + const { data, merchant, features, wooSettings, ...transientState } = getState( state ); return transientState || EMPTY_OBJ; }; @@ -30,6 +30,10 @@ export const merchant = ( state ) => { return getState( state ).merchant || EMPTY_OBJ; }; +export const features = ( state ) => { + return getState( state ).features || EMPTY_OBJ; +}; + export const wooSettings = ( state ) => { return getState( state ).wooSettings || EMPTY_OBJ; }; diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 0be2b4ad3..6f249006a 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -201,10 +201,12 @@ class CommonRestEndpoint extends RestEndpoint { $this->merchant_info_map ); - $extra_data['merchant'] = apply_filters( - 'woocommerce_paypal_payments_rest_common_merchant_data', - $extra_data['merchant'], - ); + if ( $this->settings->is_merchant_connected() ) { + $extra_data['features'] = apply_filters( + 'woocommerce_paypal_payments_rest_common_merchant_data', + array(), + ); + } return $extra_data; } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index f754a3cbe..e4e13a91d 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -550,16 +550,12 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul add_filter( 'woocommerce_paypal_payments_rest_common_merchant_data', - function( array $merchant_data ) use ( $c ): array { - if ( ! isset( $merchant_data['features'] ) ) { - $merchant_data['features'] = array(); - } - + function( array $features ) use ( $c ): array { $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); $reference_transactions_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); - $merchant_data['features']['save_paypal_and_venmo'] = array( + $features['save_paypal_and_venmo'] = array( 'enabled' => $reference_transactions_enabled, ); @@ -567,11 +563,11 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul assert( $dcc_product_status instanceof DCCProductStatus ); $dcc_enabled = $dcc_product_status->dcc_is_active(); - $merchant_data['features']['advanced_credit_and_debit_cards'] = array( + $features['advanced_credit_and_debit_cards'] = array( 'enabled' => $dcc_enabled, ); - return $merchant_data; + return $features; } ); From 67a6d9e765af9cc1e9c927ccd4223bc1bbd1edfe Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 16:52:21 +0100 Subject: [PATCH 49/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adjust=20=E2=80=9Cwe?= =?UTF-8?q?bhooks=E2=80=9D=20property=20in=20Redux=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/hooks.js | 5 +++- .../resources/js/data/common/reducer.js | 23 +++++++++++-------- .../resources/js/data/common/selectors.js | 10 ++++++-- .../src/Endpoint/WebhookSettingsEndpoint.php | 2 +- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 4a2429575..e9a77b79b 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -42,7 +42,6 @@ const useHooks = () => { // Persistent accessors. const isSandboxMode = usePersistent( 'useSandbox' ); const isManualConnectionMode = usePersistent( 'useManualConnection' ); - const webhooks = usePersistent( 'webhooks' ); const merchant = useSelect( ( select ) => select( STORE_NAME ).merchant(), [] @@ -57,6 +56,10 @@ const useHooks = () => { ( select ) => select( STORE_NAME ).features(), [] ); + const webhooks = useSelect( + ( select ) => select( STORE_NAME ).webhooks(), + [] + ); const savePersistent = async ( setter, value ) => { setter( value ); diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index af5aece23..019d29911 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -45,12 +45,13 @@ const defaultTransient = Object.freeze( { enabled: false, }, } ), + + webhooks: Object.freeze( [] ), } ); const defaultPersistent = Object.freeze( { useSandbox: false, useManualConnection: false, - webhooks: [], } ); // Reducer logic. @@ -105,16 +106,18 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { const newState = setPersistent( state, payload.data ); // Populate read-only properties. - [ 'wooSettings', 'merchant', 'features' ].forEach( ( key ) => { - if ( ! payload[ key ] ) { - return; - } + [ 'wooSettings', 'merchant', 'features', 'webhooks' ].forEach( + ( key ) => { + if ( ! payload[ key ] ) { + return; + } - newState[ key ] = Object.freeze( { - ...newState[ key ], - ...payload[ key ], - } ); - } ); + newState[ key ] = Object.freeze( { + ...newState[ key ], + ...payload[ key ], + } ); + } + ); return newState; }, diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 0e0ec781e..4716550bb 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -16,8 +16,14 @@ export const persistentData = ( state ) => { }; export const transientData = ( state ) => { - const { data, merchant, features, wooSettings, ...transientState } = - getState( state ); + const { + data, + merchant, + features, + wooSettings, + webhooks, + ...transientState + } = getState( state ); return transientState || EMPTY_OBJ; }; diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php index c3116a1ed..69b346734 100644 --- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -118,7 +118,7 @@ class WebhookSettingsEndpoint extends RestEndpoint { try { $webhook_list = ( $this->webhook_endpoint->list() )[0]; $webhook_events = array_map( - function ( stdClass $webhook ) { + static function ( stdClass $webhook ) { return strtolower( $webhook->name ); }, $webhook_list->event_types() From 8ce7d6ca996b02f441851c2e41f22f76a689df30 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 17:18:52 +0100 Subject: [PATCH 50/57] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20ConnectionManager?= =?UTF-8?q?=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 14 ++++----- .../Endpoint/AuthenticationRestEndpoint.php | 29 ++++++++++++------- ...nManager.php => AuthenticationManager.php} | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) rename modules/ppcp-settings/src/Service/{ConnectionManager.php => AuthenticationManager.php} (99%) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index f19dc750f..df50f886d 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -10,21 +10,21 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; -use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint; -use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint; +use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; +use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; -use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -82,7 +82,7 @@ return array( }, 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint { return new AuthenticationRestEndpoint( - $container->get( 'settings.service.connection_manager' ), + $container->get( 'settings.service.authentication_manager' ), ); }, 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint { @@ -180,8 +180,8 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'settings.service.connection_manager' => static function ( ContainerInterface $container ) : ConnectionManager { - return new ConnectionManager( + 'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager { + return new AuthenticationManager( $container->get( 'settings.data.common' ), $container->get( 'api.env.paypal-host' ), $container->get( 'api.env.endpoint.login-seller' ), diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index f3c26c15f..c11ccf1a8 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -20,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; -use WooCommerce\PayPalCommerce\Settings\Service\ConnectionManager; +use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; /** * REST controller for authenticating and connecting to a PayPal merchant account. @@ -40,6 +40,13 @@ class AuthenticationRestEndpoint extends RestEndpoint { */ protected $rest_base = 'authenticate'; + /** + * Authentication manager service. + * + * @var AuthenticationManager + */ + private AuthenticationManager $authentication_manager; + /** * Defines the JSON response format (when connection was successful). * @@ -57,16 +64,16 @@ class AuthenticationRestEndpoint extends RestEndpoint { /** * Constructor. * - * @param ConnectionManager $connection_manager The connection manager. + * @param AuthenticationManager $authentication_manager The authentication manager. */ - public function __construct( ConnectionManager $connection_manager ) { - $this->connection_manager = $connection_manager; + public function __construct( AuthenticationManager $authentication_manager ) { + $this->authentication_manager = $authentication_manager; } /** * Configure REST API routes. */ - public function register_routes() { + public function register_routes() : void { register_rest_route( $this->namespace, '/' . $this->rest_base . '/direct', @@ -139,13 +146,13 @@ class AuthenticationRestEndpoint extends RestEndpoint { $use_sandbox = $request->get_param( 'useSandbox' ); try { - $this->connection_manager->validate_id_and_secret( $client_id, $client_secret ); - $this->connection_manager->connect_via_secret( $use_sandbox, $client_id, $client_secret ); + $this->authentication_manager->validate_id_and_secret( $client_id, $client_secret ); + $this->authentication_manager->connect_via_secret( $use_sandbox, $client_id, $client_secret ); } catch ( Exception $exception ) { return $this->return_error( $exception->getMessage() ); } - $account = $this->connection_manager->get_account_details(); + $account = $this->authentication_manager->get_account_details(); $response = $this->sanitize_for_javascript( $this->response_map, $account ); return $this->return_success( $response ); @@ -165,13 +172,13 @@ class AuthenticationRestEndpoint extends RestEndpoint { $use_sandbox = $request->get_param( 'useSandbox' ); try { - $this->connection_manager->validate_id_and_auth_code( $shared_id, $auth_code ); - $this->connection_manager->connect_via_auth_code( $use_sandbox, $shared_id, $auth_code ); + $this->authentication_manager->validate_id_and_auth_code( $shared_id, $auth_code ); + $this->authentication_manager->connect_via_auth_code( $use_sandbox, $shared_id, $auth_code ); } catch ( Exception $exception ) { return $this->return_error( $exception->getMessage() ); } - $account = $this->connection_manager->get_account_details(); + $account = $this->authentication_manager->get_account_details(); $response = $this->sanitize_for_javascript( $this->response_map, $account ); return $this->return_success( $response ); diff --git a/modules/ppcp-settings/src/Service/ConnectionManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php similarity index 99% rename from modules/ppcp-settings/src/Service/ConnectionManager.php rename to modules/ppcp-settings/src/Service/AuthenticationManager.php index 85d4af927..68ba8dde0 100644 --- a/modules/ppcp-settings/src/Service/ConnectionManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -25,7 +25,7 @@ use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; /** * Class that manages the connection to PayPal. */ -class ConnectionManager { +class AuthenticationManager { /** * Data model that stores the connection details. * From ed8ae81297c882dd7cade6708fa5f41db93085d5 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 17:20:12 +0100 Subject: [PATCH 51/57] =?UTF-8?q?=F0=9F=8E=A8=20Minor=20code=20improvement?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/AuthenticationRestEndpoint.php | 7 ------- .../ppcp-settings/src/Service/AuthenticationManager.php | 7 ++++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index c11ccf1a8..0259f1c02 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -10,16 +10,9 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; use Exception; -use stdClass; -use RuntimeException; -use Psr\Log\LoggerInterface; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; -use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; -use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; /** diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 68ba8dde0..81db1ffec 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -10,6 +10,7 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Service; use JsonException; +use Throwable; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; @@ -306,7 +307,11 @@ class AuthenticationManager { $order_response = $orders->order( $order_id ); $order_body = json_decode( $order_response['body'], false, 512, JSON_THROW_ON_ERROR ); } catch ( JsonException $exception ) { + // Cast JsonException to a RuntimeException. throw new RuntimeException( 'Could not decode JSON response: ' . $exception->getMessage() ); + } catch ( Throwable $exception ) { + // Cast any other Throwable to a RuntimeException. + throw new RuntimeException( $exception->getMessage() ); } $pu = $order_body->purchase_units[0]; @@ -315,7 +320,7 @@ class AuthenticationManager { if ( ! is_object( $payee ) ) { throw new RuntimeException( 'Payee not found.' ); } - if ( ! isset( $payee->merchant_id ) || ! isset( $payee->email_address ) ) { + if ( ! isset( $payee->merchant_id, $payee->email_address ) ) { throw new RuntimeException( 'Payee info not found.' ); } From 19a7986b561e43e5e58a2b3249dda46e7b8f1417 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 17:44:53 +0100 Subject: [PATCH 52/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Align=20function=20n?= =?UTF-8?q?aming=20in=20PHP=20with=20JS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/AuthenticationRestEndpoint.php | 4 ++-- modules/ppcp-settings/src/Handler/ConnectionListener.php | 2 +- modules/ppcp-settings/src/Service/AuthenticationManager.php | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index 0259f1c02..9d72ff88c 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -140,7 +140,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { try { $this->authentication_manager->validate_id_and_secret( $client_id, $client_secret ); - $this->authentication_manager->connect_via_secret( $use_sandbox, $client_id, $client_secret ); + $this->authentication_manager->authenticate_via_direct_api( $use_sandbox, $client_id, $client_secret ); } catch ( Exception $exception ) { return $this->return_error( $exception->getMessage() ); } @@ -166,7 +166,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { try { $this->authentication_manager->validate_id_and_auth_code( $shared_id, $auth_code ); - $this->authentication_manager->connect_via_auth_code( $use_sandbox, $shared_id, $auth_code ); + $this->authentication_manager->authenticate_via_oauth( $use_sandbox, $shared_id, $auth_code ); } catch ( Exception $exception ) { return $this->return_error( $exception->getMessage() ); } diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php index 5ce3f14d6..f11c0bc20 100644 --- a/modules/ppcp-settings/src/Handler/ConnectionListener.php +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -104,7 +104,7 @@ class ConnectionListener { return; } - $this->logger->info( 'Found merchant data in request', $data ); + $this->logger->info( 'Found OAuth merchant data in request', $data ); $connection = $this->settings->get_merchant_data(); diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 81db1ffec..d44d029bb 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -156,7 +156,7 @@ class AuthenticationManager { * @return void * @throws RuntimeException When failed to retrieve payee. */ - public function connect_via_secret( bool $use_sandbox, string $client_id, string $client_secret ) : void { + public function authenticate_via_direct_api( bool $use_sandbox, string $client_id, string $client_secret ) : void { $this->disconnect(); $this->logger->info( @@ -217,11 +217,11 @@ class AuthenticationManager { * @return void * @throws RuntimeException When failed to retrieve payee. */ - public function connect_via_auth_code( bool $use_sandbox, string $shared_id, string $auth_code ) : void { + public function authenticate_via_oauth( bool $use_sandbox, string $shared_id, string $auth_code ) : void { $this->disconnect(); $this->logger->info( - 'Attempting ISU login to PayPal...', + 'Attempting OAuth login to PayPal...', array( 'sandbox' => $use_sandbox, 'shared_id' => $shared_id, From b3e766d08a995d399245ccf6b45b7f50eb8e284e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 17:47:37 +0100 Subject: [PATCH 53/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20OAuth=20logic?= =?UTF-8?q?=20from=20Listner=20to=20Auth-Manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 2 +- .../src/Handler/ConnectionListener.php | 65 ++++++++----------- .../src/Service/AuthenticationManager.php | 28 ++++++++ 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index df50f886d..080973039 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -158,8 +158,8 @@ return array( return new ConnectionListener( $page_id, - $container->get( 'settings.data.common' ), $container->get( 'settings.service.onboarding-url-manager' ), + $container->get( 'settings.service.authentication_manager' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, diff --git a/modules/ppcp-settings/src/Handler/ConnectionListener.php b/modules/ppcp-settings/src/Handler/ConnectionListener.php index f11c0bc20..7b30b51a1 100644 --- a/modules/ppcp-settings/src/Handler/ConnectionListener.php +++ b/modules/ppcp-settings/src/Handler/ConnectionListener.php @@ -12,8 +12,7 @@ namespace WooCommerce\PayPalCommerce\Settings\Handler; use Psr\Log\LoggerInterface; use RuntimeException; -use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; -use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; +use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager; use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; @@ -33,13 +32,6 @@ class ConnectionListener { */ private string $settings_page_id; - /** - * Access to connection settings. - * - * @var CommonSettings - */ - private CommonSettings $settings; - /** * Access to the onboarding URL manager. * @@ -47,6 +39,13 @@ class ConnectionListener { */ private OnboardingUrlManager $url_manager; + /** + * Authentication manager service, responsible to update connection details. + * + * @var AuthenticationManager + */ + private AuthenticationManager $authentication_manager; + /** * Logger instance, mainly used for debugging purposes. * @@ -64,16 +63,21 @@ class ConnectionListener { /** * Prepare the instance. * - * @param string $settings_page_id Current plugin settings page ID. - * @param CommonSettings $settings Access to saved connection details. - * @param OnboardingUrlManager $url_manager Get OnboardingURL instances. - * @param ?LoggerInterface $logger The logger, for debugging purposes. + * @param string $settings_page_id Current plugin settings page ID. + * @param OnboardingUrlManager $url_manager Get OnboardingURL instances. + * @param AuthenticationManager $authentication_manager Authentication manager service. + * @param ?LoggerInterface $logger The logger, for debugging purposes. */ - public function __construct( string $settings_page_id, CommonSettings $settings, OnboardingUrlManager $url_manager, LoggerInterface $logger = null ) { - $this->settings_page_id = $settings_page_id; - $this->settings = $settings; - $this->url_manager = $url_manager; - $this->logger = $logger ?: new NullLogger(); + public function __construct( + string $settings_page_id, + OnboardingUrlManager $url_manager, + AuthenticationManager $authentication_manager, + LoggerInterface $logger = null + ) { + $this->settings_page_id = $settings_page_id; + $this->url_manager = $url_manager; + $this->authentication_manager = $authentication_manager; + $this->logger = $logger ?: new NullLogger(); // Initialize as "guest", the real ID is provided via process(). $this->user_id = 0; @@ -106,15 +110,11 @@ class ConnectionListener { $this->logger->info( 'Found OAuth merchant data in request', $data ); - $connection = $this->settings->get_merchant_data(); - - if ( $connection->merchant_id !== $data['merchant_id'] ) { - throw new RuntimeException( 'Unexpected merchant ID in request' ); + try { + $this->authentication_manager->finish_oauth_authentication( $data ); + } catch ( \Exception $e ) { + $this->logger->error( 'Failed to complete authentication: ' . $e->getMessage() ); } - - $connection->merchant_email = $data['merchant_email']; - - $this->store_data( $connection ); } /** @@ -168,24 +168,11 @@ class ConnectionListener { } return array( - 'is_sandbox' => $this->settings->get_sandbox(), 'merchant_id' => $merchant_id, 'merchant_email' => $merchant_email, ); } - /** - * Persist the merchant details to the database. - * - * @param MerchantConnectionDTO $connection Merchant connection details to store. - */ - protected function store_data( MerchantConnectionDTO $connection ) : void { - $this->logger->info( 'Save merchant details to the DB', (array) $connection ); - - $this->settings->set_merchant_data( $connection ); - $this->settings->save(); - } - /** * Returns the sanitized connection token from the incoming request. * diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index d44d029bb..1eaef1aae 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -248,6 +248,34 @@ class AuthenticationManager { $this->update_connection_details( $connection ); } + /** + * Verifies the merchant details in the final OAuth redirect and extracts + * missing credentials from the URL. + * + * @param array $request_data Array of request parameters to process. + * @return void + * + * @throws RuntimeException Missing or invalid credentials. + */ + public function finish_oauth_authentication( array $request_data ) : void { + $merchant_id = $request_data['merchant_id']; + $merchant_email = $request_data['merchant_email']; + + if ( empty( $merchant_id ) || empty( $merchant_email ) ) { + throw new RuntimeException( 'Missing merchant ID or email in request' ); + } + + $connection = $this->common_settings->get_merchant_data(); + + if ( $connection->merchant_id !== $merchant_id ) { + throw new RuntimeException( 'Unexpected merchant ID in request' ); + } + + $connection->merchant_email = $merchant_email; + + $this->update_connection_details( $connection ); + } + // ---------------------------------------------------------------------------- // Internal helper methods From a9c2a8e8feea73dce063d2abf640301c7806db01 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 8 Jan 2025 17:49:04 +0100 Subject: [PATCH 54/57] =?UTF-8?q?=E2=9C=A8=20Sync=20onboarding=20completio?= =?UTF-8?q?n=20with=20connection=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/AuthenticationManager.php | 16 +++++++----- modules/ppcp-settings/src/SettingsModule.php | 26 ++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 1eaef1aae..b7d308669 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -398,11 +398,15 @@ class AuthenticationManager { $this->common_settings->set_merchant_data( $connection ); $this->common_settings->save(); - /** - * Broadcast that the plugin connected to a new PayPal merchant account. - * This is the right time to initialize merchant relative flags for the - * first time. - */ - do_action( 'woocommerce_paypal_payments_authenticated_merchant' ); + if ( $this->common_settings->is_merchant_connected() ) { + $this->logger->info( 'Merchant successfully connected to PayPal' ); + + /** + * Broadcast that the plugin connected to a new PayPal merchant account. + * This is the right time to initialize merchant relative flags for the + * first time. + */ + do_action( 'woocommerce_paypal_payments_authenticated_merchant' ); + } } } diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index d104ce7a4..59f752545 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -9,8 +9,9 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; -use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint; +use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -203,6 +204,29 @@ class SettingsModule implements ServiceModule, ExecutableModule { } ); + add_action( + 'woocommerce_paypal_payments_merchant_disconnected', + static function () use ( $container ) : void { + $onboarding_profile = $container->get( 'settings.data.onboarding' ); + assert( $onboarding_profile instanceof OnboardingProfile ); + + $onboarding_profile->set_completed( false ); + $onboarding_profile->set_step( 0 ); + $onboarding_profile->save(); + } + ); + + add_action( + 'woocommerce_paypal_payments_authenticated_merchant', + static function () use ( $container ) : void { + $onboarding_profile = $container->get( 'settings.data.onboarding' ); + assert( $onboarding_profile instanceof OnboardingProfile ); + + $onboarding_profile->set_completed( true ); + $onboarding_profile->save(); + } + ); + return true; } From eee97757c37ddcef4676feef173435c4d493b8b9 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 9 Jan 2025 12:07:09 +0100 Subject: [PATCH 55/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20merchant=20?= =?UTF-8?q?data=20instead=20of=20replacing=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/AuthenticationManager.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index b7d308669..4b3f18786 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -235,15 +235,12 @@ class AuthenticationManager { * is invoked during the page reload, once the user clicks the blue * "Return to Store" button in PayPal's login popup. */ - $empty_email = ''; + $connection = $this->common_settings->get_merchant_data(); - $connection = new MerchantConnectionDTO( - $use_sandbox, - $credentials['client_id'], - $credentials['client_secret'], - $credentials['merchant_id'], - $empty_email - ); + $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 ); } @@ -267,10 +264,11 @@ class AuthenticationManager { $connection = $this->common_settings->get_merchant_data(); - if ( $connection->merchant_id !== $merchant_id ) { + if ( $connection->merchant_id && $connection->merchant_id !== $merchant_id ) { throw new RuntimeException( 'Unexpected merchant ID in request' ); } + $connection->merchant_id = $merchant_id; $connection->merchant_email = $merchant_email; $this->update_connection_details( $connection ); From eb4c9a63027efde309cb91d2a7c0f8cf080a32d6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 9 Jan 2025 12:34:22 +0100 Subject: [PATCH 56/57] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20phpcs=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/services.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index ab58194a7..349ba5577 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -881,7 +881,11 @@ return array( return CONNECT_WOO_SANDBOX_MERCHANT_ID; }, 'api.env.paypal-host' => static function ( ContainerInterface $container ) : EnvironmentConfig { - /** @type EnvironmentConfig Configuration object */ + /** + * Environment specific API host names. + * + * @type EnvironmentConfig + */ return EnvironmentConfig::create( 'string', $container->get( 'api.paypal-host-production' ), @@ -889,7 +893,11 @@ return array( ); }, 'api.env.endpoint.login-seller' => static function ( ContainerInterface $container ) : EnvironmentConfig { - /** @type EnvironmentConfig Configuration object */ + /** + * Environment specific LoginSeller API instances. + * + * @type EnvironmentConfig + */ return EnvironmentConfig::create( LoginSeller::class, $container->get( 'api.endpoint.login-seller-production' ), @@ -897,7 +905,11 @@ return array( ); }, 'api.env.endpoint.partner-referrals' => static function ( ContainerInterface $container ) : EnvironmentConfig { - /** @type EnvironmentConfig Configuration object */ + /** + * Environment specific PartnerReferrals API instances. + * + * @type EnvironmentConfig + */ return EnvironmentConfig::create( PartnerReferrals::class, $container->get( 'api.endpoint.partner-referrals-production' ), From f384d361cf6d4d730af63950ff010b52bda1fc7b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 9 Jan 2025 12:36:05 +0100 Subject: [PATCH 57/57] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20code=20from?= =?UTF-8?q?=20onboarding-=20to=20api-client-module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-api-client/services.php | 14 ++++++++++++++ modules/ppcp-onboarding/services.php | 22 ---------------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 349ba5577..515968463 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -880,6 +880,20 @@ return array( 'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string { return CONNECT_WOO_SANDBOX_MERCHANT_ID; }, + 'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller { + return new LoginSeller( + $container->get( 'api.paypal-host-production' ), + $container->get( 'api.partner_merchant_id-production' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller { + return new LoginSeller( + $container->get( 'api.paypal-host-sandbox' ), + $container->get( 'api.partner_merchant_id-sandbox' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.env.paypal-host' => static function ( ContainerInterface $container ) : EnvironmentConfig { /** * Environment specific API host names. diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index 56a49be8e..aca3bed0a 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -14,14 +14,12 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; -use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; return array( 'api.sandbox-host' => static function ( ContainerInterface $container ): string { @@ -144,26 +142,6 @@ return array( ); }, - 'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller { - - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - return new LoginSeller( - $container->get( 'api.paypal-host-production' ), - $container->get( 'api.partner_merchant_id-production' ), - $logger - ); - }, - - 'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller { - - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - return new LoginSeller( - $container->get( 'api.paypal-host-sandbox' ), - $container->get( 'api.partner_merchant_id-sandbox' ), - $logger - ); - }, - 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { $request_data = $container->get( 'button.request-data' );