From bd83cc201ce78a7b0d8249c2887d2dca20f78a65 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 8 Nov 2024 17:05:08 +0100 Subject: [PATCH 01/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Format=20psalm=20stu?= =?UTF-8?q?bs=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .psalm/stubs.php | 90 +++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 59be5215c..93a59aa84 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -1,71 +1,89 @@ Date: Fri, 8 Nov 2024 17:09:07 +0100 Subject: [PATCH 02/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20code=20from?= =?UTF-8?q?=20old=20onboarding=20to=20api=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make services reusable in the new settings module, without creating a dependency on the old module --- .psalm/stubs.php | 33 ++++++++++-- modules/ppcp-api-client/services.php | 36 ++++++++++++- modules/ppcp-onboarding/services.php | 75 ++++++++-------------------- 3 files changed, 85 insertions(+), 59 deletions(-) diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 93a59aa84..56ba72451 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -29,9 +29,33 @@ if ( ! defined( 'MINUTE_IN_SECONDS' ) ) { if ( ! defined( 'ABSPATH' ) ) { define( 'ABSPATH', '' ); } +if ( ! defined( 'PAYPAL_API_URL' ) ) { + define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); +} +if ( ! defined( 'PAYPAL_SANDBOX_API_URL' ) ) { + define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); +} if ( ! defined( 'PPCP_PAYPAL_BN_CODE' ) ) { define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); } +if ( ! defined( 'CONNECT_WOO_CLIENT_ID' ) ) { + define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); +} +if ( ! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) ) { + define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' ); +} +if ( ! defined( 'CONNECT_WOO_MERCHANT_ID' ) ) { + define( 'CONNECT_WOO_MERCHANT_ID', 'K8SKZ36LQBWXJ' ); +} +if ( ! defined( 'CONNECT_WOO_SANDBOX_MERCHANT_ID' ) ) { + define( 'CONNECT_WOO_SANDBOX_MERCHANT_ID', 'MPMFHQTVMBZ6G' ); +} +if ( ! defined( 'CONNECT_WOO_URL' ) ) { + define( 'CONNECT_WOO_URL', 'https://api.woocommerce.com/integrations/ppc' ); +} +if ( ! defined( 'CONNECT_WOO_SANDBOX_URL' ) ) { + define( 'CONNECT_WOO_SANDBOX_URL', 'https://api.woocommerce.com/integrations/ppcsandbox' ); +} /** * Cancel the next occurrence of a scheduled action. @@ -78,12 +102,15 @@ class WP_HTML_Tag_Processor { public function __construct( $html ) { } - public function next_tag( $query = null ) { + public function next_tag( $query = null ) : bool { + return false; } - public function set_attribute( $name, $value ) { + public function set_attribute( $name, $value ) : bool { + return false; } - public function get_updated_html() { + public function get_updated_html() : string { + return ''; } } diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index a3850bd7c..4554f24f9 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -15,7 +15,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult; use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; @@ -79,6 +78,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; +use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; return array( 'api.host' => function( ContainerInterface $container ) : string { @@ -179,6 +179,22 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals { + + return new PartnerReferrals( + CONNECT_WOO_SANDBOX_URL, + new ConnectBearer(), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'api.endpoint.partner-referrals-production' => static function ( ContainerInterface $container ) : PartnerReferrals { + + return new PartnerReferrals( + CONNECT_WOO_URL, + new ConnectBearer(), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.endpoint.identity-token' => static function ( ContainerInterface $container ) : IdentityToken { $logger = $container->get( 'woocommerce.logger.woocommerce' ); $settings = $container->get( 'wcgateway.settings' ); @@ -853,4 +869,22 @@ return array( $container->get( 'api.client-credentials-cache' ) ); }, + 'api.paypal-host-production' => static function( ContainerInterface $container ) : string { + return PAYPAL_API_URL; + }, + 'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string { + return PAYPAL_SANDBOX_API_URL; + }, + 'api.paypal-website-url-production' => static function( ContainerInterface $container ) : string { + return PAYPAL_URL; + }, + 'api.paypal-website-url-sandbox' => static function( ContainerInterface $container ) : string { + return PAYPAL_SANDBOX_URL; + }, + 'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string { + return CONNECT_WOO_MERCHANT_ID; + }, + 'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string { + return CONNECT_WOO_SANDBOX_MERCHANT_ID; + }, ); diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index 369e82824..56a49be8e 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -15,7 +15,6 @@ 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\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; @@ -25,7 +24,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; return array( - 'api.sandbox-host' => static function ( ContainerInterface $container ): string { + 'api.sandbox-host' => static function ( ContainerInterface $container ): string { $state = $container->get( 'onboarding.state' ); @@ -39,7 +38,7 @@ return array( } return CONNECT_WOO_SANDBOX_URL; }, - 'api.production-host' => static function ( ContainerInterface $container ): string { + 'api.production-host' => static function ( ContainerInterface $container ): string { $state = $container->get( 'onboarding.state' ); @@ -54,7 +53,7 @@ return array( } return CONNECT_WOO_URL; }, - 'api.host' => static function ( ContainerInterface $container ): string { + 'api.host' => static function ( ContainerInterface $container ): string { $environment = $container->get( 'onboarding.environment' ); /** @@ -66,25 +65,7 @@ return array( ? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' ); }, - 'api.paypal-host-production' => static function( ContainerInterface $container ) : string { - return PAYPAL_API_URL; - }, - 'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string { - return PAYPAL_SANDBOX_API_URL; - }, - 'api.paypal-website-url-production' => static function( ContainerInterface $container ) : string { - return PAYPAL_URL; - }, - 'api.paypal-website-url-sandbox' => static function( ContainerInterface $container ) : string { - return PAYPAL_SANDBOX_URL; - }, - 'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string { - return CONNECT_WOO_MERCHANT_ID; - }, - 'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string { - return CONNECT_WOO_SANDBOX_MERCHANT_ID; - }, - 'api.paypal-host' => function( ContainerInterface $container ) : string { + 'api.paypal-host' => function( ContainerInterface $container ) : string { $environment = $container->get( 'onboarding.environment' ); /** * The current environment. @@ -97,7 +78,7 @@ return array( return $container->get( 'api.paypal-host-production' ); }, - 'api.paypal-website-url' => function( ContainerInterface $container ) : string { + 'api.paypal-website-url' => function( ContainerInterface $container ) : string { $environment = $container->get( 'onboarding.environment' ); assert( $environment instanceof Environment ); if ( $environment->current_environment_is( Environment::SANDBOX ) ) { @@ -107,7 +88,7 @@ return array( }, - 'api.bearer' => static function ( ContainerInterface $container ): Bearer { + 'api.bearer' => static function ( ContainerInterface $container ): Bearer { $state = $container->get( 'onboarding.state' ); @@ -134,16 +115,16 @@ return array( $settings ); }, - 'onboarding.state' => function( ContainerInterface $container ) : State { + 'onboarding.state' => function( ContainerInterface $container ) : State { $settings = $container->get( 'wcgateway.settings' ); return new State( $settings ); }, - 'onboarding.environment' => function( ContainerInterface $container ) : Environment { + 'onboarding.environment' => function( ContainerInterface $container ) : Environment { $settings = $container->get( 'wcgateway.settings' ); return new Environment( $settings ); }, - 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { + 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { $state = $container->get( 'onboarding.state' ); $login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' ); return new OnboardingAssets( @@ -156,14 +137,14 @@ return array( ); }, - 'onboarding.url' => static function ( ContainerInterface $container ): string { + 'onboarding.url' => static function ( ContainerInterface $container ): string { return plugins_url( '/modules/ppcp-onboarding/', dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller { + 'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller { $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new LoginSeller( @@ -173,7 +154,7 @@ return array( ); }, - 'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller { + 'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller { $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new LoginSeller( @@ -183,7 +164,7 @@ return array( ); }, - 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { + 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { $request_data = $container->get( 'button.request-data' ); $login_seller_production = $container->get( 'api.endpoint.login-seller-production' ); @@ -203,7 +184,7 @@ return array( new Cache( 'ppcp-client-credentials-cache' ) ); }, - 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { + 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { return new UpdateSignupLinksEndpoint( $container->get( 'wcgateway.settings' ), $container->get( 'button.request-data' ), @@ -213,26 +194,10 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals { - - return new PartnerReferrals( - CONNECT_WOO_SANDBOX_URL, - new ConnectBearer(), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, - 'api.endpoint.partner-referrals-production' => static function ( ContainerInterface $container ) : PartnerReferrals { - - return new PartnerReferrals( - CONNECT_WOO_URL, - new ConnectBearer(), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, - 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { + 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { return new Cache( 'ppcp-paypal-signup-link' ); }, - 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { + 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { return array( 'production-ppcp', 'production-express_checkout', @@ -240,12 +205,12 @@ return array( 'sandbox-express_checkout', ); }, - 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { + 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { return new OnboardingSendOnlyNoticeRenderer( $container->get( 'wcgateway.send-only-message' ) ); }, - 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { + 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { $partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' ); $partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' ); $partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' ); @@ -261,14 +226,14 @@ return array( $logger ); }, - 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { + 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { return new OnboardingOptionsRenderer( $container->get( 'onboarding.url' ), $container->get( 'api.shop.country' ), $container->get( 'wcgateway.settings' ) ); }, - 'onboarding.rest' => static function( $container ) : OnboardingRESTController { + 'onboarding.rest' => static function( $container ) : OnboardingRESTController { return new OnboardingRESTController( $container ); }, ); From 4e46f6f5e9c3c164cd4dcf4be9bdab60b7b30484 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 8 Nov 2024 17:18:30 +0100 Subject: [PATCH 03/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Mark=20base=20class?= =?UTF-8?q?=20as=20abstract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Endpoint/RestEndpoint.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 08191276b..35b2912fe 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -13,11 +13,8 @@ use WC_REST_Controller; /** * Base class for REST controllers in the settings module. - * - * This is a base class for specific REST endpoints; do not instantiate this - * class directly. */ -class RestEndpoint extends WC_REST_Controller { +abstract class RestEndpoint extends WC_REST_Controller { /** * Endpoint namespace. * From 3a5b66089779273470f015f3436aa8aa54c51e83 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 11 Nov 2024 17:37:47 +0100 Subject: [PATCH 04/66] =?UTF-8?q?=F0=9F=9A=A7=20Create=20new=20connection?= =?UTF-8?q?=20URL=20generator=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/PartnerReferrals.php | 10 +- .../src/Service/ConnectionUrlGenerator.php | 218 ++++++++++++++++++ 2 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php diff --git a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php index 0f188f025..c7f5ec131 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php @@ -85,8 +85,7 @@ class PartnerReferrals { $error = new RuntimeException( __( 'Could not create referral.', 'woocommerce-paypal-payments' ) ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, @@ -95,6 +94,7 @@ class PartnerReferrals { ); throw $error; } + $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 201 !== $status_code ) { @@ -102,8 +102,7 @@ class PartnerReferrals { $json, $status_code ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, @@ -122,8 +121,7 @@ class PartnerReferrals { $error = new RuntimeException( __( 'Action URL not found.', 'woocommerce-paypal-payments' ) ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php new file mode 100644 index 000000000..ede39c093 --- /dev/null +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -0,0 +1,218 @@ +partner_referrals = $partner_referrals; + $this->referrals_data = $referrals_data; + $this->cache = $cache; + $this->environment = $environment; + $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. + * + * This function creates a URL for merchants to sign up for PayPal services. + * 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. + * + * @return string The generated PayPal onboarding URL. + */ + public function generate( array $products = array() ) : string { + $cache_key = $this->cache_key( $products ); + $user_id = get_current_user_id(); + $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, $user_id ); + + if ( $this->try_load_from_cache( $onboarding_url, $cache_key ) ) { + return $onboarding_url->get(); + } + + $this->logger->info( 'Generating onboarding URL for: ' . $cache_key ); + + $url = $this->generate_new_url( $products, $onboarding_url, $cache_key ); + + if ( $url ) { + $this->persist_url( $onboarding_url, $url ); + } + + return $url; + } + + /** + * Generates a cache key from the environment and sorted product array. + * + * @param array $products Product identifiers that are part of the cache key. + * + * @return string The cache key, defining the product list and environment. + */ + protected function cache_key( array $products = array() ) : string { + // Sort products alphabetically, to improve cache implementation. + sort( $products ); + + return $this->environment() . '-' . implode( '-', $products ); + } + + /** + * Attempts to load the URL from cache. + * + * @param OnboardingUrl $onboarding_url The OnboardingUrl object. + * @param string $cache_key The cache key. + * + * @return bool True if loaded from cache, false otherwise. + */ + protected function try_load_from_cache( OnboardingUrl $onboarding_url, string $cache_key ) : bool { + try { + if ( $onboarding_url->load() ) { + $this->logger->debug( 'Loaded onboarding URL from cache: ' . $cache_key ); + + return true; + } + } catch ( Exception $e ) { + // No problem, we'll generate a new URL + } + + return false; + } + + /** + * Generates a new 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 { + $onboarding_url->init(); + + try { + $onboarding_token = $onboarding_url->token(); + } catch ( Exception $e ) { + $this->logger->warning( 'Could not generate an onboarding token for: ' . $cache_key ); + + return ''; + } + + $data = $this->prepare_referral_data( $products, $onboarding_token ); + + try { + $url = $this->partner_referrals->signup_link( $data ); + } catch ( Exception $e ) { + $this->logger->warning( 'Could not generate an onboarding URL for: ' . $cache_key ); + + return ''; + } + + return add_query_arg( array( 'displayMode' => 'minibrowser' ), $url ); + } + + /** + * Prepares the referral data. + * + * @param array $products The products array. + * @param string $onboarding_token The onboarding token. + * + * @return array The prepared referral data. + */ + protected function prepare_referral_data( array $products, string $onboarding_token ) : array { + $data = $this->referrals_data + ->with_products( $products ) + ->data(); + + return $this->referrals_data->append_onboarding_token( $data, $onboarding_token ); + } + + /** + * Persists the generated URL. + * + * @param OnboardingUrl $onboarding_url The OnboardingUrl object. + * @param string $url The URL to persist. + */ + protected function persist_url( OnboardingUrl $onboarding_url, string $url ) { + $onboarding_url->set( $url ); + $onboarding_url->persist(); + } +} From 268ab3a27a55777328ad1abfb2b618e72149dd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Wed, 13 Nov 2024 15:04:34 +0100 Subject: [PATCH 05/66] Added saving of credentials and payee info --- .../Endpoint/ConnectManualRestEndpoint.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php index af81b90ca..5b7647aa9 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php @@ -16,6 +16,7 @@ use stdClass; 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\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WP_REST_Server; @@ -134,6 +135,18 @@ class ConnectManualRestEndpoint extends RestEndpoint { ); } + $settings = new GeneralSettings(); + if ( $use_sandbox ) { + $settings->set_is_sandbox( true ); + $settings->set_sandbox_client_id( $client_id ); + $settings->set_sandbox_client_secret( $client_secret ); + } else { + $settings->set_is_sandbox( false ); + $settings->set_live_client_id( $client_id ); + $settings->set_live_client_secret( $client_secret ); + } + $settings->save(); + try { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); } catch ( Exception $exception ) { @@ -146,6 +159,15 @@ class ConnectManualRestEndpoint extends RestEndpoint { } + if ( $use_sandbox ) { + $settings->set_sandbox_merchant_id( $payee->merchant_id ); + $settings->set_sandbox_merchant_email( $payee->email_address ); + } else { + $settings->set_live_merchant_id( $payee->merchant_id ); + $settings->set_live_merchant_email( $payee->email_address ); + } + $settings->save(); + $result = array( 'merchantId' => $payee->merchant_id, 'email' => $payee->email_address, From 0fc35f8b5e7c114d950ad31f1132b01d8a2b49c9 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 14 Nov 2024 16:59:29 +0100 Subject: [PATCH 06/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20Connection?= =?UTF-8?q?UrlGenerator=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Service/ConnectionUrlGenerator.php | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php index ede39c093..6e91aba3a 100644 --- a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -7,12 +7,15 @@ declare( strict_types = 1 ); +namespace WooCommerce\PayPalCommerce\Settings\Service; + +use Exception; use Psr\Log\LoggerInterface; -use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; -use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; -use WooCommerce\WooCommerce\Logging\Logger\NullLogger; -use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; +use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; +use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; +use WooCommerce\WooCommerce\Logging\Logger\NullLogger; /** * Generator that builds the ISU connection URL. @@ -63,6 +66,7 @@ class ConnectionUrlGenerator { * @param Cache $cache The cache object used for storing and * retrieving data. * @param string $environment Environment that is used to generate the URL. + * ['production'|'sandbox']. * @param ?LoggerInterface $logger The logger object for logging messages. */ public function __construct( @@ -104,9 +108,12 @@ class ConnectionUrlGenerator { $cache_key = $this->cache_key( $products ); $user_id = get_current_user_id(); $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, $user_id ); + $cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key ); - if ( $this->try_load_from_cache( $onboarding_url, $cache_key ) ) { - return $onboarding_url->get(); + if ( $cached_url ) { + $this->logger->info( 'Using cached onboarding URL for: ' . $cache_key ); + + return $cached_url; } $this->logger->info( 'Generating onboarding URL for: ' . $cache_key ); @@ -140,20 +147,21 @@ class ConnectionUrlGenerator { * @param OnboardingUrl $onboarding_url The OnboardingUrl object. * @param string $cache_key The cache key. * - * @return bool True if loaded from cache, false otherwise. + * @return string The cached URL, or an empty string if no URL is found. */ - protected function try_load_from_cache( OnboardingUrl $onboarding_url, string $cache_key ) : bool { + protected function try_get_from_cache( OnboardingUrl $onboarding_url, string $cache_key ) : string { try { if ( $onboarding_url->load() ) { $this->logger->debug( 'Loaded onboarding URL from cache: ' . $cache_key ); - return true; + return $onboarding_url->get(); } } catch ( Exception $e ) { - // No problem, we'll generate a new URL + // No problem, return an empty string to generate a new URL. + $this->logger->warning( 'Failed to load onboarding URL from cache: ' . $cache_key ); } - return false; + return ''; } /** @@ -166,6 +174,7 @@ class ConnectionUrlGenerator { * @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 { + $query_args = array( 'displayMode' => 'minibrowser' ); $onboarding_url->init(); try { @@ -186,7 +195,7 @@ class ConnectionUrlGenerator { return ''; } - return add_query_arg( array( 'displayMode' => 'minibrowser' ), $url ); + return add_query_arg( $query_args, $url ); } /** @@ -211,7 +220,7 @@ class ConnectionUrlGenerator { * @param OnboardingUrl $onboarding_url The OnboardingUrl object. * @param string $url The URL to persist. */ - protected function persist_url( OnboardingUrl $onboarding_url, string $url ) { + protected function persist_url( OnboardingUrl $onboarding_url, string $url ) : void { $onboarding_url->set( $url ); $onboarding_url->persist(); } From c13680b7f5ae2d992b2c6bd30da13ed88d16d50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=BCsken?= Date: Fri, 15 Nov 2024 09:01:49 +0100 Subject: [PATCH 07/66] Use DI for General Settings and store only after verification --- modules/ppcp-settings/services.php | 7 +++- .../Endpoint/ConnectManualRestEndpoint.php | 40 ++++++++++--------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 80df22370..5b8b5e282 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -9,6 +9,7 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; +use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; @@ -50,7 +51,8 @@ return array( return new ConnectManualRestEndpoint( $container->get( 'api.paypal-host-production' ), $container->get( 'api.paypal-host-sandbox' ), - $container->get( 'woocommerce.logger.woocommerce' ) + $container->get( 'woocommerce.logger.woocommerce' ), + $container->get( 'settings.general' ) ); }, 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array { @@ -109,4 +111,7 @@ return array( return in_array( $country, $eligible_countries, true ); }, + 'settings.general' => static function ( ContainerInterface $container ) : GeneralSettings { + return new GeneralSettings(); + }, ); diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php index 5b7647aa9..23f90589e 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php @@ -56,6 +56,13 @@ class ConnectManualRestEndpoint extends RestEndpoint { */ protected $rest_base = 'connect_manual'; + /** + * Settings instance. + * + * @var GeneralSettings + */ + private $settings = null; + /** * Field mapping for request. * @@ -82,16 +89,19 @@ class ConnectManualRestEndpoint extends RestEndpoint { * @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. */ public function __construct( string $live_host, string $sandbox_host, - LoggerInterface $logger + LoggerInterface $logger, + GeneralSettings $settings ) { $this->live_host = $live_host; $this->sandbox_host = $sandbox_host; $this->logger = $logger; + $this->settings = $settings; } /** @@ -135,18 +145,6 @@ class ConnectManualRestEndpoint extends RestEndpoint { ); } - $settings = new GeneralSettings(); - if ( $use_sandbox ) { - $settings->set_is_sandbox( true ); - $settings->set_sandbox_client_id( $client_id ); - $settings->set_sandbox_client_secret( $client_secret ); - } else { - $settings->set_is_sandbox( false ); - $settings->set_live_client_id( $client_id ); - $settings->set_live_client_secret( $client_secret ); - } - $settings->save(); - try { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); } catch ( Exception $exception ) { @@ -160,13 +158,19 @@ class ConnectManualRestEndpoint extends RestEndpoint { } if ( $use_sandbox ) { - $settings->set_sandbox_merchant_id( $payee->merchant_id ); - $settings->set_sandbox_merchant_email( $payee->email_address ); + $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 { - $settings->set_live_merchant_id( $payee->merchant_id ); - $settings->set_live_merchant_email( $payee->email_address ); + $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 ); } - $settings->save(); + $this->settings->save(); $result = array( 'merchantId' => $payee->merchant_id, From 9d01d2444fbae9f0b12b5f8c3fdb51d8242a6c59 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 14 Nov 2024 17:13:01 +0100 Subject: [PATCH 08/66] =?UTF-8?q?=F0=9F=A7=B1=20Add=20DI=20services=20for?= =?UTF-8?q?=20ConnectionUrlGenerator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 37 +++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 80df22370..34fecd8ee 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -9,10 +9,12 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; -use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; +use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -109,4 +111,33 @@ return array( return in_array( $country, $eligible_countries, true ); }, + 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache { + return new Cache( 'ppcp-paypal-signup-link' ); + }, + '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' ), + ), + ); + + $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' ), + $container->get( 'settings.service.signup-link-cache' ), + $environment, + $container->get( 'woocommerce.logger.woocommerce' ) + ); + } + + return $generators; + }, ); From f6b1d7a3cb1d8805041f64e31a13d1e58820a90a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 15 Nov 2024 14:03:57 +0100 Subject: [PATCH 09/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20REST=20en?= =?UTF-8?q?dpoint=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/SettingsModule.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index c2e688c7f..8c737f5ac 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -9,12 +9,11 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; -use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; -use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; /** * Class SettingsModule @@ -109,13 +108,15 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_action( 'rest_api_init', static function () use ( $container ) : void { - $onboarding_endpoint = $container->get( 'settings.rest.onboarding' ); - assert( $onboarding_endpoint instanceof OnboardingRestEndpoint ); - $onboarding_endpoint->register_routes(); + $endpoints = array( + $container->get( 'settings.rest.onboarding' ), + $container->get( 'settings.rest.connect_manual' ), + ); - $connect_manual_endpoint = $container->get( 'settings.rest.connect_manual' ); - assert( $connect_manual_endpoint instanceof ConnectManualRestEndpoint ); - $connect_manual_endpoint->register_routes(); + foreach ( $endpoints as $endpoint ) { + assert( $endpoint instanceof RestEndpoint ); + $endpoint->register_routes(); + } } ); From fe7dd9a65383fc38c3e7ef8dada801bd1a3d5091 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 15 Nov 2024 14:05:52 +0100 Subject: [PATCH 10/66] =?UTF-8?q?=E2=9C=A8=20Add=20LoginLinkRestEndpoint?= =?UTF-8?q?=20for=20merchant=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/services.php | 6 + .../src/Endpoint/LoginLinkRestEndpoint.php | 104 ++++++++++++++++++ modules/ppcp-settings/src/SettingsModule.php | 1 + 3 files changed, 111 insertions(+) create mode 100644 modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 34fecd8ee..9db70b4c3 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint; return array( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -55,6 +56,11 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint { + return new LoginLinkRestEndpoint( + $container->get( 'settings.service.connection-url-generators' ), + ); + }, 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array { return array( 'AR', diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php new file mode 100644 index 000000000..cb969c7ca --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -0,0 +1,104 @@ +url_generators = $url_generators; + } + + /** + * Configure REST API routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_login_url' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'products' => array( + 'required' => true, + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'sanitize_callback' => function ( $products ) { + return array_map( 'sanitize_text_field', $products ); + }, + ), + ), + ), + ) + ); + } + + /** + * Returns the full login URL for the requested environment and products. + * + * @param WP_REST_Request $request The request object. + * + * @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' ); + $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 ); + + return rest_ensure_response( $url ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( 'error' => $e->getMessage() ), + 500 + ); + } + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index 8c737f5ac..03a3a5e85 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -111,6 +111,7 @@ class SettingsModule implements ServiceModule, ExecutableModule { $endpoints = array( $container->get( 'settings.rest.onboarding' ), $container->get( 'settings.rest.connect_manual' ), + $container->get( 'settings.rest.login_link' ), ); foreach ( $endpoints as $endpoint ) { From b921a54e9fefb8af1b762dbfed891b18ac3a1b4e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 16:22:01 +0100 Subject: [PATCH 11/66] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20WordPress?= =?UTF-8?q?=20deps=20(lock=20file)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8d605e524..f808c687c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2671,9 +2671,9 @@ mime "^3.0.0" web-vitals "^4.2.1" -"@wordpress/element@^6.1.0": +"@wordpress/element@*", "@wordpress/element@^6.1.0": version "6.11.0" - resolved "https://registry.npmjs.org/@wordpress/element/-/element-6.11.0.tgz" + resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-6.11.0.tgz#7bc3e453a95bb806a707b4dc617373afa108af19" integrity sha512-UvHFYkT+EEaXEyEfw+iqLHRO9OwBjjsUydEMHcqntzkNcsYeAbmaL9V8R9ikXHLe6ftdbkwoXgF85xVPhVsL+Q== dependencies: "@babel/runtime" "7.25.7" @@ -2715,6 +2715,15 @@ globals "^13.12.0" requireindex "^1.2.0" +"@wordpress/icons@^10.11.0": + version "10.11.0" + resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-10.11.0.tgz#0beedef8ee49c135412fb81fc59440dd48d652aa" + integrity sha512-RMetpFwUIeh3sVj2+p6+QX5AW8pF7DvQzxH9jUr8YjaF2iLE64vy6m0cZz/X8xkSktHrXMuPJIr7YIVF20TEyw== + dependencies: + "@babel/runtime" "7.25.7" + "@wordpress/element" "*" + "@wordpress/primitives" "*" + "@wordpress/jest-console@*": version "8.11.0" resolved "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.11.0.tgz" @@ -2749,6 +2758,15 @@ resolved "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz" integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw== +"@wordpress/primitives@*": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-4.11.0.tgz#7bc24c07ed11057340832791c1c21e75a5181194" + integrity sha512-CoBXbh0mOSxcZtuzL7gK3RVumFx71DXQBfd3IkbRHuuVxa+2hI4KDuFyomSsbjQDshHsfuVrKUvuT3UGt6pdpQ== + dependencies: + "@babel/runtime" "7.25.7" + "@wordpress/element" "*" + clsx "^2.1.1" + "@wordpress/scripts@~30.0.0": version "30.0.6" resolved "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.0.6.tgz" @@ -3727,6 +3745,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.npmjs.org/co/-/co-4.6.0.tgz" From 4dd31965ea2ac48e4da4b8097f13e50b8aa6c32e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 16:23:54 +0100 Subject: [PATCH 12/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20the=20main?= =?UTF-8?q?=20store-generation=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/store.js | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/store.js b/modules/ppcp-settings/resources/js/data/store.js index a4acaf548..01b713cc5 100644 --- a/modules/ppcp-settings/resources/js/data/store.js +++ b/modules/ppcp-settings/resources/js/data/store.js @@ -1,20 +1,23 @@ import { createReduxStore, register, combineReducers } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; import { STORE_NAME } from './constants'; -import * as onboarding from './onboarding'; + +// Import Redux modules. +import * as Onboarding from './onboarding'; const actions = {}; const selectors = {}; const resolvers = {}; -[ onboarding ].forEach( ( item ) => { - Object.assign( actions, { ...item.actions } ); - Object.assign( selectors, { ...item.selectors } ); - Object.assign( resolvers, { ...item.resolvers } ); +[ Onboarding ].forEach( ( item ) => { + Object.assign( actions, { ...( item.actions || {} ) } ); + Object.assign( selectors, { ...( item.selectors || {} ) } ); + Object.assign( resolvers, { ...( item.resolvers || {} ) } ); + Object.assign( controls, { ...( item.controls || {} ) } ); } ); const reducer = combineReducers( { - onboarding: onboarding.reducer, + [ Onboarding.STORE_KEY ]: Onboarding.reducer, } ); export const initStore = () => { @@ -28,31 +31,53 @@ export const initStore = () => { register( store ); - /* eslint-disable no-console */ - // Provide a debug tool to inspect the Redux store via the JS console. - if ( window.ppcpSettings?.debug && console?.groupCollapsed ) { - window.ppcpSettings.dumpStore = () => { - const storeSelector = `wp.data.select('${ STORE_NAME }')`; - console.group( `[STORE] ${ storeSelector }` ); - - const storeState = wp.data.select( STORE_NAME ); - Object.keys( selectors ).forEach( ( selector ) => { - console.groupCollapsed( `[SELECTOR] .${ selector }()` ); - console.table( storeState[ selector ]() ); - console.groupEnd(); - } ); - - console.groupEnd(); - }; - window.ppcpSettings.resetStore = () => { - wp.data.dispatch( STORE_NAME ).resetOnboarding(); - wp.data.dispatch( STORE_NAME ).persist(); - }; - window.ppcpSettings.startOnboarding = () => { - wp.data.dispatch( STORE_NAME ).setCompleted( false ); - wp.data.dispatch( STORE_NAME ).setOnboardingStep( 0 ); - wp.data.dispatch( STORE_NAME ).persist(); - }; - } - /* eslint-enable no-console */ + addDebugTools(); +}; + +const addDebugTools = () => { + const context = window.ppcpSettings; + + // Provide a debug tool to inspect the Redux store via the JS console. + if ( ! context?.debug ) { + return; + } + + const getSelectors = () => wp.data.select( STORE_NAME ); + const getActions = () => wp.data.dispatch( STORE_NAME ); + + context.dumpStore = () => { + /* eslint-disable no-console */ + if ( ! console?.groupCollapsed ) { + console.error( 'console.groupCollapsed is not supported.' ); + return; + } + + const storeSelector = `wp.data.select('${ STORE_NAME }')`; + console.group( `[STORE] ${ storeSelector }` ); + + const select = getSelectors(); + Object.keys( selectors ).forEach( ( selector ) => { + console.groupCollapsed( `[SELECTOR] .${ selector }()` ); + console.table( select[ selector ]() ); + console.groupEnd(); + } ); + + console.groupEnd(); + /* eslint-enable no-console */ + }; + + context.resetStore = () => { + const dispatch = getActions(); + + dispatch.resetOnboarding(); + dispatch.persist(); + }; + + context.startOnboarding = () => { + const dispatch = getActions(); + + dispatch.setCompleted( false ); + dispatch.setOnboardingStep( 0 ); + dispatch.persist(); + }; }; From cf3798f610f07ca7e2db240bb15befa20a046633 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 16:25:57 +0100 Subject: [PATCH 13/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Introduce=20new=20ST?= =?UTF-8?q?ORE=5FKEY=20const?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/constants.js | 10 ++++++++++ .../resources/js/data/onboarding/index.js | 3 ++- .../resources/js/data/onboarding/selectors.js | 12 +++++++----- 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/onboarding/constants.js diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js new file mode 100644 index 000000000..638b329df --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -0,0 +1,10 @@ +/** + * Name of the module-store in the main Redux store. + * Helps to isolate data. + * + * Used by: Reducer, Selector, Index + * + * @type {string} + */ +export const STORE_KEY = 'onboarding'; + diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js index 0b07abf46..4f5ec7ed1 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/index.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js @@ -1,6 +1,7 @@ +import { STORE_KEY } from './constants'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; -export { reducer, selectors, actions, resolvers }; +export { reducer, selectors, actions, resolvers, STORE_KEY }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index b7721b992..eb329c7d4 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -1,22 +1,24 @@ +import { STORE_KEY } from './constants'; + const EMPTY_OBJ = Object.freeze( {} ); -const getOnboardingState = ( state ) => { +const getState = ( state ) => { if ( ! state ) { return EMPTY_OBJ; } - return state.onboarding || EMPTY_OBJ; + return state[ STORE_KEY ] || EMPTY_OBJ; }; export const getPersistentData = ( state ) => { - return getOnboardingState( state ).data || EMPTY_OBJ; + return getState( state ).data || EMPTY_OBJ; }; export const getTransientData = ( state ) => { - const { data, flags, ...transientState } = getOnboardingState( state ); + const { data, flags, ...transientState } = getState( state ); return transientState || EMPTY_OBJ; }; export const getFlags = ( state ) => { - return getOnboardingState( state ).flags || EMPTY_OBJ; + return getState( state ).flags || EMPTY_OBJ; }; From 84d2af5f3914501e380b8f24a80424ca1dccd7bc Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 16:31:05 +0100 Subject: [PATCH 14/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20onboardin?= =?UTF-8?q?g=20actions=20to=20use=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/actions.js | 81 +++++-------------- .../resources/js/data/onboarding/constants.js | 19 +++++ .../resources/js/data/onboarding/controls.js | 64 +++++++++++++++ .../resources/js/data/onboarding/index.js | 3 +- 4 files changed, 105 insertions(+), 62 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/onboarding/controls.js diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 09229e63e..9fc03b24e 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -1,7 +1,10 @@ -import { select } from '@wordpress/data'; -import { apiFetch } from '@wordpress/data-controls'; import ACTION_TYPES from './action-types'; -import { NAMESPACE, STORE_NAME } from '../constants'; + +/** + * @typedef {Object} Action An action object that is handled by a reducer or control. + * @property {string} type - The action type. + * @property {Object?} payload - Optional payload for the action. + */ /** * Special. Resets all values in the onboarding store to initial defaults. @@ -169,67 +172,23 @@ export const setProducts = ( products ) => { }; /** - * Attempts to establish a connection using client ID and secret via the server-side - * connection endpoint. + * Side effect. Triggers the persistence of onboarding data to the server. * - * @return {Object} The server response object + * @return {Action} The action. */ -export function* connectViaIdAndSecret() { - let result = null; - - try { - const path = `${ NAMESPACE }/connect_manual`; - const { clientId, clientSecret, useSandbox } = - yield select( STORE_NAME ).getPersistentData(); - - yield setManualConnectionIsBusy( true ); - - result = yield apiFetch( { - path, - method: 'POST', - data: { - clientId, - clientSecret, - useSandbox, - }, - } ); - } catch ( e ) { - result = { - success: false, - error: e, - }; - } finally { - yield setManualConnectionIsBusy( false ); - } - - return result; -} +export const persist = () => { + return { + type: ACTION_TYPES.DO_PERSIST_DATA, + }; +}; /** - * Saves the persistent details to the WP database. + * Side effect. Initiates a manual connection attempt using the provided client ID and secret. * - * @return {boolean} True, if the values were successfully saved. + * @return {Action} The action. */ -export function* persist() { - let error = null; - - try { - const path = `${ NAMESPACE }/onboarding`; - const data = select( STORE_NAME ).getPersistentData(); - - yield setIsSaving( true ); - - yield apiFetch( { - path, - method: 'post', - data, - } ); - } catch ( e ) { - error = e; - console.error( 'Error saving progress.', e ); - } finally { - yield setIsSaving( false ); - } - - return error === null; -} +export const connectViaIdAndSecret = () => { + return { + type: ACTION_TYPES.DO_MANUAL_CONNECTION, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js index 638b329df..a890d9c85 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -8,3 +8,22 @@ */ export const STORE_KEY = 'onboarding'; +/** + * REST path to persist data of this module to the WP DB. + * + * Used by: Controls + * See: OnboardingRestEndpoint.php + * + * @type {string} + */ +export const REST_PERSIST_PATH = 'onboarding'; + +/** + * REST path to perform the manual connection check, using client ID and secret, + * + * Used by: Controls + * See: ConnectManualRestEndpoint.php + * + * @type {string} + */ +export const REST_MANUAL_CONNECTION_PATH = 'connect_manual'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/controls.js b/modules/ppcp-settings/resources/js/data/onboarding/controls.js new file mode 100644 index 000000000..abe9f73f9 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/onboarding/controls.js @@ -0,0 +1,64 @@ +import { select } from '@wordpress/data'; +import { apiFetch } from '@wordpress/api-fetch'; + +import { NAMESPACE, STORE_NAME } from '../constants'; +import { REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH } from './constants'; +import ACTION_TYPES from './action-types'; +import { setIsSaving, setManualConnectionIsBusy } from './actions'; + +export const controls = { + [ ACTION_TYPES.DO_PERSIST_DATA ]: async ( { dispatch } ) => { + let error = null; + + try { + const path = `${ NAMESPACE }/${ REST_PERSIST_PATH }`; + const data = select( STORE_NAME ).onboardingPersistentData(); + + dispatch( setIsSaving( true ) ); + + await apiFetch( { + path, + method: 'post', + data, + } ); + } catch ( e ) { + error = e; + console.error( 'Error saving progress.', e ); + } finally { + dispatch( setIsSaving( false ) ); + } + + return error === null; + }, + + [ ACTION_TYPES.DO_MANUAL_CONNECTION ]: async ( { dispatch } ) => { + let result = null; + + try { + const path = `${ NAMESPACE }/${ REST_MANUAL_CONNECTION_PATH }`; + const { clientId, clientSecret, useSandbox } = + select( STORE_NAME ).onboardingPersistentData(); + + dispatch( setManualConnectionIsBusy( true ) ); + + result = await apiFetch( { + path, + method: 'POST', + data: { + clientId, + clientSecret, + useSandbox, + }, + } ); + } catch ( e ) { + result = { + success: false, + error: e, + }; + } finally { + dispatch( setManualConnectionIsBusy( false ) ); + } + + return result; + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js index 4f5ec7ed1..a8c685d37 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/index.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js @@ -3,5 +3,6 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; +import { controls } from './controls'; -export { reducer, selectors, actions, resolvers, STORE_KEY }; +export { reducer, selectors, actions, resolvers, controls, STORE_KEY }; From ab8f8e81ae270160f0c063268f91bf57d26bb38d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 16:32:45 +0100 Subject: [PATCH 15/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20REST=20paths?= =?UTF-8?q?=20to=20constants.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/constants.js | 10 ++++++++++ .../resources/js/data/onboarding/resolvers.js | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js index a890d9c85..2be3f6717 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -8,6 +8,16 @@ */ export const STORE_KEY = 'onboarding'; +/** + * REST path to hydrate data of this module by loading data from the WP DB.. + * + * Used by: Resolvers + * See: OnboardingRestEndpoint.php + * + * @type {string} + */ +export const REST_HYDRATE_PATH = 'onboarding'; + /** * REST path to persist data of this module to the WP DB. * diff --git a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js index 18f2a7528..c1a647804 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js @@ -2,13 +2,14 @@ import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; import { NAMESPACE } from '../constants'; +import { REST_HYDRATE_PATH } from './constants'; import { setIsReady, setOnboardingDetails } from './actions'; /** * Retrieve settings from the site's REST API. */ export function* getPersistentData() { - const path = `${ NAMESPACE }/onboarding`; + const path = `${ NAMESPACE }/${ REST_HYDRATE_PATH }`; try { const result = yield apiFetch( { path } ); From f272da5a5aa369b589ca788be5631f5f4f8f5e6e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 16:33:50 +0100 Subject: [PATCH 16/66] =?UTF-8?q?=F0=9F=9A=A7=20Add=20missing=20action=20t?= =?UTF-8?q?ypes=20for=20new=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/action-types.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index 39472e2ff..fb387e29c 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -16,4 +16,8 @@ export default { SET_CLIENT_SECRET: 'SET_CLIENT_SECRET', SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER', SET_PRODUCTS: 'SET_PRODUCTS', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA', + DO_MANUAL_CONNECTION: 'ONBOARDING:DO_MANUAL_CONNECTION', }; From 81f45692f41d1fc2a8960df766ac51f736e22ff4 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 17:43:15 +0100 Subject: [PATCH 17/66] =?UTF-8?q?=E2=9C=A8=20Create=20helpers=20to=20simpl?= =?UTF-8?q?ify=20reducer=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/reducer.js | 59 +++++++-------- .../ppcp-settings/resources/js/data/utils.js | 75 +++++++++++++++++++ 2 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/utils.js diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 5c1f59263..9cdeb7018 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -1,22 +1,13 @@ +import { createReducer, createSetters } from '../utils'; import ACTION_TYPES from './action-types'; -const defaultState = { +// Store structure. + +const defaultTransient = { isReady: false, isSaving: false, isManualConnectionBusy: false, - // Data persisted to the server. - data: { - completed: false, - step: 0, - useSandbox: false, - useManualConnection: false, - clientId: '', - clientSecret: '', - isCasualSeller: null, // null value will uncheck both options in the UI. - products: [], - }, - // Read only values, provided by the server. flags: { canUseCasualSelling: false, @@ -25,29 +16,33 @@ const defaultState = { }, }; +const defaultPersistent = { + completed: false, + step: 0, + useSandbox: false, + useManualConnection: false, + clientId: '', + clientSecret: '', + isCasualSeller: null, // null value will uncheck both options in the UI. + products: [], +}; + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +const defaultState = { + ...defaultTransient, + data: { ...defaultPersistent }, +}; + export const onboardingReducer = ( state = defaultState, { type, ...action } ) => { - const setTransient = ( changes ) => { - const { data, ...transientChanges } = changes; - return { ...state, ...transientChanges }; - }; - - const setPersistent = ( changes ) => { - const validChanges = Object.keys( changes ).reduce( ( acc, key ) => { - if ( key in defaultState.data ) { - acc[ key ] = changes[ key ]; - } - return acc; - }, {} ); - - return { - ...state, - data: { ...state.data, ...validChanges }, - }; - }; - switch ( type ) { // Reset store to initial state. case ACTION_TYPES.RESET_ONBOARDING: diff --git a/modules/ppcp-settings/resources/js/data/utils.js b/modules/ppcp-settings/resources/js/data/utils.js new file mode 100644 index 000000000..2cec07815 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/utils.js @@ -0,0 +1,75 @@ +/** + * Updates an object with new values, filtering based on allowed keys. + * + * Helper method used by createSetters. + * + * @param {Object} oldObject The original object to update. + * @param {Object} newValues The new values to apply. + * @param {Object} allowedKeys An object whose keys define the allowed keys to update. + * @return {Object} A new object with the allowed updates applied. + */ +const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( { + ...oldObject, + ...Object.keys( newValues ).reduce( ( acc, key ) => { + if ( key in allowedKeys ) { + acc[ key ] = newValues[ key ]; + } + return acc; + }, {} ), +} ); + +/** + * Creates setter functions for updating state. + * + * Only properties that are present in the "defaultTransient" or "defaultPersistent" + * arguments can be updated by the setters. Make sure that the default state defines + * ALL possible properties. + * + * @param {Object} defaultTransient Object defining initial transient values. + * @param {Object} defaultPersistent Object defining initial persistent values. + * @return {[Function, Function]} An array containing setTransient and setPersistent functions. + */ +export const createSetters = ( defaultTransient, defaultPersistent ) => { + const setTransient = ( oldState, newValues ) => + updateObject( oldState, newValues, defaultTransient ); + + const setPersistent = ( oldState, newValues ) => ( { + ...oldState, + data: updateObject( oldState.data, newValues, defaultPersistent ), + } ); + + return [ setTransient, setPersistent ]; +}; + +/** + * Creates a reducer function with predefined action handlers. + * + * @param {Object} defaultTransient Object defining initial transient values. + * @param {Object} defaultPersistent Object defining initial persistent values. + * @param {Object} handlers An object mapping action types to handler functions. + * @return {Function} A reducer function. + */ +export const createReducer = ( + defaultTransient, + defaultPersistent, + handlers +) => { + if ( Object.hasOwnProperty.call( defaultTransient, 'data' ) ) { + throw new Error( + 'The transient state cannot contain a "data" property.' + ); + } + + const initialState = { + ...defaultTransient, + data: defaultPersistent, + }; + + return function reducer( state = initialState, action ) { + if ( Object.hasOwnProperty.call( handlers, action.type ) ) { + return handlers[ action.type ]( state, action.data ); + } + + return state; + }; +}; From fee9a016c602b69b0c230629f189d5fb85bb6b39 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 18:26:10 +0100 Subject: [PATCH 18/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20onboardin?= =?UTF-8?q?g=20state=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/data/onboarding/action-types.js | 18 +--- .../resources/js/data/onboarding/actions.js | 90 +++++++++---------- .../resources/js/data/onboarding/hooks.js | 52 ++++++----- .../resources/js/data/onboarding/reducer.js | 75 ++++------------ .../resources/js/data/onboarding/resolvers.js | 7 +- .../resources/js/data/onboarding/selectors.js | 6 +- 6 files changed, 100 insertions(+), 148 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index fb387e29c..f4834bbf5 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -1,21 +1,11 @@ export default { - RESET_ONBOARDING: 'RESET_ONBOARDING', - // Transient data. - SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY', - SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING', - SET_MANUAL_CONNECTION_BUSY: 'SET_MANUAL_CONNECTION_BUSY', + SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT', // Persistent data. - SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED', - SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS', - SET_ONBOARDING_STEP: 'SET_ONBOARDING_STEP', - SET_SANDBOX_MODE: 'SET_SANDBOX_MODE', - SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE', - SET_CLIENT_ID: 'SET_CLIENT_ID', - SET_CLIENT_SECRET: 'SET_CLIENT_SECRET', - SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER', - SET_PRODUCTS: 'SET_PRODUCTS', + SET_PERSISTENT: 'ONBOARDING:SET_PERSISTENT', + RESET: 'ONBOARDING:RESET', + HYDRATE: 'ONBOARDING:HYDRATE', // Controls - always start with "DO_". DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA', diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 9fc03b24e..22c9571e1 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -9,48 +9,48 @@ import ACTION_TYPES from './action-types'; /** * Special. Resets all values in the onboarding store to initial defaults. * - * @return {{type: string}} The action. + * @return {Action} The action. */ export const resetOnboarding = () => { - return { type: ACTION_TYPES.RESET_ONBOARDING }; + return { type: ACTION_TYPES.RESET }; }; /** - * Non-persistent. Marks the onboarding details as "ready", i.e., fully initialized. + * Transient. Marks the onboarding details as "ready", i.e., fully initialized. * * @param {boolean} isReady - * @return {{type: string, isReady}} The action. + * @return {Action} The action. */ export const setIsReady = ( isReady ) => { return { - type: ACTION_TYPES.SET_ONBOARDING_IS_READY, - isReady, + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, }; }; /** - * Non-persistent. Changes the "saving" flag. + * Transient. Changes the "saving" flag. * * @param {boolean} isSaving - * @return {{type: string, isSaving}} The action. + * @return {Action} The action. */ export const setIsSaving = ( isSaving ) => { return { - type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING, - isSaving, + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isSaving }, }; }; /** - * Non-persistent. Changes the "manual connection is busy" flag. + * Transient. Changes the "manual connection is busy" flag. * * @param {boolean} isBusy - * @return {{type: string, isBusy}} The action. + * @return {Action} The action. */ export const setManualConnectionIsBusy = ( isBusy ) => { return { - type: ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY, - isBusy, + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isBusy }, }; }; @@ -58,11 +58,11 @@ export const setManualConnectionIsBusy = ( isBusy ) => { * Persistent. Set the full onboarding details, usually during app initialization. * * @param {{data: {}, flags?: {}}} payload - * @return {{type: string, payload}} The action. + * @return {Action} The action. */ -export const setOnboardingDetails = ( payload ) => { +export const hydrateOnboardingDetails = ( payload ) => { return { - type: ACTION_TYPES.SET_ONBOARDING_DETAILS, + type: ACTION_TYPES.HYDRATE, payload, }; }; @@ -71,12 +71,12 @@ export const setOnboardingDetails = ( payload ) => { * Persistent.Set the "onboarding completed" flag which shows or hides the wizard. * * @param {boolean} completed - * @return {{type: string, payload}} The action. + * @return {Action} The action. */ export const setCompleted = ( completed ) => { return { - type: ACTION_TYPES.SET_ONBOARDING_COMPLETED, - completed, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { completed }, }; }; @@ -84,38 +84,38 @@ export const setCompleted = ( completed ) => { * Persistent. Sets the onboarding wizard to a new step. * * @param {number} step - * @return {{type: string, step}} An action. + * @return {Action} The action. */ export const setOnboardingStep = ( step ) => { return { - type: ACTION_TYPES.SET_ONBOARDING_STEP, - step, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { step }, }; }; /** * Persistent. Sets the sandbox mode on or off. * - * @param {boolean} sandboxMode - * @return {{type: string, useSandbox}} An action. + * @param {boolean} useSandbox + * @return {Action} The action. */ -export const setSandboxMode = ( sandboxMode ) => { +export const setSandboxMode = ( useSandbox ) => { return { - type: ACTION_TYPES.SET_SANDBOX_MODE, - useSandbox: sandboxMode, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { useSandbox }, }; }; /** * Persistent. Toggles the "Manual Connection" mode on or off. * - * @param {boolean} manualConnectionMode - * @return {{type: string, useManualConnection}} An action. + * @param {boolean} useManualConnection + * @return {Action} The action. */ -export const setManualConnectionMode = ( manualConnectionMode ) => { +export const setManualConnectionMode = ( useManualConnection ) => { return { - type: ACTION_TYPES.SET_MANUAL_CONNECTION_MODE, - useManualConnection: manualConnectionMode, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { useManualConnection }, }; }; @@ -123,12 +123,12 @@ export const setManualConnectionMode = ( manualConnectionMode ) => { * Persistent. Changes the "client ID" value. * * @param {string} clientId - * @return {{type: string, clientId}} The action. + * @return {Action} The action. */ export const setClientId = ( clientId ) => { return { - type: ACTION_TYPES.SET_CLIENT_ID, - clientId, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { clientId }, }; }; @@ -136,12 +136,12 @@ export const setClientId = ( clientId ) => { * Persistent. Changes the "client secret" value. * * @param {string} clientSecret - * @return {{type: string, clientSecret}} The action. + * @return {Action} The action. */ export const setClientSecret = ( clientSecret ) => { return { - type: ACTION_TYPES.SET_CLIENT_SECRET, - clientSecret, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { clientSecret }, }; }; @@ -149,12 +149,12 @@ export const setClientSecret = ( clientSecret ) => { * Persistent. Sets the "isCasualSeller" value. * * @param {boolean} isCasualSeller - * @return {{type: string, isCasualSeller}} The action. + * @return {Action} The action. */ export const setIsCasualSeller = ( isCasualSeller ) => { return { - type: ACTION_TYPES.SET_IS_CASUAL_SELLER, - isCasualSeller, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { isCasualSeller }, }; }; @@ -162,12 +162,12 @@ export const setIsCasualSeller = ( isCasualSeller ) => { * Persistent. Sets the "products" array. * * @param {string[]} products - * @return {{type: string, products}} The action. + * @return {Action} The action. */ export const setProducts = ( products ) => { return { - type: ACTION_TYPES.SET_PRODUCTS, - products, + type: ACTION_TYPES.SET_PERSISTENT, + payload: { products }, }; }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index ff9052d69..d74fa2010 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -1,7 +1,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; -import apiFetch from '@wordpress/api-fetch'; -import { NAMESPACE, PRODUCT_TYPES, STORE_NAME } from '../constants'; -import { getFlags } from './selectors'; + +import { PRODUCT_TYPES, STORE_NAME } from '../constants'; const useOnboardingDetails = () => { const { @@ -16,55 +15,60 @@ const useOnboardingDetails = () => { setProducts, } = useDispatch( STORE_NAME ); - // Transient accessors. - const isSaving = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isSaving; - }, [] ); - - const isReady = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isReady; - } ); - - const isManualConnectionBusy = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isManualConnectionBusy; - }, [] ); + const transientData = ( select ) => + select( STORE_NAME ).onboardingTransientData(); + const persistentData = ( select ) => + select( STORE_NAME ).onboardingPersistentData(); // Read-only flags. const flags = useSelect( ( select ) => { - return select( STORE_NAME ).getFlags(); + return select( STORE_NAME ).onboardingFlags(); } ); + // Transient accessors. + const isSaving = useSelect( ( select ) => { + return transientData( select ).isSaving; + }, [] ); + + const isReady = useSelect( ( select ) => { + return transientData( select ).isReady; + } ); + + const isManualConnectionBusy = useSelect( ( select ) => { + return transientData( select ).isManualConnectionBusy; + }, [] ); + // Persistent accessors. const step = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().step || 0; + return persistentData( select ).step || 0; } ); const completed = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().completed; + return persistentData( select ).completed; } ); const clientId = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().clientId; + return persistentData( select ).clientId; }, [] ); const clientSecret = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().clientSecret; + return persistentData( select ).clientSecret; }, [] ); const isSandboxMode = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().useSandbox; + return persistentData( select ).useSandbox; }, [] ); const isManualConnectionMode = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().useManualConnection; + return persistentData( select ).useManualConnection; }, [] ); const isCasualSeller = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().isCasualSeller; + return persistentData( select ).isCasualSeller; }, [] ); const products = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().products || []; + return persistentData( select ).products || []; }, [] ); const toggleProduct = ( list ) => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 9cdeb7018..a037928f3 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -34,69 +34,26 @@ const [ setTransient, setPersistent ] = createSetters( defaultPersistent ); -const defaultState = { - ...defaultTransient, - data: { ...defaultPersistent }, -}; +const onboardingReducer = createReducer( defaultTransient, defaultPersistent, { + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, { payload } ) => + setTransient( state, payload ), -export const onboardingReducer = ( - state = defaultState, - { type, ...action } -) => { - switch ( type ) { - // Reset store to initial state. - case ACTION_TYPES.RESET_ONBOARDING: - return setPersistent( defaultState.data ); + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, { payload } ) => + setPersistent( state, payload ), - // Transient data. - case ACTION_TYPES.SET_ONBOARDING_IS_READY: - return setTransient( { isReady: action.isReady } ); + [ ACTION_TYPES.RESET ]: ( state ) => + setPersistent( state, defaultPersistent ), - case ACTION_TYPES.SET_IS_SAVING_ONBOARDING: - return setTransient( { isSaving: action.isSaving } ); + [ ACTION_TYPES.HYDRATE ]: ( state, { payload } ) => { + const newState = setPersistent( payload ); - case ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY: - return setTransient( { isManualConnectionBusy: action.isBusy } ); + // Flags are not updated by `setPersistent()`. + if ( payload.flags ) { + newState.flags = { ...newState.flags, ...payload.flags }; + } - // Persistent data. - case ACTION_TYPES.SET_ONBOARDING_DETAILS: - const newState = setPersistent( action.payload.data ); - - if ( action.payload.flags ) { - newState.flags = { ...newState.flags, ...action.payload.flags }; - } - - return newState; - - case ACTION_TYPES.SET_ONBOARDING_COMPLETED: - return setPersistent( { completed: action.completed } ); - - case ACTION_TYPES.SET_CLIENT_ID: - return setPersistent( { clientId: action.clientId } ); - - case ACTION_TYPES.SET_CLIENT_SECRET: - return setPersistent( { clientSecret: action.clientSecret } ); - - case ACTION_TYPES.SET_ONBOARDING_STEP: - return setPersistent( { step: action.step } ); - - case ACTION_TYPES.SET_SANDBOX_MODE: - return setPersistent( { useSandbox: action.useSandbox } ); - - case ACTION_TYPES.SET_MANUAL_CONNECTION_MODE: - return setPersistent( { - useManualConnection: action.useManualConnection, - } ); - - case ACTION_TYPES.SET_IS_CASUAL_SELLER: - return setPersistent( { isCasualSeller: action.isCasualSeller } ); - - case ACTION_TYPES.SET_PRODUCTS: - return setPersistent( { products: action.products } ); - - default: - return state; - } -}; + return newState; + }, +} ); export default onboardingReducer; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js index c1a647804..7009ecff4 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js @@ -1,19 +1,20 @@ import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; + import { NAMESPACE } from '../constants'; import { REST_HYDRATE_PATH } from './constants'; -import { setIsReady, setOnboardingDetails } from './actions'; +import { setIsReady, hydrateOnboardingDetails } from './actions'; /** * Retrieve settings from the site's REST API. */ -export function* getPersistentData() { +export function* onboardingPersistentData() { const path = `${ NAMESPACE }/${ REST_HYDRATE_PATH }`; try { const result = yield apiFetch( { path } ); - yield setOnboardingDetails( result ); + yield hydrateOnboardingDetails( result ); yield setIsReady( true ); } catch ( e ) { yield dispatch( 'core/notices' ).createErrorNotice( diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index eb329c7d4..a2a16af13 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -10,15 +10,15 @@ const getState = ( state ) => { return state[ STORE_KEY ] || EMPTY_OBJ; }; -export const getPersistentData = ( state ) => { +export const onboardingPersistentData = ( state ) => { return getState( state ).data || EMPTY_OBJ; }; -export const getTransientData = ( state ) => { +export const onboardingTransientData = ( state ) => { const { data, flags, ...transientState } = getState( state ); return transientState || EMPTY_OBJ; }; -export const getFlags = ( state ) => { +export const onboardingFlags = ( state ) => { return getState( state ).flags || EMPTY_OBJ; }; From ef873dcda2704011f99730103973b0902297eb3e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 18:51:11 +0100 Subject: [PATCH 19/66] =?UTF-8?q?=F0=9F=92=A1=20Document=20all=20Redux=20s?= =?UTF-8?q?tore=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/action-types.js | 9 +++++++++ .../resources/js/data/onboarding/actions.js | 10 ++++++++++ .../resources/js/data/onboarding/controls.js | 9 +++++++++ .../resources/js/data/onboarding/hooks.js | 10 ++++++++++ .../resources/js/data/onboarding/reducer.js | 11 +++++++++++ .../resources/js/data/onboarding/resolvers.js | 10 ++++++++++ .../resources/js/data/onboarding/selectors.js | 10 ++++++++++ 7 files changed, 69 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index f4834bbf5..7a38dbb1b 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -1,3 +1,12 @@ +/** + * Action Types: Define unique identifiers for actions across all store modules. + * + * Keys are module-internal and can have any value. + * Values must be unique across all store modules to avoid collisions. + * + * @file + */ + export default { // Transient data. SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT', diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 22c9571e1..ac194252d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -1,3 +1,13 @@ +/** + * Action Creators: Define functions to create action objects. + * + * These functions update state or trigger side effects (e.g., async operations). + * Exported functions must have unique names across all store modules. + * Actions are categorized as Transient, Persistent, or Side effect. + * + * @file + */ + import ACTION_TYPES from './action-types'; /** diff --git a/modules/ppcp-settings/resources/js/data/onboarding/controls.js b/modules/ppcp-settings/resources/js/data/onboarding/controls.js index abe9f73f9..729df17c6 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/controls.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/controls.js @@ -1,3 +1,12 @@ +/** + * Controls: Implement side effects, typically asynchronous operations. + * + * Controls use ACTION_TYPES keys as identifiers to ensure uniqueness. + * They are triggered by corresponding actions and handle external interactions. + * + * @file + */ + import { select } from '@wordpress/data'; import { apiFetch } from '@wordpress/api-fetch'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index d74fa2010..9e5f1d179 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -1,3 +1,13 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * Exported hooks must have unique names across all store modules. + * + * @file + */ + import { useSelect, useDispatch } from '@wordpress/data'; import { PRODUCT_TYPES, STORE_NAME } from '../constants'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index a037928f3..41a57c578 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -1,3 +1,14 @@ +/** + * Reducer: Defines store structure and state updates for this module. + * + * Manages both transient (temporary) and persistent (saved) state. + * Each module uses isolated memory objects to prevent conflicts. + * The initial state must define all properties, as dynamic additions are not supported. + * Reducers are separated per module to avoid conflicts in state management. + * + * @file + */ + import { createReducer, createSetters } from '../utils'; import ACTION_TYPES from './action-types'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js index 7009ecff4..7d79e35e6 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js @@ -1,3 +1,13 @@ +/** + * Resolvers: Handle asynchronous data fetching for the store. + * + * These functions update store state with data from external sources. + * Each resolver corresponds to a specific selector but must have a unique name. + * Resolvers are called automatically when selectors request unavailable data. + * + * @file + */ + import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index a2a16af13..89c62a360 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -1,3 +1,13 @@ +/** + * Selectors: Extract specific pieces of state from the store. + * + * These functions provide a consistent interface for accessing store data. + * They allow components to retrieve data without knowing the store structure. + * Exported functions must have unique names across all store modules. + * + * @file + */ + import { STORE_KEY } from './constants'; const EMPTY_OBJ = Object.freeze( {} ); From 0f03a636b23c2f935da2d9609d21afdd2d4618a7 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 18 Nov 2024 18:58:54 +0100 Subject: [PATCH 20/66] =?UTF-8?q?=F0=9F=9A=A7=20Initial=20store-module=20f?= =?UTF-8?q?or=20common=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 19 ++++ .../resources/js/data/common/actions.js | 100 ++++++++++++++++++ .../resources/js/data/common/constants.js | 26 +++++ .../resources/js/data/common/controls.js | 32 ++++++ .../resources/js/data/common/hooks.js | 9 ++ .../resources/js/data/common/index.js | 8 ++ .../resources/js/data/common/reducer.js | 43 ++++++++ .../resources/js/data/common/resolvers.js | 37 +++++++ .../resources/js/data/common/selectors.js | 34 ++++++ .../resources/js/data/onboarding/reducer.js | 1 - 10 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 modules/ppcp-settings/resources/js/data/common/action-types.js create mode 100644 modules/ppcp-settings/resources/js/data/common/actions.js create mode 100644 modules/ppcp-settings/resources/js/data/common/constants.js create mode 100644 modules/ppcp-settings/resources/js/data/common/controls.js create mode 100644 modules/ppcp-settings/resources/js/data/common/hooks.js create mode 100644 modules/ppcp-settings/resources/js/data/common/index.js create mode 100644 modules/ppcp-settings/resources/js/data/common/reducer.js create mode 100644 modules/ppcp-settings/resources/js/data/common/resolvers.js create mode 100644 modules/ppcp-settings/resources/js/data/common/selectors.js diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js new file mode 100644 index 000000000..1645547d9 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -0,0 +1,19 @@ +/** + * Action Types: Define unique identifiers for actions across all store modules. + * + * Keys are module-internal and can have any value. + * Values must be unique across all store modules to avoid collisions. + * + * @file + */ + +export default { + // Transient data. + SET_TRANSIENT: 'COMMON:SET_TRANSIENT', + + // Persistent data. + SET_PERSISTENT: 'COMMON:SET_PERSISTENT', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', +}; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js new file mode 100644 index 000000000..3992e0ff5 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -0,0 +1,100 @@ +/** + * Action Creators: Define functions to create action objects. + * + * These functions update state or trigger side effects (e.g., async operations). + * Exported functions must have unique names across all store modules. + * Actions are categorized as Transient, Persistent, or Side effect. + * + * @file + */ + +import ACTION_TYPES from './action-types'; + +/** + * Transient. Marks the onboarding details as "ready", i.e., fully initialized. + * + * @param {boolean} isReady + * @return {{type: string, isReady: boolean}} The action. + */ +export const setIsReady = ( isReady ) => { + return { + type: ACTION_TYPES.SET_TRANSIENT, + isReady, + }; +}; + +/** + * Transient. Changes the "saving" flag. + * + * @param {boolean} isSaving + * @return {{type: string, isSaving: boolean}} The action. + */ +export const setIsSaving = ( isSaving ) => { + return { + type: ACTION_TYPES.SET_TRANSIENT, + isSaving, + }; +}; + +/** + * Persistent. Sets the sandbox mode on or off. + * + * @param {boolean} useSandbox + * @return {{type: string, useSandbox: boolean}} An action. + */ +export const setSandboxMode = ( useSandbox ) => { + return { + type: ACTION_TYPES.SET_PERSISTENT, + useSandbox, + }; +}; + +/** + * Persistent. Toggles the "Manual Connection" mode on or off. + * + * @param {boolean} useManualConnection + * @return {{type: string, useManualConnection: boolean}} An action. + */ +export const setManualConnectionMode = ( useManualConnection ) => { + return { + type: ACTION_TYPES.SET_PERSISTENT, + useManualConnection, + }; +}; + +/** + * Persistent. Changes the "client ID" value. + * + * @param {string} clientId + * @return {{type: string, clientId: string}} The action. + */ +export const setClientId = ( clientId ) => { + return { + type: ACTION_TYPES.SET_PERSISTENT, + clientId, + }; +}; + +/** + * Persistent. Changes the "client secret" value. + * + * @param {string} clientSecret + * @return {{type: string, clientSecret: string}} The action. + */ +export const setClientSecret = ( clientSecret ) => { + return { + type: ACTION_TYPES.SET_PERSISTENT, + clientSecret, + }; +}; + +/** + * Side effect. Saves the persistent details to the WP database. + * + * @return {{type: string}} The action. + */ +export function* commonPersist() { + return { + type: ACTION_TYPES.DO_PERSIST_DATA, + }; +} diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js new file mode 100644 index 000000000..365bfe305 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -0,0 +1,26 @@ +/** + * Name of the module-store in the main Redux store. + * + * Helps to isolate data, used by reducer and selectors. + * + * @type {string} + */ +export const STORE_KEY = 'common'; + +/** + * REST path to hydrate data of this module by loading data from the WP DB.. + * + * Used by resolvers. + * + * @type {string} + */ +export const REST_HYDRATE_PATH = 'common'; + +/** + * REST path to persist data of this module to the WP DB. + * + * Used by controls. + * + * @type {string} + */ +export const REST_PERSIST_PATH = 'common'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js new file mode 100644 index 000000000..45b3e6ea5 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -0,0 +1,32 @@ +/** + * Controls: Implement side effects, typically asynchronous operations. + * + * Controls use ACTION_TYPES keys as identifiers to ensure uniqueness. + * They are triggered by corresponding actions and handle external interactions. + * + * @file + */ + +import { select } from '@wordpress/data'; +import { apiFetch } from '@wordpress/api-fetch'; + +import { NAMESPACE, STORE_NAME } from '../constants'; +import { REST_PERSIST_PATH } from './constants'; +import ACTION_TYPES from './action-types'; +export const controls = { + [ ACTION_TYPES.DO_PERSIST_DATA ]: async () => { + const path = `${ NAMESPACE }/${ REST_PERSIST_PATH }`; + const data = select( STORE_NAME ).getPersistentData(); + + try { + return await apiFetch( { + path, + method: 'post', + data, + } ); + } catch ( error ) { + console.error( 'Error saving progress.', error ); + throw error; + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js new file mode 100644 index 000000000..a201c6d37 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -0,0 +1,9 @@ +/** + * Hooks: Provide the main API for components to interact with the store. + * + * These encapsulate store interactions, offering a consistent interface. + * Hooks simplify data access and manipulation for components. + * Exported hooks must have unique names across all store modules. + * + * @file + */ diff --git a/modules/ppcp-settings/resources/js/data/common/index.js b/modules/ppcp-settings/resources/js/data/common/index.js new file mode 100644 index 000000000..a8c685d37 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/index.js @@ -0,0 +1,8 @@ +import { STORE_KEY } from './constants'; +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import * as resolvers from './resolvers'; +import { controls } from './controls'; + +export { reducer, selectors, actions, resolvers, controls, STORE_KEY }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js new file mode 100644 index 000000000..583fd7540 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -0,0 +1,43 @@ +/** + * Reducer: Defines store structure and state updates for this module. + * + * Manages both transient (temporary) and persistent (saved) state. + * Each module uses isolated memory objects to prevent conflicts. + * The initial state must define all properties, as dynamic additions are not supported. + * + * @file + */ + +import { createReducer, createSetters } from '../utils'; +import ACTION_TYPES from './action-types'; + +// Store structure. + +const defaultTransient = { + isReady: false, + isSaving: false, +}; + +const defaultPersistent = { + useSandbox: false, + useManualConnection: false, + clientId: '', + clientSecret: '', +}; + +// Reducer logic. + +const [ setTransient, setPersistent ] = createSetters( + defaultTransient, + defaultPersistent +); + +const commonReducer = createReducer( defaultTransient, defaultPersistent, { + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, action ) => + setTransient( state, action ), + + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) => + setPersistent( state, action ), +} ); + +export default commonReducer; diff --git a/modules/ppcp-settings/resources/js/data/common/resolvers.js b/modules/ppcp-settings/resources/js/data/common/resolvers.js new file mode 100644 index 000000000..2358496be --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/resolvers.js @@ -0,0 +1,37 @@ +/** + * Resolvers: Handle asynchronous data fetching for the store. + * + * These functions update store state with data from external sources. + * Each resolver corresponds to a specific selector but must have a unique name. + * Resolvers are called automatically when selectors request unavailable data. + * + * @file + */ + +import { dispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { apiFetch } from '@wordpress/data-controls'; + +import { NAMESPACE } from '../constants'; +import { setIsReady, setCommonDetails } from './actions'; +import { REST_HYDRATE_PATH } from './constants'; + +/** + * Retrieve settings from the site's REST API. + */ +export function* commonPersistentData() { + const path = `${ NAMESPACE }/${ REST_HYDRATE_PATH }`; + + try { + const result = yield apiFetch( { path } ); + yield setCommonDetails( result ); + yield setIsReady( true ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + __( + 'Error retrieving plugin details.', + 'woocommerce-paypal-payments' + ) + ); + } +} diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js new file mode 100644 index 000000000..4ea6e86a7 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -0,0 +1,34 @@ +/** + * Selectors: Extract specific pieces of state from the store. + * + * These functions provide a consistent interface for accessing store data. + * They allow components to retrieve data without knowing the store structure. + * Exported functions must have unique names across all store modules. + * + * @file + */ + +import { STORE_KEY } from './constants'; + +const EMPTY_OBJ = Object.freeze( {} ); + +const getState = ( state ) => { + if ( ! state ) { + return EMPTY_OBJ; + } + + return state[ STORE_KEY ] || EMPTY_OBJ; +}; + +export const commonPersistentData = ( state ) => { + return getState( state ).data || EMPTY_OBJ; +}; + +export const commonTransientData = ( state ) => { + const { data, flags, ...transientState } = getState( state ); + return transientState || EMPTY_OBJ; +}; + +export const commonFlags = ( state ) => { + return getState( state ).flags || EMPTY_OBJ; +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 41a57c578..0f253fc02 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -4,7 +4,6 @@ * Manages both transient (temporary) and persistent (saved) state. * Each module uses isolated memory objects to prevent conflicts. * The initial state must define all properties, as dynamic additions are not supported. - * Reducers are separated per module to avoid conflicts in state management. * * @file */ From 650858a50e1ff605a1bd758a8de149270958a33a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:24:53 +0100 Subject: [PATCH 21/66] =?UTF-8?q?=F0=9F=90=9B=20Fix=20problem=20with=20mis?= =?UTF-8?q?sing=20notification=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/css/components/screens/_onboarding-global.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss b/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss index e98813d14..7878ef729 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss @@ -1,7 +1,8 @@ body:has(.ppcp-r-container--onboarding) { background-color: #fff !important; - .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout, .wrap.woocommerce form > h2, #screen-meta-links { + .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout__header, .wrap.woocommerce form > h2, #screen-meta-links { display: none !important; + visibility: hidden; } } From 4ae61dfb584b78c622683ee439bdb007cec34515 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:32:07 +0100 Subject: [PATCH 22/66] =?UTF-8?q?=F0=9F=90=9B=20Fix=20incorrect=20param=20?= =?UTF-8?q?name=20in=20reducer=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/utils.js b/modules/ppcp-settings/resources/js/data/utils.js index 2cec07815..45c652862 100644 --- a/modules/ppcp-settings/resources/js/data/utils.js +++ b/modules/ppcp-settings/resources/js/data/utils.js @@ -30,10 +30,10 @@ const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( { * @return {[Function, Function]} An array containing setTransient and setPersistent functions. */ export const createSetters = ( defaultTransient, defaultPersistent ) => { - const setTransient = ( oldState, newValues ) => + const setTransient = ( oldState, newValues = {} ) => updateObject( oldState, newValues, defaultTransient ); - const setPersistent = ( oldState, newValues ) => ( { + const setPersistent = ( oldState, newValues = {} ) => ( { ...oldState, data: updateObject( oldState.data, newValues, defaultPersistent ), } ); @@ -67,7 +67,7 @@ export const createReducer = ( return function reducer( state = initialState, action ) { if ( Object.hasOwnProperty.call( handlers, action.type ) ) { - return handlers[ action.type ]( state, action.data ); + return handlers[ action.type ]( state, action.payload ?? {} ); } return state; From 9e4d0c0819f827cdfae0814b3df474f63298fd6c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:42:18 +0100 Subject: [PATCH 23/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20debug=20logic?= =?UTF-8?q?=20to=20separate=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/debug.js | 51 +++++++++++++++++++ .../ppcp-settings/resources/js/data/index.js | 3 ++ .../ppcp-settings/resources/js/data/store.js | 50 ------------------ 3 files changed, 54 insertions(+), 50 deletions(-) create mode 100644 modules/ppcp-settings/resources/js/data/debug.js diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js new file mode 100644 index 000000000..96117b613 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -0,0 +1,51 @@ +import { OnboardingStoreName } from './index'; +import * as selectors from './onboarding/selectors'; + +export const addDebugTools = ( context ) => { + if ( ! context || ! context?.debug ) { + return; + } + + context.dumpStore = async () => { + /* eslint-disable no-console */ + if ( ! console?.groupCollapsed ) { + console.error( 'console.groupCollapsed is not supported.' ); + return; + } + + [ OnboardingStoreName ].forEach( ( storeName ) => { + const storeSelector = `wp.data.select('${ storeName }')`; + console.group( `[STORE] ${ storeSelector }` ); + + const dumpStore = async ( selector ) => { + const contents = await wp.data + .resolveSelect( storeName ) + [ selector ](); + + console.groupCollapsed( `[SELECTOR] .${ selector }()` ); + console.table( contents ); + console.groupEnd(); + }; + + Object.keys( selectors ).forEach( async ( selector ) => { + await dumpStore( selector ); + } ); + + console.groupEnd(); + } ); + /* eslint-enable no-console */ + }; + + context.resetStore = () => { + const onboarding = wp.data.dispatch( OnboardingStoreName ); + onboarding.reset(); + onboarding.persist(); + }; + + context.startOnboarding = () => { + const onboarding = wp.data.dispatch( OnboardingStoreName ); + onboarding.setCompleted( false ); + onboarding.setStep( 0 ); + onboarding.persist(); + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 1c95c4261..0946af2a0 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -1,7 +1,10 @@ import { STORE_NAME } from './constants'; import { initStore } from './store'; +import { addDebugTools } from './debug'; initStore(); export const WC_PAYPAL_STORE_NAME = STORE_NAME; export * from './onboarding/hooks'; + +addDebugTools( window.ppcpSettings ); diff --git a/modules/ppcp-settings/resources/js/data/store.js b/modules/ppcp-settings/resources/js/data/store.js index 01b713cc5..f7aea4d85 100644 --- a/modules/ppcp-settings/resources/js/data/store.js +++ b/modules/ppcp-settings/resources/js/data/store.js @@ -30,54 +30,4 @@ export const initStore = () => { } ); register( store ); - - addDebugTools(); -}; - -const addDebugTools = () => { - const context = window.ppcpSettings; - - // Provide a debug tool to inspect the Redux store via the JS console. - if ( ! context?.debug ) { - return; - } - - const getSelectors = () => wp.data.select( STORE_NAME ); - const getActions = () => wp.data.dispatch( STORE_NAME ); - - context.dumpStore = () => { - /* eslint-disable no-console */ - if ( ! console?.groupCollapsed ) { - console.error( 'console.groupCollapsed is not supported.' ); - return; - } - - const storeSelector = `wp.data.select('${ STORE_NAME }')`; - console.group( `[STORE] ${ storeSelector }` ); - - const select = getSelectors(); - Object.keys( selectors ).forEach( ( selector ) => { - console.groupCollapsed( `[SELECTOR] .${ selector }()` ); - console.table( select[ selector ]() ); - console.groupEnd(); - } ); - - console.groupEnd(); - /* eslint-enable no-console */ - }; - - context.resetStore = () => { - const dispatch = getActions(); - - dispatch.resetOnboarding(); - dispatch.persist(); - }; - - context.startOnboarding = () => { - const dispatch = getActions(); - - dispatch.setCompleted( false ); - dispatch.setOnboardingStep( 0 ); - dispatch.persist(); - }; }; From 79174459d8f86353b328772976b6121345ed05c9 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:45:08 +0100 Subject: [PATCH 24/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Create=20isolated=20?= =?UTF-8?q?onboarding=20Redux=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/constants.js | 2 -- .../ppcp-settings/resources/js/data/index.js | 9 +++-- .../resources/js/data/onboarding/constants.js | 5 ++- .../resources/js/data/onboarding/index.js | 22 +++++++++++-- .../ppcp-settings/resources/js/data/store.js | 33 ------------------- 5 files changed, 26 insertions(+), 45 deletions(-) delete mode 100644 modules/ppcp-settings/resources/js/data/store.js diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js index e6f8f9de5..65aa5dac4 100644 --- a/modules/ppcp-settings/resources/js/data/constants.js +++ b/modules/ppcp-settings/resources/js/data/constants.js @@ -1,6 +1,4 @@ export const NAMESPACE = '/wc/v3/wc_paypal'; -export const STORE_NAME = 'wc/paypal'; - export const BUSINESS_TYPES = { CASUAL_SELLER: 'casual_seller', BUSINESS: 'business', diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 0946af2a0..79a0702a0 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -1,10 +1,9 @@ -import { STORE_NAME } from './constants'; -import { initStore } from './store'; +import * as Onboarding from './onboarding'; import { addDebugTools } from './debug'; -initStore(); +Onboarding.initStore(); -export const WC_PAYPAL_STORE_NAME = STORE_NAME; -export * from './onboarding/hooks'; +export const OnboardingHooks = Onboarding.hooks; +export const OnboardingStoreName = Onboarding.STORE_NAME; addDebugTools( window.ppcpSettings ); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js index 2be3f6717..2c6b33cba 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -1,12 +1,11 @@ /** - * Name of the module-store in the main Redux store. - * Helps to isolate data. + * Name of the Redux store module. * * Used by: Reducer, Selector, Index * * @type {string} */ -export const STORE_KEY = 'onboarding'; +export const STORE_NAME = 'wc/paypal/onboarding'; /** * REST path to hydrate data of this module by loading data from the WP DB.. diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js index a8c685d37..0518db626 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/index.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js @@ -1,8 +1,26 @@ -import { STORE_KEY } from './constants'; +import { createReduxStore, register } from '@wordpress/data'; +import { controls as wpControls } from '@wordpress/data-controls'; + +import { STORE_NAME } from './constants'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; +import * as hooks from './hooks'; import { controls } from './controls'; -export { reducer, selectors, actions, resolvers, controls, STORE_KEY }; +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks }; + +export { STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/store.js b/modules/ppcp-settings/resources/js/data/store.js deleted file mode 100644 index f7aea4d85..000000000 --- a/modules/ppcp-settings/resources/js/data/store.js +++ /dev/null @@ -1,33 +0,0 @@ -import { createReduxStore, register, combineReducers } from '@wordpress/data'; -import { controls } from '@wordpress/data-controls'; -import { STORE_NAME } from './constants'; - -// Import Redux modules. -import * as Onboarding from './onboarding'; - -const actions = {}; -const selectors = {}; -const resolvers = {}; - -[ Onboarding ].forEach( ( item ) => { - Object.assign( actions, { ...( item.actions || {} ) } ); - Object.assign( selectors, { ...( item.selectors || {} ) } ); - Object.assign( resolvers, { ...( item.resolvers || {} ) } ); - Object.assign( controls, { ...( item.controls || {} ) } ); -} ); - -const reducer = combineReducers( { - [ Onboarding.STORE_KEY ]: Onboarding.reducer, -} ); - -export const initStore = () => { - const store = createReduxStore( STORE_NAME, { - reducer, - controls, - actions, - selectors, - resolvers, - } ); - - register( store ); -}; From 90c6cd1e7ddb947c12bc4fb33caf7d5b4139865b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:48:50 +0100 Subject: [PATCH 25/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20resolver?= =?UTF-8?q?=20and=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/constants.js | 1 - .../resources/js/data/onboarding/constants.js | 6 +-- .../resources/js/data/onboarding/controls.js | 47 +++++++------------ .../resources/js/data/onboarding/index.js | 2 +- .../resources/js/data/onboarding/resolvers.js | 43 +++++++++-------- 5 files changed, 42 insertions(+), 57 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js index 65aa5dac4..5654ad476 100644 --- a/modules/ppcp-settings/resources/js/data/constants.js +++ b/modules/ppcp-settings/resources/js/data/constants.js @@ -1,4 +1,3 @@ -export const NAMESPACE = '/wc/v3/wc_paypal'; export const BUSINESS_TYPES = { CASUAL_SELLER: 'casual_seller', BUSINESS: 'business', diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js index 2c6b33cba..db9efebc6 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/constants.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -15,7 +15,7 @@ export const STORE_NAME = 'wc/paypal/onboarding'; * * @type {string} */ -export const REST_HYDRATE_PATH = 'onboarding'; +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/onboarding'; /** * REST path to persist data of this module to the WP DB. @@ -25,7 +25,7 @@ export const REST_HYDRATE_PATH = 'onboarding'; * * @type {string} */ -export const REST_PERSIST_PATH = 'onboarding'; +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/onboarding'; /** * REST path to perform the manual connection check, using client ID and secret, @@ -35,4 +35,4 @@ export const REST_PERSIST_PATH = 'onboarding'; * * @type {string} */ -export const REST_MANUAL_CONNECTION_PATH = 'connect_manual'; +export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/controls.js b/modules/ppcp-settings/resources/js/data/onboarding/controls.js index 729df17c6..a8665b0b7 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/controls.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/controls.js @@ -1,57 +1,46 @@ /** * Controls: Implement side effects, typically asynchronous operations. * - * Controls use ACTION_TYPES keys as identifiers to ensure uniqueness. + * Controls use ACTION_TYPES keys as identifiers. * They are triggered by corresponding actions and handle external interactions. * * @file */ import { select } from '@wordpress/data'; -import { apiFetch } from '@wordpress/api-fetch'; +import apiFetch from '@wordpress/api-fetch'; -import { NAMESPACE, STORE_NAME } from '../constants'; -import { REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH } from './constants'; +import { + STORE_NAME, + REST_PERSIST_PATH, + REST_MANUAL_CONNECTION_PATH, +} from './constants'; import ACTION_TYPES from './action-types'; -import { setIsSaving, setManualConnectionIsBusy } from './actions'; export const controls = { - [ ACTION_TYPES.DO_PERSIST_DATA ]: async ( { dispatch } ) => { - let error = null; - + async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { + console.log( 'Do PERSIST: ', data ); try { - const path = `${ NAMESPACE }/${ REST_PERSIST_PATH }`; - const data = select( STORE_NAME ).onboardingPersistentData(); - - dispatch( setIsSaving( true ) ); - await apiFetch( { - path, - method: 'post', + path: REST_PERSIST_PATH, + method: 'POST', data, } ); } catch ( e ) { - error = e; console.error( 'Error saving progress.', e ); - } finally { - dispatch( setIsSaving( false ) ); } - - return error === null; }, - [ ACTION_TYPES.DO_MANUAL_CONNECTION ]: async ( { dispatch } ) => { + async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( { + clientId, + clientSecret, + useSandbox, + } ) { let result = null; try { - const path = `${ NAMESPACE }/${ REST_MANUAL_CONNECTION_PATH }`; - const { clientId, clientSecret, useSandbox } = - select( STORE_NAME ).onboardingPersistentData(); - - dispatch( setManualConnectionIsBusy( true ) ); - result = await apiFetch( { - path, + path: REST_MANUAL_CONNECTION_PATH, method: 'POST', data: { clientId, @@ -64,8 +53,6 @@ export const controls = { success: false, error: e, }; - } finally { - dispatch( setManualConnectionIsBusy( false ) ); } return result; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js index 0518db626..60f81da60 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/index.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js @@ -5,8 +5,8 @@ import { STORE_NAME } from './constants'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; -import * as resolvers from './resolvers'; import * as hooks from './hooks'; +import { resolvers } from './resolvers'; import { controls } from './controls'; export const initStore = () => { diff --git a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js index 7d79e35e6..bf7828dd3 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js @@ -2,7 +2,7 @@ * Resolvers: Handle asynchronous data fetching for the store. * * These functions update store state with data from external sources. - * Each resolver corresponds to a specific selector but must have a unique name. + * Each resolver corresponds to a specific selector (selector with same name must exist). * Resolvers are called automatically when selectors request unavailable data. * * @file @@ -12,26 +12,25 @@ import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; -import { NAMESPACE } from '../constants'; -import { REST_HYDRATE_PATH } from './constants'; -import { setIsReady, hydrateOnboardingDetails } from './actions'; +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; -/** - * Retrieve settings from the site's REST API. - */ -export function* onboardingPersistentData() { - const path = `${ NAMESPACE }/${ REST_HYDRATE_PATH }`; +export const resolvers = { + /** + * Retrieve settings from the site's REST API. + */ + *persistentData() { + try { + const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); - try { - const result = yield apiFetch( { path } ); - yield hydrateOnboardingDetails( result ); - yield setIsReady( true ); - } catch ( e ) { - yield dispatch( 'core/notices' ).createErrorNotice( - __( - 'Error retrieving onboarding details.', - 'woocommerce-paypal-payments' - ) - ); - } -} + yield dispatch( STORE_NAME ).hydrate( result ); + yield dispatch( STORE_NAME ).setIsReady( true ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + __( + 'Error retrieving onboarding details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; From a7b854abb550058504a3b02d776276c9402fe960 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:53:21 +0100 Subject: [PATCH 26/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix=20new=20implemen?= =?UTF-8?q?tation=20of=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/actions.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index ac194252d..425ef3278 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -186,10 +186,12 @@ export const setProducts = ( products ) => { * * @return {Action} The action. */ -export const persist = () => { - return { - type: ACTION_TYPES.DO_PERSIST_DATA, - }; +export const persist = function* () { + const data = yield select( STORE_NAME ).persistentData(); + + yield setIsSaving( true ); + yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; + yield setIsSaving( false ); }; /** @@ -197,8 +199,19 @@ export const persist = () => { * * @return {Action} The action. */ -export const connectViaIdAndSecret = () => { - return { +export const connectViaIdAndSecret = function* () { + const { clientId, clientSecret, useSandbox } = + yield select( STORE_NAME ).persistentData(); + + yield setManualConnectionIsBusy( true ); + + const result = yield { type: ACTION_TYPES.DO_MANUAL_CONNECTION, + clientId, + clientSecret, + useSandbox, }; + yield setManualConnectionIsBusy( false ); + + return result; }; From 4d84bdbd43519895b2a4a135dde47ab62c547941 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:53:46 +0100 Subject: [PATCH 27/66] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bugs=20in=20reducer?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/onboarding/reducer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 0f253fc02..6264c7e83 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -45,17 +45,17 @@ const [ setTransient, setPersistent ] = createSetters( ); const onboardingReducer = createReducer( defaultTransient, defaultPersistent, { - [ ACTION_TYPES.SET_TRANSIENT ]: ( state, { payload } ) => + [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => setTransient( state, payload ), - [ ACTION_TYPES.SET_PERSISTENT ]: ( state, { payload } ) => + [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) => setPersistent( state, payload ), [ ACTION_TYPES.RESET ]: ( state ) => setPersistent( state, defaultPersistent ), - [ ACTION_TYPES.HYDRATE ]: ( state, { payload } ) => { - const newState = setPersistent( payload ); + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => { + const newState = setPersistent( state, payload.data ); // Flags are not updated by `setPersistent()`. if ( payload.flags ) { From 329135d1dc799ad24625d7f6dd3249d002988036 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:54:11 +0100 Subject: [PATCH 28/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20selectors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/selectors.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index 89c62a360..d4d57ef4d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -3,32 +3,23 @@ * * These functions provide a consistent interface for accessing store data. * They allow components to retrieve data without knowing the store structure. - * Exported functions must have unique names across all store modules. * * @file */ -import { STORE_KEY } from './constants'; - const EMPTY_OBJ = Object.freeze( {} ); -const getState = ( state ) => { - if ( ! state ) { - return EMPTY_OBJ; - } +const getState = ( state ) => state || EMPTY_OBJ; - return state[ STORE_KEY ] || EMPTY_OBJ; -}; - -export const onboardingPersistentData = ( state ) => { +export const persistentData = ( state ) => { return getState( state ).data || EMPTY_OBJ; }; -export const onboardingTransientData = ( state ) => { +export const transientData = ( state ) => { const { data, flags, ...transientState } = getState( state ); return transientState || EMPTY_OBJ; }; -export const onboardingFlags = ( state ) => { +export const flags = ( state ) => { return getState( state ).flags || EMPTY_OBJ; }; From 1c27e9092356c489803af952542b884ec6f2c3aa Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:54:29 +0100 Subject: [PATCH 29/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/actions.js | 128 +++++++----------- 1 file changed, 52 insertions(+), 76 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 425ef3278..76a54bbcb 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -2,13 +2,15 @@ * Action Creators: Define functions to create action objects. * * These functions update state or trigger side effects (e.g., async operations). - * Exported functions must have unique names across all store modules. * Actions are categorized as Transient, Persistent, or Side effect. * * @file */ +import { select } from '@wordpress/data'; + import ACTION_TYPES from './action-types'; +import { STORE_NAME } from './constants'; /** * @typedef {Object} Action An action object that is handled by a reducer or control. @@ -21,9 +23,7 @@ import ACTION_TYPES from './action-types'; * * @return {Action} The action. */ -export const resetOnboarding = () => { - return { type: ACTION_TYPES.RESET }; -}; +export const reset = () => ( { type: ACTION_TYPES.RESET } ); /** * Transient. Marks the onboarding details as "ready", i.e., fully initialized. @@ -31,12 +31,10 @@ export const resetOnboarding = () => { * @param {boolean} isReady * @return {Action} The action. */ -export const setIsReady = ( isReady ) => { - return { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, - }; -}; +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); /** * Transient. Changes the "saving" flag. @@ -44,12 +42,10 @@ export const setIsReady = ( isReady ) => { * @param {boolean} isSaving * @return {Action} The action. */ -export const setIsSaving = ( isSaving ) => { - return { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isSaving }, - }; -}; +export const setIsSaving = ( isSaving ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isSaving }, +} ); /** * Transient. Changes the "manual connection is busy" flag. @@ -57,12 +53,10 @@ export const setIsSaving = ( isSaving ) => { * @param {boolean} isBusy * @return {Action} The action. */ -export const setManualConnectionIsBusy = ( isBusy ) => { - return { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isBusy }, - }; -}; +export const setManualConnectionIsBusy = ( isBusy ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isBusy }, +} ); /** * Persistent. Set the full onboarding details, usually during app initialization. @@ -70,12 +64,10 @@ export const setManualConnectionIsBusy = ( isBusy ) => { * @param {{data: {}, flags?: {}}} payload * @return {Action} The action. */ -export const hydrateOnboardingDetails = ( payload ) => { - return { - type: ACTION_TYPES.HYDRATE, - payload, - }; -}; +export const hydrate = ( payload ) => ( { + type: ACTION_TYPES.HYDRATE, + payload, +} ); /** * Persistent.Set the "onboarding completed" flag which shows or hides the wizard. @@ -83,12 +75,10 @@ export const hydrateOnboardingDetails = ( payload ) => { * @param {boolean} completed * @return {Action} The action. */ -export const setCompleted = ( completed ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { completed }, - }; -}; +export const setCompleted = ( completed ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { completed }, +} ); /** * Persistent. Sets the onboarding wizard to a new step. @@ -96,12 +86,10 @@ export const setCompleted = ( completed ) => { * @param {number} step * @return {Action} The action. */ -export const setOnboardingStep = ( step ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { step }, - }; -}; +export const setStep = ( step ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { step }, +} ); /** * Persistent. Sets the sandbox mode on or off. @@ -109,12 +97,10 @@ export const setOnboardingStep = ( step ) => { * @param {boolean} useSandbox * @return {Action} The action. */ -export const setSandboxMode = ( useSandbox ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { useSandbox }, - }; -}; +export const setSandboxMode = ( useSandbox ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { useSandbox }, +} ); /** * Persistent. Toggles the "Manual Connection" mode on or off. @@ -122,12 +108,10 @@ export const setSandboxMode = ( useSandbox ) => { * @param {boolean} useManualConnection * @return {Action} The action. */ -export const setManualConnectionMode = ( useManualConnection ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { useManualConnection }, - }; -}; +export const setManualConnectionMode = ( useManualConnection ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { useManualConnection }, +} ); /** * Persistent. Changes the "client ID" value. @@ -135,12 +119,10 @@ export const setManualConnectionMode = ( useManualConnection ) => { * @param {string} clientId * @return {Action} The action. */ -export const setClientId = ( clientId ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { clientId }, - }; -}; +export const setClientId = ( clientId ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { clientId }, +} ); /** * Persistent. Changes the "client secret" value. @@ -148,12 +130,10 @@ export const setClientId = ( clientId ) => { * @param {string} clientSecret * @return {Action} The action. */ -export const setClientSecret = ( clientSecret ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { clientSecret }, - }; -}; +export const setClientSecret = ( clientSecret ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { clientSecret }, +} ); /** * Persistent. Sets the "isCasualSeller" value. @@ -161,12 +141,10 @@ export const setClientSecret = ( clientSecret ) => { * @param {boolean} isCasualSeller * @return {Action} The action. */ -export const setIsCasualSeller = ( isCasualSeller ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { isCasualSeller }, - }; -}; +export const setIsCasualSeller = ( isCasualSeller ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { isCasualSeller }, +} ); /** * Persistent. Sets the "products" array. @@ -174,12 +152,10 @@ export const setIsCasualSeller = ( isCasualSeller ) => { * @param {string[]} products * @return {Action} The action. */ -export const setProducts = ( products ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { products }, - }; -}; +export const setProducts = ( products ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { products }, +} ); /** * Side effect. Triggers the persistence of onboarding data to the server. From 4bcdb445d4f4c1d0fe7ee3fa8ddd69791943c87f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:54:53 +0100 Subject: [PATCH 30/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adjust=20hooks=20to?= =?UTF-8?q?=20new=20code,=20rename=20a=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/hooks.js | 96 +++++++------------ .../resources/js/data/onboarding/reducer.js | 3 +- 2 files changed, 36 insertions(+), 63 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index 9e5f1d179..547a1eaa1 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -3,19 +3,31 @@ * * These encapsulate store interactions, offering a consistent interface. * Hooks simplify data access and manipulation for components. - * Exported hooks must have unique names across all store modules. * * @file */ import { useSelect, useDispatch } from '@wordpress/data'; -import { PRODUCT_TYPES, STORE_NAME } from '../constants'; +import { PRODUCT_TYPES } from '../constants'; +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); const useOnboardingDetails = () => { const { persist, - setOnboardingStep, + setStep, setCompleted, setSandboxMode, setManualConnectionMode, @@ -25,61 +37,23 @@ const useOnboardingDetails = () => { setProducts, } = useDispatch( STORE_NAME ); - const transientData = ( select ) => - select( STORE_NAME ).onboardingTransientData(); - const persistentData = ( select ) => - select( STORE_NAME ).onboardingPersistentData(); - // Read-only flags. - const flags = useSelect( ( select ) => { - return select( STORE_NAME ).onboardingFlags(); - } ); + const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); // Transient accessors. - const isSaving = useSelect( ( select ) => { - return transientData( select ).isSaving; - }, [] ); - - const isReady = useSelect( ( select ) => { - return transientData( select ).isReady; - } ); - - const isManualConnectionBusy = useSelect( ( select ) => { - return transientData( select ).isManualConnectionBusy; - }, [] ); + const isSaving = useTransient( 'isSaving' ); + const isReady = useTransient( 'isReady' ); + const isBusy = useTransient( 'isBusy' ); // Persistent accessors. - const step = useSelect( ( select ) => { - return persistentData( select ).step || 0; - } ); - - const completed = useSelect( ( select ) => { - return persistentData( select ).completed; - } ); - - const clientId = useSelect( ( select ) => { - return persistentData( select ).clientId; - }, [] ); - - const clientSecret = useSelect( ( select ) => { - return persistentData( select ).clientSecret; - }, [] ); - - const isSandboxMode = useSelect( ( select ) => { - return persistentData( select ).useSandbox; - }, [] ); - - const isManualConnectionMode = useSelect( ( select ) => { - return persistentData( select ).useManualConnection; - }, [] ); - - const isCasualSeller = useSelect( ( select ) => { - return persistentData( select ).isCasualSeller; - }, [] ); - - const products = useSelect( ( select ) => { - return persistentData( select ).products || []; - }, [] ); + const step = usePersistent( 'step' ); + const completed = usePersistent( 'completed' ); + const clientId = usePersistent( 'clientId' ); + const clientSecret = usePersistent( 'clientSecret' ); + const isSandboxMode = usePersistent( 'useSandbox' ); + const isManualConnectionMode = usePersistent( 'useManualConnection' ); + const isCasualSeller = usePersistent( 'isCasualSeller' ); + const products = usePersistent( 'products' ); const toggleProduct = ( list ) => { const validProducts = list.filter( ( item ) => @@ -96,9 +70,9 @@ const useOnboardingDetails = () => { return { isSaving, isReady, - isManualConnectionBusy, + isBusy, step, - setStep: ( value ) => setDetailAndPersist( setOnboardingStep, value ), + setStep: ( value ) => setDetailAndPersist( setStep, value ), completed, setCompleted: ( state ) => setDetailAndPersist( setCompleted, state ), isSandboxMode, @@ -121,10 +95,10 @@ const useOnboardingDetails = () => { }; }; -export const useOnboardingStepWelcome = () => { +export const useConnection = () => { const { isSaving, - isManualConnectionBusy, + isBusy, isSandboxMode, setSandboxMode, isManualConnectionMode, @@ -137,7 +111,7 @@ export const useOnboardingStepWelcome = () => { return { isSaving, - isManualConnectionBusy, + isBusy, isSandboxMode, setSandboxMode, isManualConnectionMode, @@ -149,19 +123,19 @@ export const useOnboardingStepWelcome = () => { }; }; -export const useOnboardingStepBusiness = () => { +export const useBusiness = () => { const { isCasualSeller, setIsCasualSeller } = useOnboardingDetails(); return { isCasualSeller, setIsCasualSeller }; }; -export const useOnboardingStepProducts = () => { +export const useProducts = () => { const { products, toggleProduct } = useOnboardingDetails(); return { products, toggleProduct }; }; -export const useOnboardingStep = () => { +export const useSteps = () => { const { isReady, step, setStep, completed, setCompleted, flags } = useOnboardingDetails(); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 6264c7e83..0ca7dc646 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -2,7 +2,6 @@ * Reducer: Defines store structure and state updates for this module. * * Manages both transient (temporary) and persistent (saved) state. - * Each module uses isolated memory objects to prevent conflicts. * The initial state must define all properties, as dynamic additions are not supported. * * @file @@ -16,7 +15,7 @@ import ACTION_TYPES from './action-types'; const defaultTransient = { isReady: false, isSaving: false, - isManualConnectionBusy: false, + isBusy: false, // Read only values, provided by the server. flags: { From 6a91d1ee9927f486280dd0367a4e963aed24922e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:55:07 +0100 Subject: [PATCH 31/66] =?UTF-8?q?=F0=9F=92=A1=20Update=20a=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/onboarding/action-types.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index 7a38dbb1b..f5d9944d3 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -1,9 +1,6 @@ /** * Action Types: Define unique identifiers for actions across all store modules. * - * Keys are module-internal and can have any value. - * Values must be unique across all store modules to avoid collisions. - * * @file */ From d933d8e6f4e6d111db13694f85b4767dc9f3ed15 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:55:31 +0100 Subject: [PATCH 32/66] =?UTF-8?q?=E2=9C=A8=20Export=20global=20constants?= =?UTF-8?q?=20in=20main=20data-module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 79a0702a0..9782613d4 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -6,4 +6,6 @@ Onboarding.initStore(); export const OnboardingHooks = Onboarding.hooks; export const OnboardingStoreName = Onboarding.STORE_NAME; +export * from './constants'; + addDebugTools( window.ppcpSettings ); From a5508835d7aba5aafe3a590a521c14a8e9cb589a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:55:56 +0100 Subject: [PATCH 33/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Implement=20new=20st?= =?UTF-8?q?ore=20logic=20in=20=E2=80=9CAdvaced=20Options=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 5b394e0c7..224e0b0d3 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,11 +7,11 @@ import { store as noticesStore } from '@wordpress/notices'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; -import { useManualConnect, useOnboardingStepWelcome } from '../../../../data'; +import { OnboardingHooks } from '../../../../data'; const AdvancedOptionsForm = ( { setCompleted } ) => { const { - isManualConnectionBusy, + isBusy, isSandboxMode, setSandboxMode, isManualConnectionMode, @@ -20,11 +20,11 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { setClientId, clientSecret, setClientSecret, - } = useOnboardingStepWelcome(); + } = OnboardingHooks.useConnection(); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); - const { connectManual } = useManualConnect(); + const { connectManual } = OnboardingHooks.useManualConnect(); const refClientId = useRef( null ); const refClientSecret = useRef( null ); @@ -125,14 +125,14 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { Date: Wed, 20 Nov 2024 16:56:44 +0100 Subject: [PATCH 34/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Implement=20new=20st?= =?UTF-8?q?ore=20logic=20in=20on=20boarding=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/Screens/Onboarding/Onboarding.js | 4 ++-- .../js/Components/Screens/Onboarding/StepBusiness.js | 5 ++--- .../js/Components/Screens/Onboarding/StepProducts.js | 5 ++--- .../resources/js/Components/Screens/Settings.js | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index a91eabb48..5d053a793 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -1,5 +1,5 @@ import Container from '../../ReusableComponents/Container'; -import { useOnboardingStep } from '../../../data'; +import { OnboardingHooks } from '../../../data'; import { getSteps } from './availableSteps'; import Navigation from '../../ReusableComponents/Navigation'; @@ -15,7 +15,7 @@ const getCurrentStep = ( requestedStep, steps ) => { }; const Onboarding = () => { - const { step, setStep, setCompleted, flags } = useOnboardingStep(); + const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps(); const steps = getSteps( flags ); const CurrentStepComponent = getCurrentStep( step, steps ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js index 8bbc3c199..3a6632c74 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js @@ -3,8 +3,7 @@ import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBox from '../../ReusableComponents/SelectBox'; import { __ } from '@wordpress/i18n'; import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; -import { useOnboardingStepBusiness } from '../../../data'; -import { BUSINESS_TYPES } from '../../../data/constants'; +import { OnboardingHooks, BUSINESS_TYPES } from '../../../data'; const BUSINESS_RADIO_GROUP_NAME = 'business'; @@ -14,7 +13,7 @@ const StepBusiness = ( { stepperOrder, setCompleted, } ) => { - const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); + const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness(); const handleSellerTypeChange = ( value ) => { setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index 44c56d509..f44cc89c1 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -2,8 +2,7 @@ import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import { __ } from '@wordpress/i18n'; import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; -import { useOnboardingStepProducts } from '../../../data'; -import { PRODUCT_TYPES } from '../../../data/constants'; +import { OnboardingHooks, PRODUCT_TYPES } from '../../../data'; const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; @@ -13,7 +12,7 @@ const StepProducts = ( { stepperOrder, setCompleted, } ) => { - const { products, toggleProduct } = useOnboardingStepProducts(); + const { products, toggleProduct } = OnboardingHooks.useProducts(); return (
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index c74805499..abe4490b2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -1,10 +1,10 @@ import TabNavigation from '../ReusableComponents/TabNavigation'; import { getSettingsTabs } from './tabs'; -import { useOnboardingStep } from '../../data'; +import { OnboardingHooks } from '../../data'; import Onboarding from './Onboarding/Onboarding'; const Settings = () => { - const onboardingProgress = useOnboardingStep(); + const onboardingProgress = OnboardingHooks.useSteps(); if ( ! onboardingProgress.isReady ) { // TODO: Use better loading state indicator. From c96957152b7fe0512cd9228ce0feef41c48f04c2 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 16:59:26 +0100 Subject: [PATCH 35/66] =?UTF-8?q?=F0=9F=9A=9A=20Move=20navigation=20compon?= =?UTF-8?q?ent=20to=20onboarding=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components}/Navigation.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/ppcp-settings/resources/js/Components/{ReusableComponents => Screens/Onboarding/Components}/Navigation.js (100%) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js similarity index 100% rename from modules/ppcp-settings/resources/js/Components/ReusableComponents/Navigation.js rename to modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js From 40a6514af707af1d322d9895617c2191a4df6bba Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 17:00:00 +0100 Subject: [PATCH 36/66] =?UTF-8?q?=F0=9F=8E=A8=20Apply=20eslint=20styling?= =?UTF-8?q?=20to=20Navigation=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Onboarding/Components/Navigation.js | 184 ++++++++++-------- 1 file changed, 107 insertions(+), 77 deletions(-) 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 032d9c5c5..18b13e976 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,16 +1,14 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import {useOnboardingStepBusiness, useOnboardingStepProducts} from "../../data"; -import data from "../../utils/data"; +import { + useOnboardingStepBusiness, + useOnboardingStepProducts, +} from '../../data'; +import data from '../../utils/data'; -const Navigation = ( { - setStep, - setCompleted, - currentStep, - stepperOrder -} ) => { - const isLastStep = () => currentStep + 1 === stepperOrder.length; - const isFistStep = () => currentStep === 0; +const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { + const isLastStep = () => currentStep + 1 === stepperOrder.length; + const isFistStep = () => currentStep === 0; const navigateBy = ( stepDirection ) => { let newStep = currentStep + stepDirection; @@ -26,77 +24,109 @@ const Navigation = ( { } }; - const { products, toggleProduct } = useOnboardingStepProducts(); - const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); + const { products, toggleProduct } = useOnboardingStepProducts(); + const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); - let navigationTitle = ''; - let disabled = false; + let navigationTitle = ''; + let disabled = false; - switch ( currentStep ) { - case 1: - navigationTitle = __( 'Set up store type', 'woocommerce-paypal-payments' ); - disabled = isCasualSeller === null - break; - case 2: - navigationTitle = __( 'Select product types', 'woocommerce-paypal-payments' ); - disabled = products.length < 1 - break; - case 3: - navigationTitle = __( 'Choose checkout options', 'woocommerce-paypal-payments' ); - case 4: - navigationTitle = __( 'Connect your PayPal account', 'woocommerce-paypal-payments' ); - break; - default: - navigationTitle = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); - } + switch ( currentStep ) { + case 1: + navigationTitle = __( + 'Set up store type', + 'woocommerce-paypal-payments' + ); + disabled = isCasualSeller === null; + break; + case 2: + navigationTitle = __( + 'Select product types', + 'woocommerce-paypal-payments' + ); + disabled = products.length < 1; + break; + case 3: + navigationTitle = __( + 'Choose checkout options', + 'woocommerce-paypal-payments' + ); + case 4: + navigationTitle = __( + 'Connect your PayPal account', + 'woocommerce-paypal-payments' + ); + break; + default: + navigationTitle = __( + 'PayPal Payments', + 'woocommerce-paypal-payments' + ); + } return ( -
-
-
- {data().getImage('icon-arrow-left.svg')} - {!isFistStep() ? ( - - ) : ( - - {navigationTitle} - - )} -
- {!isFistStep() && ( -
- - { __( 'Save and exit', 'woocommerce-paypal-payments' ) } - - {!isLastStep() && ( - - )} -
- )} -
-
-
- ); +
+
+
+ { data().getImage( 'icon-arrow-left.svg' ) } + { ! isFistStep() ? ( + + ) : ( + + { navigationTitle } + + ) } +
+ { ! isFistStep() && ( +
+ + { __( + 'Save and exit', + 'woocommerce-paypal-payments' + ) } + + { ! isLastStep() && ( + + ) } +
+ ) } +
+
+
+ ); }; export default Navigation; From df0837b89492aefe893a54d713a7a5e1f404319f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 17:01:34 +0100 Subject: [PATCH 37/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Implement=20new=20st?= =?UTF-8?q?ore=20logic=20in=20Navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/Onboarding/Components/Navigation.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 18b13e976..7a4c8b15f 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,10 +1,8 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { - useOnboardingStepBusiness, - useOnboardingStepProducts, -} from '../../data'; -import data from '../../utils/data'; + +import { OnboardingHooks } from '../../../../data'; +import data from '../../../../utils/data'; const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { const isLastStep = () => currentStep + 1 === stepperOrder.length; @@ -24,8 +22,8 @@ const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { } }; - const { products, toggleProduct } = useOnboardingStepProducts(); - const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); + const { products, toggleProduct } = OnboardingHooks.useProducts(); + const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness(); let navigationTitle = ''; let disabled = false; From 80bbf512207cb6890e5381a7134eb884268663bd Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 17:20:39 +0100 Subject: [PATCH 38/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20outdated=20?= =?UTF-8?q?action=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/onboarding/actions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 76a54bbcb..bd3705fae 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -53,7 +53,7 @@ export const setIsSaving = ( isSaving ) => ( { * @param {boolean} isBusy * @return {Action} The action. */ -export const setManualConnectionIsBusy = ( isBusy ) => ( { +export const setIsBusy = ( isBusy ) => ( { type: ACTION_TYPES.SET_TRANSIENT, payload: { isBusy }, } ); @@ -179,7 +179,7 @@ export const connectViaIdAndSecret = function* () { const { clientId, clientSecret, useSandbox } = yield select( STORE_NAME ).persistentData(); - yield setManualConnectionIsBusy( true ); + yield setIsBusy( true ); const result = yield { type: ACTION_TYPES.DO_MANUAL_CONNECTION, @@ -187,7 +187,7 @@ export const connectViaIdAndSecret = function* () { clientSecret, useSandbox, }; - yield setManualConnectionIsBusy( false ); + yield setIsBusy( false ); return result; }; From 8f12e978f37c547324fe67189120fec317b673c6 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 17:20:48 +0100 Subject: [PATCH 39/66] =?UTF-8?q?=F0=9F=94=A5=20Clean=20up=20unused=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/onboarding/controls.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/onboarding/controls.js b/modules/ppcp-settings/resources/js/data/onboarding/controls.js index a8665b0b7..94d108e16 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/controls.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/controls.js @@ -7,19 +7,13 @@ * @file */ -import { select } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; -import { - STORE_NAME, - REST_PERSIST_PATH, - REST_MANUAL_CONNECTION_PATH, -} from './constants'; +import { REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH } from './constants'; import ACTION_TYPES from './action-types'; export const controls = { async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { - console.log( 'Do PERSIST: ', data ); try { await apiFetch( { path: REST_PERSIST_PATH, From 7ae4184d302ab76a840a9f53f03f8a66aaf1f121 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 17:21:09 +0100 Subject: [PATCH 40/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Apply=20new=20code?= =?UTF-8?q?=20style=20to=20=E2=80=9Ccommon=E2=80=9D=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/action-types.js | 4 +- .../resources/js/data/common/actions.js | 100 ++++++++++-------- .../resources/js/data/common/constants.js | 6 +- .../resources/js/data/common/controls.js | 19 ++-- .../resources/js/data/common/hooks.js | 17 ++- .../resources/js/data/common/index.js | 24 ++++- .../resources/js/data/common/reducer.js | 5 +- .../resources/js/data/common/resolvers.js | 43 ++++---- .../resources/js/data/common/selectors.js | 21 +--- 9 files changed, 130 insertions(+), 109 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 1645547d9..5d5968591 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -1,9 +1,6 @@ /** * Action Types: Define unique identifiers for actions across all store modules. * - * Keys are module-internal and can have any value. - * Values must be unique across all store modules to avoid collisions. - * * @file */ @@ -13,6 +10,7 @@ export default { // Persistent data. SET_PERSISTENT: 'COMMON:SET_PERSISTENT', + HYDRATE: 'COMMON:HYDRATE', // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 3992e0ff5..4586b8923 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -2,7 +2,6 @@ * Action Creators: Define functions to create action objects. * * These functions update state or trigger side effects (e.g., async operations). - * Exported functions must have unique names across all store modules. * Actions are categorized as Transient, Persistent, or Side effect. * * @file @@ -10,91 +9,98 @@ import ACTION_TYPES from './action-types'; +/** + * @typedef {Object} Action An action object that is handled by a reducer or control. + * @property {string} type - The action type. + * @property {Object?} payload - Optional payload for the action. + */ + /** * Transient. Marks the onboarding details as "ready", i.e., fully initialized. * * @param {boolean} isReady - * @return {{type: string, isReady: boolean}} The action. + * @return {Action} The action. */ -export const setIsReady = ( isReady ) => { - return { - type: ACTION_TYPES.SET_TRANSIENT, - isReady, - }; -}; +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); /** * Transient. Changes the "saving" flag. * * @param {boolean} isSaving - * @return {{type: string, isSaving: boolean}} The action. + * @return {Action} The action. */ -export const setIsSaving = ( isSaving ) => { - return { - type: ACTION_TYPES.SET_TRANSIENT, - isSaving, - }; -}; +export const setIsSaving = ( isSaving ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isSaving }, +} ); + +/** + * Transient. Changes the "manual connection is busy" flag. + * + * @param {boolean} isBusy + * @return {Action} The action. + */ +export const setIsBusy = ( isBusy ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isBusy }, +} ); /** * Persistent. Sets the sandbox mode on or off. * * @param {boolean} useSandbox - * @return {{type: string, useSandbox: boolean}} An action. + * @return {Action} The action. */ -export const setSandboxMode = ( useSandbox ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - useSandbox, - }; -}; +export const setSandboxMode = ( useSandbox ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { useSandbox }, +} ); /** * Persistent. Toggles the "Manual Connection" mode on or off. * * @param {boolean} useManualConnection - * @return {{type: string, useManualConnection: boolean}} An action. + * @return {Action} The action. */ -export const setManualConnectionMode = ( useManualConnection ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - useManualConnection, - }; -}; +export const setManualConnectionMode = ( useManualConnection ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { useManualConnection }, +} ); /** * Persistent. Changes the "client ID" value. * * @param {string} clientId - * @return {{type: string, clientId: string}} The action. + * @return {Action} The action. */ -export const setClientId = ( clientId ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - clientId, - }; -}; +export const setClientId = ( clientId ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { clientId }, +} ); /** * Persistent. Changes the "client secret" value. * * @param {string} clientSecret - * @return {{type: string, clientSecret: string}} The action. + * @return {Action} The action. */ -export const setClientSecret = ( clientSecret ) => { - return { - type: ACTION_TYPES.SET_PERSISTENT, - clientSecret, - }; -}; +export const setClientSecret = ( clientSecret ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { clientSecret }, +} ); /** * Side effect. Saves the persistent details to the WP database. * - * @return {{type: string}} The action. + * @return {Action} The action. */ -export function* commonPersist() { - return { +export const persist = function* () { + yield setIsBusy( true ); + yield { type: ACTION_TYPES.DO_PERSIST_DATA, }; -} + yield setIsBusy( false ); +}; diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index 365bfe305..881cac2f5 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -5,7 +5,7 @@ * * @type {string} */ -export const STORE_KEY = 'common'; +export const STORE_NAME = 'wc/paypal/common'; /** * REST path to hydrate data of this module by loading data from the WP DB.. @@ -14,7 +14,7 @@ export const STORE_KEY = 'common'; * * @type {string} */ -export const REST_HYDRATE_PATH = 'common'; +export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common'; /** * REST path to persist data of this module to the WP DB. @@ -23,4 +23,4 @@ export const REST_HYDRATE_PATH = 'common'; * * @type {string} */ -export const REST_PERSIST_PATH = 'common'; +export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 45b3e6ea5..e18bb48ec 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -1,32 +1,27 @@ /** * Controls: Implement side effects, typically asynchronous operations. * - * Controls use ACTION_TYPES keys as identifiers to ensure uniqueness. + * Controls use ACTION_TYPES keys as identifiers. * They are triggered by corresponding actions and handle external interactions. * * @file */ -import { select } from '@wordpress/data'; -import { apiFetch } from '@wordpress/api-fetch'; +import apiFetch from '@wordpress/api-fetch'; -import { NAMESPACE, STORE_NAME } from '../constants'; import { REST_PERSIST_PATH } from './constants'; import ACTION_TYPES from './action-types'; -export const controls = { - [ ACTION_TYPES.DO_PERSIST_DATA ]: async () => { - const path = `${ NAMESPACE }/${ REST_PERSIST_PATH }`; - const data = select( STORE_NAME ).getPersistentData(); +export const controls = { + async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) { try { return await apiFetch( { - path, - method: 'post', + path: REST_PERSIST_PATH, + method: 'POST', data, } ); } catch ( error ) { - console.error( 'Error saving progress.', error ); - throw error; + console.error( 'Error saving data.', error ); } }, }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index a201c6d37..907ee00f3 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -3,7 +3,22 @@ * * These encapsulate store interactions, offering a consistent interface. * Hooks simplify data access and manipulation for components. - * Exported hooks must have unique names across all store modules. * * @file */ + +import { useSelect } from '@wordpress/data'; + +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); diff --git a/modules/ppcp-settings/resources/js/data/common/index.js b/modules/ppcp-settings/resources/js/data/common/index.js index a8c685d37..60f81da60 100644 --- a/modules/ppcp-settings/resources/js/data/common/index.js +++ b/modules/ppcp-settings/resources/js/data/common/index.js @@ -1,8 +1,26 @@ -import { STORE_KEY } from './constants'; +import { createReduxStore, register } from '@wordpress/data'; +import { controls as wpControls } from '@wordpress/data-controls'; + +import { STORE_NAME } from './constants'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; -import * as resolvers from './resolvers'; +import * as hooks from './hooks'; +import { resolvers } from './resolvers'; import { controls } from './controls'; -export { reducer, selectors, actions, resolvers, controls, STORE_KEY }; +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks }; + +export { STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js index 583fd7540..d16106379 100644 --- a/modules/ppcp-settings/resources/js/data/common/reducer.js +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -2,7 +2,6 @@ * Reducer: Defines store structure and state updates for this module. * * Manages both transient (temporary) and persistent (saved) state. - * Each module uses isolated memory objects to prevent conflicts. * The initial state must define all properties, as dynamic additions are not supported. * * @file @@ -16,6 +15,7 @@ import ACTION_TYPES from './action-types'; const defaultTransient = { isReady: false, isSaving: false, + isBusy: false, }; const defaultPersistent = { @@ -38,6 +38,9 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) => setPersistent( state, action ), + + [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => + setPersistent( state, payload.data ), } ); export default commonReducer; diff --git a/modules/ppcp-settings/resources/js/data/common/resolvers.js b/modules/ppcp-settings/resources/js/data/common/resolvers.js index 2358496be..ceebca53f 100644 --- a/modules/ppcp-settings/resources/js/data/common/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/common/resolvers.js @@ -2,7 +2,7 @@ * Resolvers: Handle asynchronous data fetching for the store. * * These functions update store state with data from external sources. - * Each resolver corresponds to a specific selector but must have a unique name. + * Each resolver corresponds to a specific selector (selector with same name must exist). * Resolvers are called automatically when selectors request unavailable data. * * @file @@ -12,26 +12,25 @@ import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; -import { NAMESPACE } from '../constants'; -import { setIsReady, setCommonDetails } from './actions'; -import { REST_HYDRATE_PATH } from './constants'; +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; -/** - * Retrieve settings from the site's REST API. - */ -export function* commonPersistentData() { - const path = `${ NAMESPACE }/${ REST_HYDRATE_PATH }`; +export const resolvers = { + /** + * Retrieve settings from the site's REST API. + */ + *persistentData() { + try { + const result = yield apiFetch( { path: REST_HYDRATE_PATH } ); - try { - const result = yield apiFetch( { path } ); - yield setCommonDetails( result ); - yield setIsReady( true ); - } catch ( e ) { - yield dispatch( 'core/notices' ).createErrorNotice( - __( - 'Error retrieving plugin details.', - 'woocommerce-paypal-payments' - ) - ); - } -} + yield dispatch( STORE_NAME ).hydrate( result ); + yield dispatch( STORE_NAME ).setIsReady( true ); + } catch ( e ) { + yield dispatch( 'core/notices' ).createErrorNotice( + __( + 'Error retrieving plugin details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js index 4ea6e86a7..14334fcf3 100644 --- a/modules/ppcp-settings/resources/js/data/common/selectors.js +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -3,32 +3,19 @@ * * These functions provide a consistent interface for accessing store data. * They allow components to retrieve data without knowing the store structure. - * Exported functions must have unique names across all store modules. * * @file */ -import { STORE_KEY } from './constants'; - const EMPTY_OBJ = Object.freeze( {} ); -const getState = ( state ) => { - if ( ! state ) { - return EMPTY_OBJ; - } +const getState = ( state ) => state || EMPTY_OBJ; - return state[ STORE_KEY ] || EMPTY_OBJ; -}; - -export const commonPersistentData = ( state ) => { +export const persistentData = ( state ) => { return getState( state ).data || EMPTY_OBJ; }; -export const commonTransientData = ( state ) => { - const { data, flags, ...transientState } = getState( state ); +export const transientData = ( state ) => { + const { data, ...transientState } = getState( state ); return transientState || EMPTY_OBJ; }; - -export const commonFlags = ( state ) => { - return getState( state ).flags || EMPTY_OBJ; -}; From 49d088b252070a14194dd5e27ab7dda736e10fa1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 17:23:19 +0100 Subject: [PATCH 41/66] =?UTF-8?q?=F0=9F=90=9B=20Fix=20incorrect=20path=20a?= =?UTF-8?q?fter=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/Screens/Onboarding/Onboarding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index 5d053a793..30cd52ffe 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -1,7 +1,7 @@ import Container from '../../ReusableComponents/Container'; import { OnboardingHooks } from '../../../data'; import { getSteps } from './availableSteps'; -import Navigation from '../../ReusableComponents/Navigation'; +import Navigation from './Components/Navigation'; const getCurrentStep = ( requestedStep, steps ) => { const isValidStep = ( step ) => From ce5c6a3676b25d5532e7ec71b4b25057bffba912 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 18:37:28 +0100 Subject: [PATCH 42/66] =?UTF-8?q?=E2=9C=A8=20Stores=20now=20also=20export?= =?UTF-8?q?=20their=20selectors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/common/index.js | 4 +--- modules/ppcp-settings/resources/js/data/onboarding/index.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/index.js b/modules/ppcp-settings/resources/js/data/common/index.js index 60f81da60..28c162f98 100644 --- a/modules/ppcp-settings/resources/js/data/common/index.js +++ b/modules/ppcp-settings/resources/js/data/common/index.js @@ -21,6 +21,4 @@ export const initStore = () => { register( store ); }; -export { hooks }; - -export { STORE_NAME }; +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js index 60f81da60..28c162f98 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/index.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js @@ -21,6 +21,4 @@ export const initStore = () => { register( store ); }; -export { hooks }; - -export { STORE_NAME }; +export { hooks, selectors, STORE_NAME }; From e181892a3439a2478186bea110a1d8b71e544391 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 18:38:06 +0100 Subject: [PATCH 43/66] =?UTF-8?q?=E2=9C=A8=20Register=20the=20common=20sto?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/resources/js/data/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 9782613d4..b780dd5fc 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -1,10 +1,15 @@ -import * as Onboarding from './onboarding'; import { addDebugTools } from './debug'; +import * as Onboarding from './onboarding'; +import * as Common from './common'; Onboarding.initStore(); +Common.initStore(); export const OnboardingHooks = Onboarding.hooks; +export const CommonHooks = Common.hooks; + export const OnboardingStoreName = Onboarding.STORE_NAME; +export const CommonStoreName = Common.STORE_NAME; export * from './constants'; From 1b96b112c2a01059945fccbbfc43fd8155748412 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 20 Nov 2024 18:38:39 +0100 Subject: [PATCH 44/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20debugging=20c?= =?UTF-8?q?ode=20more=20dynamic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/debug.js | 20 ++++++++----------- .../ppcp-settings/resources/js/data/index.js | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index 96117b613..b292d1920 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -1,7 +1,6 @@ import { OnboardingStoreName } from './index'; -import * as selectors from './onboarding/selectors'; -export const addDebugTools = ( context ) => { +export const addDebugTools = ( context, modules ) => { if ( ! context || ! context?.debug ) { return; } @@ -13,23 +12,20 @@ export const addDebugTools = ( context ) => { return; } - [ OnboardingStoreName ].forEach( ( storeName ) => { - const storeSelector = `wp.data.select('${ storeName }')`; + modules.forEach( ( module ) => { + const storeName = module.STORE_NAME; + const storeSelector = `wp.data.select( '${ storeName }' )`; console.group( `[STORE] ${ storeSelector }` ); - const dumpStore = async ( selector ) => { - const contents = await wp.data - .resolveSelect( storeName ) - [ selector ](); + const dumpStore = ( selector ) => { + const contents = wp.data.select( storeName )[ selector ](); - console.groupCollapsed( `[SELECTOR] .${ selector }()` ); + console.groupCollapsed( `.${ selector }()` ); console.table( contents ); console.groupEnd(); }; - Object.keys( selectors ).forEach( async ( selector ) => { - await dumpStore( selector ); - } ); + Object.keys( module.selectors ).forEach( dumpStore ); console.groupEnd(); } ); diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index b780dd5fc..274aac790 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -13,4 +13,4 @@ export const CommonStoreName = Common.STORE_NAME; export * from './constants'; -addDebugTools( window.ppcpSettings ); +addDebugTools( window.ppcpSettings, [ Onboarding, Common ] ); From 10a3767e2fe5ad4d2c29e8722cb64ccee88a77fb Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 16:00:03 +0100 Subject: [PATCH 45/66] =?UTF-8?q?=E2=9C=A8=20Accordion=20can=20be=20toggle?= =?UTF-8?q?d=20via=20URL=20hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReusableComponents/AccordionSection.js | 29 +++++++++++++++++-- .../Screens/Onboarding/StepWelcome.js | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index 05cc758db..23f01a09c 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,3 +1,4 @@ +import { useEffect } from '@wordpress/element'; import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; @@ -5,11 +6,33 @@ import { useState } from 'react'; const Accordion = ( { title, - initiallyOpen = false, + initiallyOpen = null, className = '', + id = '', children, } ) => { - const [ isOpen, setIsOpen ] = useState( initiallyOpen ); + const determineInitialState = () => { + if ( id && initiallyOpen === null ) { + return window.location.hash === `#${ id }`; + } + return !! initiallyOpen; + }; + + const [ isOpen, setIsOpen ] = useState( determineInitialState ); + + useEffect( () => { + const handleHashChange = () => { + if ( id && window.location.hash === `#${ id }` ) { + setIsOpen( true ); + } + }; + + window.addEventListener( 'hashchange', handleHashChange ); + + return () => { + window.removeEventListener( 'hashchange', handleHashChange ); + }; + }, [ id ] ); const toggleOpen = ( ev ) => { setIsOpen( ! isOpen ); @@ -26,7 +49,7 @@ const Accordion = ( { } return ( -
+
- + Date: Thu, 21 Nov 2024 17:42:48 +0100 Subject: [PATCH 52/66] =?UTF-8?q?=F0=9F=8E=A8=20Small=20code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/data/common/actions.js | 8 +-- .../resources/js/data/onboarding/actions.js | 66 ++++--------------- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 63ace933b..edf6d8d4b 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -101,11 +101,9 @@ export const setClientSecret = ( clientSecret ) => ( { * @return {Action} The action. */ export const persist = function* () { - yield setIsBusy( true ); - yield { - type: ACTION_TYPES.DO_PERSIST_DATA, - }; - yield setIsBusy( false ); + const data = yield select( STORE_NAME ).persistentData(); + + yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; }; /** diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 84bca8e1a..1742048e1 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -25,17 +25,6 @@ import { STORE_NAME } from './constants'; */ export const reset = () => ( { type: ACTION_TYPES.RESET } ); -/** - * Transient. Marks the onboarding details as "ready", i.e., fully initialized. - * - * @param {boolean} isReady - * @return {Action} The action. - */ -export const setIsReady = ( isReady ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, -} ); - /** * Persistent. Set the full onboarding details, usually during app initialization. * @@ -47,6 +36,17 @@ export const hydrate = ( payload ) => ( { payload, } ); +/** + * Transient. Marks the onboarding details as "ready", i.e., fully initialized. + * + * @param {boolean} isReady + * @return {Action} The action. + */ +export const setIsReady = ( isReady ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { isReady }, +} ); + /** * Persistent.Set the "onboarding completed" flag which shows or hides the wizard. * @@ -69,50 +69,6 @@ export const setStep = ( step ) => ( { payload: { step }, } ); -/** - * Persistent. Sets the sandbox mode on or off. - * - * @param {boolean} useSandbox - * @return {Action} The action. - */ -export const setSandboxMode = ( useSandbox ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - payload: { useSandbox }, -} ); - -/** - * Persistent. Toggles the "Manual Connection" mode on or off. - * - * @param {boolean} useManualConnection - * @return {Action} The action. - */ -export const setManualConnectionMode = ( useManualConnection ) => ( { - type: ACTION_TYPES.SET_PERSISTENT, - 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 }, -} ); - /** * Persistent. Sets the "isCasualSeller" value. * From f2f0329e4e5729b3f9f53cfe9adefb7d136cca8c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 17:43:22 +0100 Subject: [PATCH 53/66] =?UTF-8?q?=F0=9F=90=9B=20Implement=20missing=20comm?= =?UTF-8?q?on-store=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppcp-settings/resources/js/data/common/actions.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index edf6d8d4b..6c20ca5a6 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -18,6 +18,17 @@ import { STORE_NAME } from './constants'; * @property {Object?} payload - Optional payload for the action. */ +/** + * Persistent. Set the full onboarding details, usually during app initialization. + * + * @param {{data: {}, flags?: {}}} payload + * @return {Action} The action. + */ +export const hydrate = ( payload ) => ( { + type: ACTION_TYPES.HYDRATE, + payload, +} ); + /** * Transient. Marks the onboarding details as "ready", i.e., fully initialized. * From a0910962b4ba4b9d078a84f124268ca0aa19dab1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 18:04:11 +0100 Subject: [PATCH 54/66] =?UTF-8?q?=F0=9F=90=9B=20Move=20required=20action?= =?UTF-8?q?=20type=20to=20common=20store?= 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 | 1 + modules/ppcp-settings/resources/js/data/common/hooks.js | 2 +- .../ppcp-settings/resources/js/data/onboarding/action-types.js | 1 - 3 files changed, 2 insertions(+), 2 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 5d5968591..908d3ebad 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -14,4 +14,5 @@ export default { // Controls - always start with "DO_". DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA', + DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION', }; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 642eed3dc..97b16e6a6 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -8,9 +8,9 @@ */ import { useDispatch, useSelect } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; import { STORE_NAME } from './constants'; -import { useCallback } from '@wordpress/element'; const useTransient = ( key ) => useSelect( diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index f5d9944d3..2e16f8468 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -15,5 +15,4 @@ export default { // Controls - always start with "DO_". DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA', - DO_MANUAL_CONNECTION: 'ONBOARDING:DO_MANUAL_CONNECTION', }; From b7ef3242bf703afaf4645d1e302e34d4e9dbdb60 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:04:27 +0100 Subject: [PATCH 55/66] =?UTF-8?q?=E2=9C=A8=20Standardize=20REST=20response?= =?UTF-8?q?=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/CommonRestEndpoint.php | 6 +-- .../Endpoint/ConnectManualRestEndpoint.php | 27 +++------- .../src/Endpoint/LoginLinkRestEndpoint.php | 7 +-- .../src/Endpoint/OnboardingRestEndpoint.php | 4 +- .../src/Endpoint/RestEndpoint.php | 49 +++++++++++++++++++ 5 files changed, 62 insertions(+), 31 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 18c9de902..c7345148e 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -109,11 +109,7 @@ class CommonRestEndpoint extends RestEndpoint { $this->field_map ); - return rest_ensure_response( - array( - 'data' => $js_data, - ) - ); + return $this->return_success( $js_data ); } /** diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php index 5225b4d13..e394b880b 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php @@ -126,33 +126,22 @@ class ConnectManualRestEndpoint extends RestEndpoint { $use_sandbox = (bool) ( $data['use_sandbox'] ?? false ); if ( empty( $client_id ) || empty( $client_secret ) ) { - return rest_ensure_response( - array( - 'success' => false, - 'message' => 'No client ID or secret provided.', - ) - ); + return $this->return_error( 'No client ID or secret provided.' ); } try { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); } catch ( Exception $exception ) { - return rest_ensure_response( - array( - 'success' => false, - 'message' => $exception->getMessage(), - ) - ); - + return $this->return_error( $exception->getMessage() ); } - $result = array( - 'merchantId' => $payee->merchant_id, - 'email' => $payee->email_address, - 'success' => true, + return $this->return_success( + array( + 'merchantId' => $payee->merchant_id, + 'email' => $payee->email_address, + 'success' => true, + ) ); - - return rest_ensure_response( $result ); } /** diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index cb969c7ca..85ced8276 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -93,12 +93,9 @@ class LoginLinkRestEndpoint extends RestEndpoint { try { $url = $url_generator->generate( $products ); - return rest_ensure_response( $url ); + return $this->return_success( $url ); } catch ( \Exception $e ) { - return new WP_REST_Response( - array( 'error' => $e->getMessage() ), - 500 - ); + return $this->return_error( $e->getMessage() ); } } } diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 03690d3bf..0a102c178 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -131,9 +131,9 @@ class OnboardingRestEndpoint extends RestEndpoint { $this->flag_map ); - return rest_ensure_response( + return $this->return_success( + $js_data, array( - 'data' => $js_data, 'flags' => $js_flags, ) ); diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 35b2912fe..76626ac0c 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -10,6 +10,7 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; use WC_REST_Controller; +use WP_REST_Response; /** * Base class for REST controllers in the settings module. @@ -31,6 +32,54 @@ abstract class RestEndpoint extends WC_REST_Controller { return current_user_can( 'manage_woocommerce' ); } + /** + * Returns a successful REST API response. + * + * @param mixed $data The main response data. + * @param array $extra Optional, additional response data. + * + * @return WP_REST_Response The successful response. + */ + protected function return_success( $data, array $extra = array() ) : WP_REST_Response { + $response = array( + 'success' => true, + 'data' => $data, + ); + + if ( $extra ) { + foreach ( $extra as $key => $value ) { + if ( isset( $response[ $key ] ) ) { + continue; + } + + $response[ $key ] = $value; + } + } + + return rest_ensure_response( $response ); + } + + /** + * Returns an error REST API response. + * + * @param string $reason The reason for the error. + * @param mixed $details Optional details about the error. + * + * @return WP_REST_Response The error response. + */ + protected function return_error( string $reason, $details = null ) : WP_REST_Response { + $response = array( + 'success' => false, + 'message' => $reason, + ); + + if ( ! is_null( $details ) ) { + $response['details'] = $details; + } + + return rest_ensure_response( $response ); + } + /** * Sanitizes parameters based on a field mapping. * From 2a28f38491549fe73ddf46fb52b9759f59a3b3d1 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:07:32 +0100 Subject: [PATCH 56/66] =?UTF-8?q?=E2=9C=A8=20Implement=20sandbox=20login?= =?UTF-8?q?=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 21 ++++++++++++-- .../resources/js/data/common/action-types.js | 1 + .../resources/js/data/common/actions.js | 14 ++++++++++ .../resources/js/data/common/constants.js | 10 +++++++ .../resources/js/data/common/controls.js | 28 ++++++++++++++++++- .../resources/js/data/common/hooks.js | 6 ++-- .../src/Endpoint/LoginLinkRestEndpoint.php | 10 +++++-- 7 files changed, 82 insertions(+), 8 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 acc78af72..3702a6e14 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 @@ -11,7 +11,8 @@ import { OnboardingHooks, CommonHooks } from '../../../../data'; const AdvancedOptionsForm = ( { setCompleted } ) => { const { isBusy } = CommonHooks.useBusyState(); - const { isSandboxMode, setSandboxMode } = CommonHooks.useSandbox(); + const { isSandboxMode, setSandboxMode, connectViaSandbox } = + CommonHooks.useSandbox(); const { isManualConnectionMode, setManualConnectionMode, @@ -81,6 +82,21 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { setCompleted( true ); }; + const handleSandboxConnect = async () => { + const res = await connectViaSandbox(); + + if ( ! res.success || ! res.data ) { + handleServerError( + res, + __( + 'Could not generate a Sandbox login link.', + 'woocommerce-paypal-payments' + ) + ); + return; + } + }; + const handleConnect = async () => { if ( ! handleFormValidation() ) { return; @@ -117,8 +133,9 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { ) } isToggled={ !! isSandboxMode } setToggled={ setSandboxMode } + isLoading={ isBusy } > - 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 908d3ebad..47de76afe 100644 --- a/modules/ppcp-settings/resources/js/data/common/action-types.js +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -15,4 +15,5 @@ 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', }; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js index 6c20ca5a6..619aaca5f 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions.js +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -117,6 +117,20 @@ export const persist = function* () { yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; }; +/** + * Side effect. Initiates the sandbox login ISU. + * + * @return {Action} The action. + */ +export const connectViaSandbox = function* () { + yield setIsBusy( true ); + + const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN }; + yield setIsBusy( false ); + + return result; +}; + /** * Side effect. Initiates a manual connection attempt using the provided client ID and secret. * diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js index c0fa5200f..c7ea9b4c1 100644 --- a/modules/ppcp-settings/resources/js/data/common/constants.js +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -34,3 +34,13 @@ export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common'; * @type {string} */ export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual'; + +/** + * REST path to generate an ISU URL for the sandbox-login. + * + * Used by: Controls + * See: LoginLinkRestEndpoint.php + * + * @type {string} + */ +export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js index 78dcc7f38..6de513e0b 100644 --- a/modules/ppcp-settings/resources/js/data/common/controls.js +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -9,7 +9,11 @@ import apiFetch from '@wordpress/api-fetch'; -import { REST_PERSIST_PATH, REST_MANUAL_CONNECTION_PATH } from './constants'; +import { + REST_PERSIST_PATH, + REST_MANUAL_CONNECTION_PATH, + REST_SANDBOX_CONNECTION_PATH, +} from './constants'; import ACTION_TYPES from './action-types'; export const controls = { @@ -25,6 +29,28 @@ export const controls = { } }, + async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() { + let result = null; + + try { + result = await apiFetch( { + path: REST_SANDBOX_CONNECTION_PATH, + method: 'POST', + data: { + environment: 'sandbox', + products: [ 'EXPRESS_CHECKOUT' ], + }, + } ); + } catch ( e ) { + result = { + success: false, + error: e, + }; + } + + return result; + }, + async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( { clientId, clientSecret, diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 97b16e6a6..8be3857b0 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -31,6 +31,7 @@ const useHooks = () => { setManualConnectionMode, setClientId, setClientSecret, + connectViaSandbox, connectViaIdAndSecret, } = useDispatch( STORE_NAME ); @@ -66,6 +67,7 @@ const useHooks = () => { setClientSecret: ( value ) => { return savePersistent( setClientSecret, value ); }, + connectViaSandbox, connectViaIdAndSecret, }; }; @@ -81,9 +83,9 @@ export const useBusyState = () => { }; export const useSandbox = () => { - const { isSandboxMode, setSandboxMode } = useHooks(); + const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks(); - return { isSandboxMode, setSandboxMode }; + return { isSandboxMode, setSandboxMode, connectViaSandbox }; }; export const useManualConnection = () => { diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index 85ced8276..8ed204383 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -47,14 +47,18 @@ class LoginLinkRestEndpoint extends RestEndpoint { public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\w]+)', + '/' . $this->rest_base, array( array( - 'methods' => WP_REST_Server::READABLE, + 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'get_login_url' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( - 'products' => array( + 'environment' => array( + 'required' => true, + 'type' => 'string', + ), + 'products' => array( 'required' => true, 'type' => 'array', 'items' => array( From 88f2ed9185fd8f8f9a035e54572055807c8a100a Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:08:50 +0100 Subject: [PATCH 57/66] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20code=20impro?= =?UTF-8?q?vement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 24 +++++++++---------- 1 file changed, 11 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 3702a6e14..79724a533 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,7 +7,7 @@ import { store as noticesStore } from '@wordpress/notices'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; -import { OnboardingHooks, CommonHooks } from '../../../../data'; +import { CommonHooks } from '../../../../data'; const AdvancedOptionsForm = ( { setCompleted } ) => { const { isBusy } = CommonHooks.useBusyState(); @@ -61,17 +61,9 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { return true; }; - const handleServerError = ( res ) => { - if ( res.message ) { - createErrorNotice( res.message ); - } else { - createErrorNotice( - __( - 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', - 'woocommerce-paypal-payments' - ) - ); - } + const handleServerError = ( res, genericMessage ) => { + console.error( 'Connection error', res ); + createErrorNotice( res?.message ?? genericMessage ); }; const handleServerSuccess = () => { @@ -107,7 +99,13 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { if ( res.success ) { handleServerSuccess(); } else { - handleServerError( res ); + handleServerError( + res, + __( + 'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.', + 'woocommerce-paypal-payments' + ) + ); } }; From 5f8dda098039db479268cda91ceb98c40c59024c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:10:02 +0100 Subject: [PATCH 58/66] =?UTF-8?q?=E2=9C=A8=20Implement=20PayPal=20connecti?= =?UTF-8?q?on=20via=20popup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AdvancedOptionsForm.js | 17 +++++++- .../resources/js/utils/window.js | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create 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 79724a533..b18edc6fb 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 @@ -8,6 +8,7 @@ import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock import Separator from '../../../ReusableComponents/Separator'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; import { CommonHooks } from '../../../../data'; +import { openPopup } from '../../../../utils/window'; const AdvancedOptionsForm = ( { setCompleted } ) => { const { isBusy } = CommonHooks.useBusyState(); @@ -87,9 +88,21 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { ); return; } + + const connectionUrl = res.data; + const popup = openPopup( connectionUrl ); + + if ( ! popup ) { + createErrorNotice( + __( + 'Popup blocked. Please allow popups for this site to connect to PayPal.', + 'woocommerce-paypal-payments' + ) + ); + } }; - const handleConnect = async () => { + const handleManualConnect = async () => { if ( ! handleFormValidation() ) { return; } @@ -183,7 +196,7 @@ const AdvancedOptionsForm = ( { setCompleted } ) => { onChange={ setClientSecret } type="password" /> - diff --git a/modules/ppcp-settings/resources/js/utils/window.js b/modules/ppcp-settings/resources/js/utils/window.js new file mode 100644 index 000000000..165874302 --- /dev/null +++ b/modules/ppcp-settings/resources/js/utils/window.js @@ -0,0 +1,42 @@ +/** + * 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 a95296b8ed5b91795fa3ac0ef09992963093668d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:28:08 +0100 Subject: [PATCH 59/66] =?UTF-8?q?=F0=9F=8E=A8=20Code=20format=20&=20cleanu?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Screens/Onboarding/StepBusiness.js | 47 ++++++++----------- .../Screens/Onboarding/StepCompleteSetup.js | 10 ++-- .../Screens/Onboarding/StepProducts.js | 10 ++-- .../Screens/Onboarding/StepWelcome.js | 4 +- 4 files changed, 28 insertions(+), 43 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js index 6dcf435c9..a223686ff 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js @@ -1,18 +1,13 @@ +import { __ } from '@wordpress/i18n'; + import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBox from '../../ReusableComponents/SelectBox'; -import { __ } from '@wordpress/i18n'; -import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; import { OnboardingHooks, BUSINESS_TYPES } from '../../../data'; const BUSINESS_RADIO_GROUP_NAME = 'business'; -const StepBusiness = ( { - setStep, - currentStep, - stepperOrder, - setCompleted, -} ) => { +const StepBusiness = ( {} ) => { const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness(); const handleSellerTypeChange = ( value ) => { @@ -39,23 +34,22 @@ const StepBusiness = ( { />
- - + - + >
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index 5dc18f619..76afa8de2 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -1,13 +1,9 @@ -import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import { __ } from '@wordpress/i18n'; import { Button, Icon } from '@wordpress/components'; -const StepCompleteSetup = ( { - setStep, - currentStep, - stepperOrder, - setCompleted, -} ) => { +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; + +const StepCompleteSetup = ( { setCompleted } ) => { const ButtonIcon = () => ( ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index d84fed57e..76311f67e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -1,17 +1,13 @@ -import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import { __ } from '@wordpress/i18n'; + +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import { OnboardingHooks, PRODUCT_TYPES } from '../../../data'; const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; -const StepProducts = ( { - setStep, - currentStep, - stepperOrder, - setCompleted, -} ) => { +const StepProducts = () => { const { products, toggleProduct } = OnboardingHooks.useProducts(); return ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index f94d57eac..2220efb37 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -1,13 +1,13 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; import Separator from '../../ReusableComponents/Separator'; import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs'; +import AccordionSection from '../../ReusableComponents/AccordionSection'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; -import AccordionSection from '../../ReusableComponents/AccordionSection'; const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { return ( From c7e5395e1269bfc61e3c02266cc8367bb27a1343 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:28:32 +0100 Subject: [PATCH 60/66] =?UTF-8?q?=F0=9F=90=9B=20Fix=20incorrect=20hook=20u?= =?UTF-8?q?sage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/Components/Screens/Onboarding/StepProducts.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index 76311f67e..cbd642327 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -8,7 +8,7 @@ import { OnboardingHooks, PRODUCT_TYPES } from '../../../data'; const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; const StepProducts = () => { - const { products, toggleProduct } = OnboardingHooks.useProducts(); + const { products, setProducts } = OnboardingHooks.useProducts(); return (
@@ -28,7 +28,7 @@ const StepProducts = () => { ) } name={ PRODUCTS_CHECKBOX_GROUP_NAME } value={ PRODUCT_TYPES.VIRTUAL } - changeCallback={ toggleProduct } + changeCallback={ setProducts } currentValue={ products } type="checkbox" > @@ -70,7 +70,7 @@ const StepProducts = () => { ) } name={ PRODUCTS_CHECKBOX_GROUP_NAME } value={ PRODUCT_TYPES.PHYSICAL } - changeCallback={ toggleProduct } + changeCallback={ setProducts } currentValue={ products } type="checkbox" > @@ -97,7 +97,7 @@ const StepProducts = () => { ) } name={ PRODUCTS_CHECKBOX_GROUP_NAME } value={ PRODUCT_TYPES.SUBSCRIPTIONS } - changeCallback={ toggleProduct } + changeCallback={ setProducts } currentValue={ products } type="checkbox" > From a923045eff8e120b357a8bcb63d6edaa4b856a8b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 21 Nov 2024 19:39:39 +0100 Subject: [PATCH 61/66] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20code=20style=20issue?= =?UTF-8?q?s=20&=20add=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Endpoint/SwitchSettingsUiEndpoint.php | 2 ++ modules/ppcp-settings/src/SettingsModule.php | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php index ebc85d9dc..244c26dfe 100644 --- a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php @@ -15,6 +15,8 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; /** * Class SwitchSettingsUiEndpoint + * + * Note: This is an ajax handler, not a REST endpoint */ class SwitchSettingsUiEndpoint { diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index b668acbfb..7c9dca2f8 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -88,10 +88,13 @@ class SettingsModule implements ServiceModule, ExecutableModule { $endpoint = $container->get( 'settings.switch-ui.endpoint' ); assert( $endpoint instanceof SwitchSettingsUiEndpoint ); - add_action( 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT, array( - $endpoint, - 'handle_request', - ) ); + add_action( + 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT, + array( + $endpoint, + 'handle_request', + ) + ); return true; } From 7f10cade0aad43fe4905e8883e4a5344e14c6f16 Mon Sep 17 00:00:00 2001 From: Himad M Date: Thu, 21 Nov 2024 18:27:32 -0400 Subject: [PATCH 62/66] New Settings UI: Update onboarding button styles --- .../components/reusable-components/_button.scss | 8 ++++---- .../Screens/Onboarding/StepCompleteSetup.js | 16 ++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 027016760..9815633e3 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -8,16 +8,16 @@ button.components-button, a.components-button { color: $color-white; } - border-radius: 2px; - padding: 14px 17px; + border-radius: 50px; + padding: 15px 32px; height: auto; } &.is-primary { - @include font(13, 20, 400); + @include font(14, 18, 900); &:not(:disabled) { - background-color: $color-blueberry; + background-color: $color-black; } } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index 5dc18f619..9e492ab44 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -12,20 +12,16 @@ const StepCompleteSetup = ( { ( - - + ) } /> From 1ad8127015e85321ae5300068f596de3b7d7d88b Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 22 Nov 2024 13:53:53 +0100 Subject: [PATCH 63/66] Update modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php Co-authored-by: Narek Zakarian --- modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index 8ed204383..ac05b0030 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -23,7 +23,7 @@ class LoginLinkRestEndpoint extends RestEndpoint { * * @var string */ - protected $rest_base = 'login_link'; + protected string $rest_base = 'login_link'; /** * Link generator list, with environment name as array key. From 3e00c3fe6055f390e066a99cab6611282e66d10e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 22 Nov 2024 13:54:03 +0100 Subject: [PATCH 64/66] Update modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php Co-authored-by: Narek Zakarian --- modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index c7345148e..f5ee5c39e 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -27,7 +27,7 @@ class CommonRestEndpoint extends RestEndpoint { * * @var string */ - protected $rest_base = 'common'; + protected string $rest_base = 'common'; /** * The settings instance. From 8e19b6b8043473d53f9fafe7d70d777745390741 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 22 Nov 2024 21:30:21 +0100 Subject: [PATCH 65/66] =?UTF-8?q?=F0=9F=90=9B=20Fix=20fatal=20error=20in?= =?UTF-8?q?=20derived=20REST=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php | 2 +- modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index f5ee5c39e..c7345148e 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -27,7 +27,7 @@ class CommonRestEndpoint extends RestEndpoint { * * @var string */ - protected string $rest_base = 'common'; + protected $rest_base = 'common'; /** * The settings instance. diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index ac05b0030..8ed204383 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -23,7 +23,7 @@ class LoginLinkRestEndpoint extends RestEndpoint { * * @var string */ - protected string $rest_base = 'login_link'; + protected $rest_base = 'login_link'; /** * Link generator list, with environment name as array key. From 4d729cd5dcbebb67a23735e59157219b26e933d4 Mon Sep 17 00:00:00 2001 From: Himad M Date: Mon, 25 Nov 2024 20:47:13 -0400 Subject: [PATCH 66/66] New Settings UI: Add optional payment methods step --- .../reusable-components/_badge-box.scss | 2 +- .../reusable-components/_separator.scss | 2 +- .../reusable-components/_welcome-docs.scss | 10 - .../css/components/screens/_onboarding.scss | 1 + .../onboarding/_step-payment-methods.scss | 38 ++ .../screens/onboarding/_step-welcome.scss | 5 +- .../AcdcOptionalPaymentMethods.js | 271 ++++++++ .../BcdcOptionalPaymentMethods.js | 66 ++ .../OptionalPaymentMethods.js | 28 + .../WelcomeDocs/AcdcFlow.js | 585 ++++++++---------- .../WelcomeDocs/BcdcFlow.js | 328 +++++----- .../WelcomeDocs/WelcomeDocs.js | 4 +- .../Screens/Onboarding/StepPaymentMethods.js | 78 +++ .../Screens/Onboarding/availableSteps.js | 2 + .../resources/js/data/onboarding/actions.js | 13 + .../resources/js/data/onboarding/hooks.js | 29 +- .../resources/js/data/onboarding/reducer.js | 1 + .../src/Data/OnboardingProfile.php | 10 + .../src/Endpoint/OnboardingRestEndpoint.php | 4 + 19 files changed, 987 insertions(+), 490 deletions(-) create mode 100644 modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js create mode 100644 modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js create mode 100644 modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss index d65d1f184..427b4b1fb 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss @@ -18,7 +18,7 @@ } } - &-image-badge { + .ppcp-r-badge-box__title-text:not(:empty) + .ppcp-r-badge-box__title-image-badge { margin-left: 7px; img { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss index 8d64755fd..6c3976ed7 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss @@ -10,7 +10,7 @@ } &__line { - background-color: $color-gray-600; + background-color: $color-gray-400; height: 1px; } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss index e78c940ea..15c5265e6 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss @@ -7,16 +7,6 @@ margin: 0 0 32px 0; } - &__description { - text-align: center; - @include font(14, 22, 400); - font-style: italic; - - a { - color: $color-gray-700; - } - } - &__wrapper { padding: 8px; margin: 0 0 48px 0; diff --git a/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss b/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss index 5a00adc9d..cfef2e04f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss @@ -1,6 +1,7 @@ @import './onboarding/step-welcome'; @import './onboarding/step-business'; @import './onboarding/step-products'; +@import './onboarding/step-payment-methods'; .ppcp-r-tabs.onboarding, .ppcp-r-container--onboarding { diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss new file mode 100644 index 000000000..4ab630733 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss @@ -0,0 +1,38 @@ +.ppcp-r-page-optional-payment-methods { + .ppcp-r-select-box:first-child { + .ppcp-r-select-box__title { + margin-bottom: 20px; + } + } +} + +.ppcp-r-optional-payment-methods { + &__wrapper { + .ppcp-r-badge-box { + margin: 0 0 24px 0; + &:last-child { + margin: 0; + } + } + + .ppcp-r-badge-box__description { + margin: 12px 0 0 0; + @include font(14, 20, 400); + } + } + + &__description { + margin: 32px 0 0 0; + text-align: center; + @include font(14, 22, 400); + font-style: italic; + + a { + color: $color-gray-700; + } + } + + &__separator { + margin: 0 0 24px 0; + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss index f8fb42050..394b2bd10 100644 --- a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss @@ -16,13 +16,10 @@ text-align: center; } - .ppcp-r-page-welcome-or-separator { - margin: 0 0 16px 0; - } - .components-base-control__field { margin: 0 0 24px 0; } + .ppcp-r-toggle-block__toggled-content > button{ @include small-button; color: $color-white; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js new file mode 100644 index 000000000..18a5456af --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/AcdcOptionalPaymentMethods.js @@ -0,0 +1,271 @@ +import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; +import { __, sprintf } from '@wordpress/i18n'; +import Separator from '../Separator'; + +const AcdcOptionalPaymentMethods = ( { + isFastlane, + isPayLater, + storeCountry, +} ) => { + if ( isFastlane && isPayLater && storeCountry === 'us' ) { + return ( +
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Accept Apple Pay on eligible devices and Google Pay through mobile and web. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Seamless payments for customers across the globe using their preferred payment methods. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Speed up guest checkout with Fatslane. Link a customer\'s email address to their payment details. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+ ); + } + + if ( isPayLater && storeCountry === 'uk' ) { + return ( +
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Accept Apple Pay on eligible devices and Google Pay through mobile and web. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Seamless payments for customers across the globe using their preferred payment methods. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+ ); + } + + return ( +
+ + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Accept Apple Pay on eligible devices and Google Pay through mobile and web. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Seamless payments for customers across the globe using their preferred payment methods. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+ ); +}; + +export default AcdcOptionalPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js new file mode 100644 index 000000000..d48c37fd5 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/BcdcOptionalPaymentMethods.js @@ -0,0 +1,66 @@ +import BadgeBox from '../BadgeBox'; +import { __, sprintf } from '@wordpress/i18n'; + +const BcdcOptionalPaymentMethods = ( { isPayLater, storeCountry } ) => { + if ( isPayLater && storeCountry === 'us' ) { + return ( +
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Process major credit and debit cards through PayPal’s card fields. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+ ); + } + + return ( +
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ sprintf( + // translators: %s: Link to PayPal REST application guide + __( + 'Process major credit and debit cards through PayPal’s card fields. Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+ ); +}; + +export default BcdcOptionalPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js new file mode 100644 index 000000000..b83fad366 --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods.js @@ -0,0 +1,28 @@ +import AcdcOptionalPaymentMethods from './AcdcOptionalPaymentMethods'; +import BcdcOptionalPaymentMethods from './BcdcOptionalPaymentMethods'; + +const OptionalPaymentMethods = ( { + useAcdc, + isFastlane, + isPayLater, + storeCountry, +} ) => { + return ( +
+ { useAcdc ? ( + + ) : ( + + ) } +
+ ); +}; + +export default OptionalPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js index 18fc248b2..de52259a8 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/AcdcFlow.js @@ -1,327 +1,276 @@ -import BadgeBox, { BADGE_BOX_TITLE_BIG } from "../BadgeBox"; +import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; import Separator from '../Separator'; +import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods'; const AcdcFlow = ( { isFastlane, isPayLater, storeCountry } ) => { - if (isFastlane && isPayLater && storeCountry === 'us') { - return ( -
-
- 1', 'woocommerce-paypal-payments')} - description={__( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', - 'woocommerce-paypal-payments' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Accept Apple Pay on eligible devices and Google Pay through mobile and web. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Seamless payments for customers across the globe using their preferred payment methods. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Speed up guest checkout with Fatslane. Link a customer\'s email address to their payment details. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- ); - } + if ( isFastlane && isPayLater && storeCountry === 'us' ) { + return ( +
+
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', + 'woocommerce-paypal-payments' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+
+ + +
+
+ ); + } - if (isPayLater && storeCountry === 'uk') { - return ( -
-
- 1', 'woocommerce-paypal-payments')} - description={__( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', - 'woocommerce-paypal-payments' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Accept Apple Pay on eligible devices and Google Pay through mobile and web. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Seamless payments for customers across the globe using their preferred payment methods. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- ); - } + if ( isPayLater && storeCountry === 'uk' ) { + return ( +
+
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', + 'woocommerce-paypal-payments' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+
+ + +
+
+ ); + } - return ( -
-
- 1', 'woocommerce-paypal-payments')} - description={__( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', - 'woocommerce-paypal-payments' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Style the credit card fields to match your own style. Includes advanced processing with risk management, 3D Secure, fraud protection options, and chargeback protection. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Accept Apple Pay on eligible devices and Google Pay through mobile and web. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Seamless payments for customers across the globe using their preferred payment methods. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- ); + return ( +
+
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', + 'woocommerce-paypal-payments' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+
+ + +
+
+ ); }; export default AcdcFlow; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js index c2400ba62..9ba0d14e4 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/BcdcFlow.js @@ -1,160 +1,184 @@ -import BadgeBox, { BADGE_BOX_TITLE_BIG } from "../BadgeBox"; +import BadgeBox, { BADGE_BOX_TITLE_BIG } from '../BadgeBox'; import { __, sprintf } from '@wordpress/i18n'; import Separator from '../Separator'; +import OptionalPaymentMethods from '../OptionalPaymentMethods/OptionalPaymentMethods'; const BcdcFlow = ( { isPayLater, storeCountry } ) => { - if (isPayLater && storeCountry === 'us') { - return ( -
-
- 1', 'woocommerce-paypal-payments')} - description={__( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', - 'woocommerce-paypal-payments' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Process major credit and debit cards through PayPal’s card fields. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
-
- ); - } + if ( isPayLater && storeCountry === 'us' ) { + return ( +
+
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', + 'woocommerce-paypal-payments' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> +
+
+ + +
+
+ ); + } - return ( -
- 1', 'woocommerce-paypal-payments')} - description={__( - 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', - 'woocommerce-paypal-payments' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> - - - 1', 'woocommerce-paypal-payments')} - description={sprintf( - // translators: %s: Link to PayPal REST application guide - __( - 'Process major credit and debit cards through PayPal’s card fields. Learn more', - 'woocommerce-paypal-payments' - ), - 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' - )} - /> -
- ); + return ( +
+ 1', + 'woocommerce-paypal-payments' + ) } + description={ __( + 'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion', + 'woocommerce-paypal-payments' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + Learn more', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ) } + /> + + + +
+ ); }; export default BcdcFlow; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js index b32c1f85e..182f25b04 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/WelcomeDocs/WelcomeDocs.js @@ -22,8 +22,8 @@ const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry, storeCurr )}

); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js new file mode 100644 index 000000000..a9d2f6b9e --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js @@ -0,0 +1,78 @@ +import { __, sprintf } from '@wordpress/i18n'; + +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; +import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; +import SelectBox from '../../ReusableComponents/SelectBox'; +import { OnboardingHooks } from '../../../data'; +import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods'; + +const OPM_RADIO_GROUP_NAME = 'optional-payment-methods'; + +const StepPaymentMethods = ( {} ) => { + const { + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, + } = OnboardingHooks.useOptionalPaymentMethods(); + const pricesBasedDescription = sprintf( + // translators: %s: Link to PayPal REST application guide + __( + '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ); + + return ( +
+ +
+ + + } + name={ OPM_RADIO_GROUP_NAME } + value={ true } + changeCallback={ setAreOptionalPaymentMethodsEnabled } + currentValue={ areOptionalPaymentMethodsEnabled } + type="radio" + > + + +

+
+
+ ); +}; + +export default StepPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js index a5555180d..7e8ea1556 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js @@ -1,6 +1,7 @@ import StepWelcome from './StepWelcome'; import StepBusiness from './StepBusiness'; import StepProducts from './StepProducts'; +import StepPaymentMethods from './StepPaymentMethods'; import StepCompleteSetup from './StepCompleteSetup'; export const getSteps = ( flags ) => { @@ -8,6 +9,7 @@ export const getSteps = ( flags ) => { StepWelcome, StepBusiness, StepProducts, + StepPaymentMethods, StepCompleteSetup, ]; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 1742048e1..dcf401995 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -80,6 +80,19 @@ export const setIsCasualSeller = ( isCasualSeller ) => ( { payload: { isCasualSeller }, } ); +/** + * Persistent. Sets the "areOptionalPaymentMethodsEnabled" value. + * + * @param {boolean} areOptionalPaymentMethodsEnabled + * @return {Action} The action. + */ +export const setAreOptionalPaymentMethodsEnabled = ( + areOptionalPaymentMethodsEnabled +) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { areOptionalPaymentMethodsEnabled }, +} ); + /** * Persistent. Sets the "products" array. * diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index e240f0863..4ae5bd947 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -25,8 +25,14 @@ const usePersistent = ( key ) => ); const useHooks = () => { - const { persist, setStep, setCompleted, setIsCasualSeller, setProducts } = - useDispatch( STORE_NAME ); + const { + persist, + setStep, + setCompleted, + setIsCasualSeller, + setAreOptionalPaymentMethodsEnabled, + setProducts, + } = useDispatch( STORE_NAME ); // Read-only flags. const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); @@ -38,6 +44,9 @@ const useHooks = () => { const step = usePersistent( 'step' ); const completed = usePersistent( 'completed' ); const isCasualSeller = usePersistent( 'isCasualSeller' ); + const areOptionalPaymentMethodsEnabled = usePersistent( + 'areOptionalPaymentMethodsEnabled' + ); const products = usePersistent( 'products' ); const savePersistent = async ( setter, value ) => { @@ -60,6 +69,10 @@ const useHooks = () => { setIsCasualSeller: ( value ) => { return savePersistent( setIsCasualSeller, value ); }, + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled: ( value ) => { + return savePersistent( setAreOptionalPaymentMethodsEnabled, value ); + }, products, setProducts: ( activeProducts ) => { const validProducts = activeProducts.filter( ( item ) => @@ -82,6 +95,18 @@ export const useProducts = () => { return { products, setProducts }; }; +export const useOptionalPaymentMethods = () => { + const { + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, + } = useHooks(); + + return { + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, + }; +}; + export const useSteps = () => { const { flags, isReady, step, setStep, completed, setCompleted } = useHooks(); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 4c80cccf1..176d4875d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -27,6 +27,7 @@ const defaultPersistent = { completed: false, step: 0, isCasualSeller: null, // null value will uncheck both options in the UI. + areOptionalPaymentMethodsEnabled: true, products: [], }; diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index 3e812a009..b04d7879f 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -67,6 +67,7 @@ class OnboardingProfile extends AbstractDataModel { 'completed' => false, 'step' => 0, 'is_casual_seller' => null, + 'are_optional_payment_methods_enabled' => true, 'products' => array(), ); } @@ -127,6 +128,15 @@ class OnboardingProfile extends AbstractDataModel { $this->data['is_casual_seller'] = $casual_seller; } + /** + * Sets the optional payment methods flag. + * + * @param bool|null $are_optional_payment_methods_enabled Whether the PayPal optional payment methods are enabled. + */ + public function set_are_optional_payment_methods_enabled( ?bool $are_optional_payment_methods_enabled ) : void { + $this->data['are_optional_payment_methods_enabled'] = $are_optional_payment_methods_enabled; + } + /** * Gets the active product types for this store. * diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 0a102c178..0fbb9fcf9 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -53,6 +53,10 @@ class OnboardingRestEndpoint extends RestEndpoint { 'js_name' => 'isCasualSeller', 'sanitize' => 'to_boolean', ), + 'are_optional_payment_methods_enabled' => array( + 'js_name' => 'areOptionalPaymentMethodsEnabled', + 'sanitize' => 'to_boolean', + ), 'products' => array( 'js_name' => 'products', ),