Merge pull request #2826 from woocommerce/PCP-3890-implement-sandbox-login-via-onboarding-wizard

Sandbox login via onboarding wizard & refactoring (3897)
This commit is contained in:
Philipp Stracker 2024-11-22 15:15:44 +01:00 committed by GitHub
commit 54f3812f7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 2134 additions and 937 deletions

View file

@ -1,71 +1,116 @@
<?php <?php
if (!defined('PAYPAL_INTEGRATION_DATE')) { /**
define('PAYPAL_INTEGRATION_DATE', '2023-06-02'); * Stubs to help psalm correctly annotate problems in the plugin.
*
* @package WooCommerce
*/
if ( ! defined( 'PAYPAL_INTEGRATION_DATE' ) ) {
define( 'PAYPAL_INTEGRATION_DATE', '2023-06-02' );
} }
if (!defined('PAYPAL_URL')) { if ( ! defined( 'PAYPAL_URL' ) ) {
define( 'PAYPAL_URL', 'https://www.paypal.com' ); define( 'PAYPAL_URL', 'https://www.paypal.com' );
} }
if (!defined('PAYPAL_SANDBOX_URL')) { if ( ! defined( 'PAYPAL_SANDBOX_URL' ) ) {
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
} }
if (!defined('EP_PAGES')) { if ( ! defined( 'EP_PAGES' ) ) {
define('EP_PAGES', 4096); define( 'EP_PAGES', 4096 );
} }
if (!defined('MONTH_IN_SECONDS')) { if ( ! defined( 'MONTH_IN_SECONDS' ) ) {
define('MONTH_IN_SECONDS', 30 * DAY_IN_SECONDS); define( 'MONTH_IN_SECONDS', 30 * DAY_IN_SECONDS );
} }
if (!defined('HOUR_IN_SECONDS')) { if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
define('HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS); define( 'HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS );
} }
if (!defined('MINUTE_IN_SECONDS')) { if ( ! defined( 'MINUTE_IN_SECONDS' ) ) {
define( 'MINUTE_IN_SECONDS', 60 ); define( 'MINUTE_IN_SECONDS', 60 );
} }
if ( ! defined( 'ABSPATH' ) ) {
if (!defined('ABSPATH')) { define( 'ABSPATH', '' );
define('ABSPATH', '');
} }
if ( ! defined( 'PAYPAL_API_URL' ) ) {
if (!defined('PPCP_PAYPAL_BN_CODE')) { define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
define('PPCP_PAYPAL_BN_CODE', 'Woo_PPCP'); }
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. * Cancel the next occurrence of a scheduled action.
* *
* While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent * While only the next instance of a recurring or cron action is unscheduled by this method, that
* all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in * will also prevent all future instances of that recurring or cron action from being run.
* a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled * Recurring and cron actions are scheduled in a sequence instead of all being scheduled at once.
* only after the former action is run. If the next instance is never run, because it's unscheduled by this function, * Each successive occurrence of a recurring action is scheduled only after the former action is
* then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled * run. If the next instance is never run, because it's unscheduled by this function, then the
* by this method also. * following instance will never be scheduled (or exist), which is effectively the same as being
* unscheduled by this method also.
* *
* @param string $hook The hook that the job will trigger. * @param string $hook The hook that the job will trigger.
* @param array $args Args that would have been passed to the job. * @param array $args Args that would have been passed to the job.
* @param string $group The group the job is assigned to. * @param string $group The group the job is assigned to.
* *
* @return string|null The scheduled action ID if a scheduled action was found, or null if no matching action found. * @return string|null The scheduled action ID if a scheduled action was found, or null if no
* matching action found.
*/ */
function as_unschedule_action($hook, $args = array(), $group = '') {} function as_unschedule_action( $hook, $args = array(), $group = '' ) {
return null;
}
/** /**
* Schedule an action to run one time * Schedule an action to run one time
* *
* @param int $timestamp When the job will run. * @param int $timestamp When the job will run.
* @param string $hook The hook to trigger. * @param string $hook The hook to trigger.
* @param array $args Arguments to pass when the hook triggers. * @param array $args Arguments to pass when the hook triggers.
* @param string $group The group to assign this job to. * @param string $group The group to assign this job to.
* @param bool $unique Whether the action should be unique. * @param bool $unique Whether the action should be unique.
* *
* @return int The action ID. * @return int The action ID.
*/ */
function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) {} function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) {
return 0;
}
/** /**
* HTML API: WP_HTML_Tag_Processor class * HTML API: WP_HTML_Tag_Processor class
*/ */
// phpcs:disable
class WP_HTML_Tag_Processor { class WP_HTML_Tag_Processor {
public function __construct( $html ) {} public function __construct( $html ) {
public function next_tag( $query = null ) {} }
public function set_attribute( $name, $value ) {}
public function get_updated_html() {} public function next_tag( $query = null ) : bool {
return false;
}
public function set_attribute( $name, $value ) : bool {
return false;
}
public function get_updated_html() : string {
return '';
}
} }

View file

@ -15,7 +15,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CardAuthenticationResult;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter; use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; 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\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
return array( return array(
'api.host' => function( ContainerInterface $container ) : string { 'api.host' => function( ContainerInterface $container ) : string {
@ -179,6 +179,22 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $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 { 'api.endpoint.identity-token' => static function ( ContainerInterface $container ) : IdentityToken {
$logger = $container->get( 'woocommerce.logger.woocommerce' ); $logger = $container->get( 'woocommerce.logger.woocommerce' );
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
@ -845,4 +861,22 @@ return array(
$container->get( 'api.client-credentials-cache' ) $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;
},
); );

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\ApiClient\Authentication * @package WooCommerce\PayPalCommerce\ApiClient\Authentication
*/ */
declare(strict_types=1); declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Authentication; namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
@ -28,7 +28,7 @@ class PayPalBearer implements Bearer {
/** /**
* The settings. * The settings.
* *
* @var ContainerInterface * @var ?ContainerInterface
*/ */
protected $settings; protected $settings;
@ -70,12 +70,12 @@ class PayPalBearer implements Bearer {
/** /**
* PayPalBearer constructor. * PayPalBearer constructor.
* *
* @param Cache $cache The cache. * @param Cache $cache The cache.
* @param string $host The host. * @param string $host The host.
* @param string $key The key. * @param string $key The key.
* @param string $secret The secret. * @param string $secret The secret.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param ContainerInterface $settings The settings. * @param ?ContainerInterface $settings The settings.
*/ */
public function __construct( public function __construct(
Cache $cache, Cache $cache,
@ -83,7 +83,7 @@ class PayPalBearer implements Bearer {
string $key, string $key,
string $secret, string $secret,
LoggerInterface $logger, LoggerInterface $logger,
ContainerInterface $settings ?ContainerInterface $settings
) { ) {
$this->cache = $cache; $this->cache = $cache;
@ -97,27 +97,62 @@ class PayPalBearer implements Bearer {
/** /**
* Returns a bearer token. * Returns a bearer token.
* *
* @return Token
* @throws RuntimeException When request fails. * @throws RuntimeException When request fails.
* @return Token
*/ */
public function bearer(): Token { public function bearer() : Token {
try { try {
$bearer = Token::from_json( (string) $this->cache->get( self::CACHE_KEY ) ); $bearer = Token::from_json( (string) $this->cache->get( self::CACHE_KEY ) );
return ( $bearer->is_valid() ) ? $bearer : $this->newBearer(); return ( $bearer->is_valid() ) ? $bearer : $this->newBearer();
} catch ( RuntimeException $error ) { } catch ( RuntimeException $error ) {
return $this->newBearer(); return $this->newBearer();
} }
} }
/**
* Retrieves the client key for authentication.
*
* @return string The client ID from settings, or the key defined via constructor.
*/
private function get_key() : string {
if (
$this->settings
&& $this->settings->has( 'client_id' )
&& $this->settings->get( 'client_id' )
) {
return $this->settings->get( 'client_id' );
}
return $this->key;
}
/**
* Retrieves the client secret for authentication.
*
* @return string The client secret from settings, or the value defined via constructor.
*/
private function get_secret() : string {
if (
$this->settings
&& $this->settings->has( 'client_secret' )
&& $this->settings->get( 'client_secret' )
) {
return $this->settings->get( 'client_secret' );
}
return $this->secret;
}
/** /**
* Creates a new bearer token. * Creates a new bearer token.
* *
* @return Token
* @throws RuntimeException When request fails. * @throws RuntimeException When request fails.
* @return Token
*/ */
private function newBearer(): Token { private function newBearer() : Token {
$key = $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) ? $this->settings->get( 'client_id' ) : $this->key; $key = $this->get_key();
$secret = $this->settings->has( 'client_secret' ) && $this->settings->get( 'client_secret' ) ? $this->settings->get( 'client_secret' ) : $this->secret; $secret = $this->get_secret();
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials'; $url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials';
$args = array( $args = array(
@ -127,10 +162,7 @@ class PayPalBearer implements Bearer {
'Authorization' => 'Basic ' . base64_encode( $key . ':' . $secret ), 'Authorization' => 'Basic ' . base64_encode( $key . ':' . $secret ),
), ),
); );
$response = $this->request( $response = $this->request( $url, $args );
$url,
$args
);
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
$error = new RuntimeException( $error = new RuntimeException(
@ -148,6 +180,7 @@ class PayPalBearer implements Bearer {
$token = Token::from_json( $response['body'] ); $token = Token::from_json( $response['body'] );
$this->cache->set( self::CACHE_KEY, $token->as_json() ); $this->cache->set( self::CACHE_KEY, $token->as_json() );
return $token; return $token;
} }
} }

View file

@ -85,8 +85,7 @@ class PartnerReferrals {
$error = new RuntimeException( $error = new RuntimeException(
__( 'Could not create referral.', 'woocommerce-paypal-payments' ) __( 'Could not create referral.', 'woocommerce-paypal-payments' )
); );
$this->logger->log( $this->logger->warning(
'warning',
$error->getMessage(), $error->getMessage(),
array( array(
'args' => $args, 'args' => $args,
@ -95,6 +94,7 @@ class PartnerReferrals {
); );
throw $error; throw $error;
} }
$json = json_decode( $response['body'] ); $json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response ); $status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) { if ( 201 !== $status_code ) {
@ -102,8 +102,7 @@ class PartnerReferrals {
$json, $json,
$status_code $status_code
); );
$this->logger->log( $this->logger->warning(
'warning',
$error->getMessage(), $error->getMessage(),
array( array(
'args' => $args, 'args' => $args,
@ -122,8 +121,7 @@ class PartnerReferrals {
$error = new RuntimeException( $error = new RuntimeException(
__( 'Action URL not found.', 'woocommerce-paypal-payments' ) __( 'Action URL not found.', 'woocommerce-paypal-payments' )
); );
$this->logger->log( $this->logger->warning(
'warning',
$error->getMessage(), $error->getMessage(),
array( array(
'args' => $args, 'args' => $args,

View file

@ -15,7 +15,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
@ -25,7 +24,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController;
return array( return array(
'api.sandbox-host' => static function ( ContainerInterface $container ): string { 'api.sandbox-host' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
@ -39,7 +38,7 @@ return array(
} }
return CONNECT_WOO_SANDBOX_URL; 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' ); $state = $container->get( 'onboarding.state' );
@ -54,7 +53,7 @@ return array(
} }
return CONNECT_WOO_URL; return CONNECT_WOO_URL;
}, },
'api.host' => static function ( ContainerInterface $container ): string { 'api.host' => static function ( ContainerInterface $container ): string {
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'onboarding.environment' );
/** /**
@ -66,25 +65,7 @@ return array(
? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' ); ? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' );
}, },
'api.paypal-host-production' => static function( ContainerInterface $container ) : string { 'api.paypal-host' => 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 {
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'onboarding.environment' );
/** /**
* The current environment. * The current environment.
@ -97,7 +78,7 @@ return array(
return $container->get( 'api.paypal-host-production' ); 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' ); $environment = $container->get( 'onboarding.environment' );
assert( $environment instanceof Environment ); assert( $environment instanceof Environment );
if ( $environment->current_environment_is( Environment::SANDBOX ) ) { 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' ); $state = $container->get( 'onboarding.state' );
@ -134,16 +115,16 @@ return array(
$settings $settings
); );
}, },
'onboarding.state' => function( ContainerInterface $container ) : State { 'onboarding.state' => function( ContainerInterface $container ) : State {
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
return new State( $settings ); return new State( $settings );
}, },
'onboarding.environment' => function( ContainerInterface $container ) : Environment { 'onboarding.environment' => function( ContainerInterface $container ) : Environment {
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
return new Environment( $settings ); return new Environment( $settings );
}, },
'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
$login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' ); $login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' );
return new OnboardingAssets( 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( return plugins_url(
'/modules/ppcp-onboarding/', '/modules/ppcp-onboarding/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' 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' ); $logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSeller( 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' ); $logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSeller( 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' ); $request_data = $container->get( 'button.request-data' );
$login_seller_production = $container->get( 'api.endpoint.login-seller-production' ); $login_seller_production = $container->get( 'api.endpoint.login-seller-production' );
@ -203,7 +184,7 @@ return array(
new Cache( 'ppcp-client-credentials-cache' ) 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( return new UpdateSignupLinksEndpoint(
$container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.settings' ),
$container->get( 'button.request-data' ), $container->get( 'button.request-data' ),
@ -213,26 +194,10 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals { 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache {
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 {
return new Cache( 'ppcp-paypal-signup-link' ); 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( return array(
'production-ppcp', 'production-ppcp',
'production-express_checkout', 'production-express_checkout',
@ -240,12 +205,12 @@ return array(
'sandbox-express_checkout', 'sandbox-express_checkout',
); );
}, },
'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) {
return new OnboardingSendOnlyNoticeRenderer( return new OnboardingSendOnlyNoticeRenderer(
$container->get( 'wcgateway.send-only-message' ) $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 = $container->get( 'api.endpoint.partner-referrals-production' );
$partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' ); $partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' ); $partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
@ -261,14 +226,14 @@ return array(
$logger $logger
); );
}, },
'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer {
return new OnboardingOptionsRenderer( return new OnboardingOptionsRenderer(
$container->get( 'onboarding.url' ), $container->get( 'onboarding.url' ),
$container->get( 'api.shop.country' ), $container->get( 'api.shop.country' ),
$container->get( 'wcgateway.settings' ) $container->get( 'wcgateway.settings' )
); );
}, },
'onboarding.rest' => static function( $container ) : OnboardingRESTController { 'onboarding.rest' => static function( $container ) : OnboardingRESTController {
return new OnboardingRESTController( $container ); return new OnboardingRESTController( $container );
}, },
); );

View file

@ -2,13 +2,22 @@
display: flex; display: flex;
align-items: center; align-items: center;
&__space,
&__line { &__line {
height: 1px; margin: 0;
background-color: $color-gray-600;
display: block; display: block;
width: 100%; width: 100%;
} }
&__line {
background-color: $color-gray-600;
height: 1px;
}
&__space {
margin-bottom: 48px;
}
&__text { &__text {
color: $color-gray; color: $color-gray;
@include font(12, 24, 500, 0.8px); @include font(12, 24, 500, 0.8px);

View file

@ -1,7 +1,8 @@
body:has(.ppcp-r-container--onboarding) { body:has(.ppcp-r-container--onboarding) {
background-color: #fff !important; 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; display: none !important;
visibility: hidden;
} }
} }

View file

@ -20,14 +20,6 @@
margin: 0 0 16px 0; margin: 0 0 16px 0;
} }
.ppcp-r-page-welcome-mode-separator {
margin: 0 0 48px 0;
.ppcp-r-separator__line {
background-color: $color-gray-300;
}
}
.components-base-control__field { .components-base-control__field {
margin: 0 0 24px 0; margin: 0 0 24px 0;
} }

View file

@ -1,3 +1,4 @@
import { useEffect } from '@wordpress/element';
import { Icon } from '@wordpress/components'; import { Icon } from '@wordpress/components';
import { chevronDown, chevronUp } from '@wordpress/icons'; import { chevronDown, chevronUp } from '@wordpress/icons';
@ -5,11 +6,33 @@ import { useState } from 'react';
const Accordion = ( { const Accordion = ( {
title, title,
initiallyOpen = false, initiallyOpen = null,
className = '', className = '',
id = '',
children, 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 ) => { const toggleOpen = ( ev ) => {
setIsOpen( ! isOpen ); setIsOpen( ! isOpen );
@ -26,7 +49,7 @@ const Accordion = ( {
} }
return ( return (
<div className={ wrapperClasses.join( ' ' ) }> <div className={ wrapperClasses.join( ' ' ) } id={ id }>
<button <button
onClick={ toggleOpen } onClick={ toggleOpen }
className="ppcp-r-accordion--title" className="ppcp-r-accordion--title"

View file

@ -1,24 +1,32 @@
const Separator = ( props ) => { const Separator = ( { className = '', text = '', withLine = true } ) => {
let separatorClass = 'ppcp-r-separator'; const separatorClass = [ 'ppcp-r-separator' ];
const innerClass = withLine
? 'ppcp-r-separator__line'
: 'ppcp-r-separator__space';
if ( props?.className ) { if ( className ) {
separatorClass += ' ' + props.className; separatorClass.push( className );
} }
if ( props.text ) { const getClass = ( type ) => `${ innerClass } ${ innerClass }--${ type }`;
return (
<div className={ separatorClass }>
<span className="ppcp-r-separator__line ppcp-r-separator__line--before"></span>
<span className="ppcp-r-separator__text">{ props.text }</span> const renderSeparator = () => {
<span className="ppcp-r-separator__line ppcp-r-separator__line--after"></span> if ( text ) {
</div> return (
); <>
} <span className={ getClass( 'before' ) }></span>
<span className="ppcp-r-separator__text">{ text }</span>
<span className={ getClass( 'after' ) }></span>
</>
);
}
return <span className={ getClass( 'full' ) }></span>;
};
return ( return (
<div className={ separatorClass }> <div className={ separatorClass.join( ' ' ) }>
<span className="ppcp-r-separator__line ppcp-r-separator__line--before"></span> { renderSeparator() }
</div> </div>
); );
}; };

View file

@ -7,24 +7,25 @@ import { store as noticesStore } from '@wordpress/notices';
import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock'; import SettingsToggleBlock from '../../../ReusableComponents/SettingsToggleBlock';
import Separator from '../../../ReusableComponents/Separator'; import Separator from '../../../ReusableComponents/Separator';
import DataStoreControl from '../../../ReusableComponents/DataStoreControl'; import DataStoreControl from '../../../ReusableComponents/DataStoreControl';
import { useManualConnect, useOnboardingStepWelcome } from '../../../../data'; import { CommonHooks } from '../../../../data';
import { openPopup } from '../../../../utils/window';
const AdvancedOptionsForm = ( { setCompleted } ) => { const AdvancedOptionsForm = ( { setCompleted } ) => {
const { isBusy } = CommonHooks.useBusyState();
const { isSandboxMode, setSandboxMode, connectViaSandbox } =
CommonHooks.useSandbox();
const { const {
isManualConnectionBusy,
isSandboxMode,
setSandboxMode,
isManualConnectionMode, isManualConnectionMode,
setManualConnectionMode, setManualConnectionMode,
clientId, clientId,
setClientId, setClientId,
clientSecret, clientSecret,
setClientSecret, setClientSecret,
} = useOnboardingStepWelcome(); connectViaIdAndSecret,
} = CommonHooks.useManualConnection();
const { createSuccessNotice, createErrorNotice } = const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore ); useDispatch( noticesStore );
const { connectManual } = useManualConnect();
const refClientId = useRef( null ); const refClientId = useRef( null );
const refClientSecret = useRef( null ); const refClientSecret = useRef( null );
@ -61,17 +62,9 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
return true; return true;
}; };
const handleServerError = ( res ) => { const handleServerError = ( res, genericMessage ) => {
if ( res.message ) { console.error( 'Connection error', res );
createErrorNotice( res.message ); createErrorNotice( res?.message ?? genericMessage );
} else {
createErrorNotice(
__(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments'
)
);
}
}; };
const handleServerSuccess = () => { const handleServerSuccess = () => {
@ -82,17 +75,50 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
setCompleted( true ); setCompleted( true );
}; };
const handleConnect = async () => { 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 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 handleManualConnect = async () => {
if ( ! handleFormValidation() ) { if ( ! handleFormValidation() ) {
return; return;
} }
const res = await connectManual(); const res = await connectViaIdAndSecret();
if ( res.success ) { if ( res.success ) {
handleServerSuccess(); handleServerSuccess();
} else { } else {
handleServerError( res ); handleServerError(
res,
__(
'Could not connect to PayPal. Please make sure your Client ID and Secret Key are correct.',
'woocommerce-paypal-payments'
)
);
} }
}; };
@ -118,21 +144,22 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
) } ) }
isToggled={ !! isSandboxMode } isToggled={ !! isSandboxMode }
setToggled={ setSandboxMode } setToggled={ setSandboxMode }
isLoading={ isBusy }
> >
<Button variant="secondary"> <Button onClick={ handleSandboxConnect } variant="secondary">
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) } { __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button> </Button>
</SettingsToggleBlock> </SettingsToggleBlock>
<Separator className="ppcp-r-page-welcome-mode-separator" /> <Separator withLine={ false } />
<SettingsToggleBlock <SettingsToggleBlock
label={ __( label={
'Manually Connect', __( 'Manually Connect', 'woocommerce-paypal-payments' ) +
'woocommerce-paypal-payments' ( isBusy ? ' ...' : '' )
) } }
description={ advancedUsersDescription } description={ advancedUsersDescription }
isToggled={ !! isManualConnectionMode } isToggled={ !! isManualConnectionMode }
setToggled={ setManualConnectionMode } setToggled={ setManualConnectionMode }
isLoading={ isManualConnectionBusy } isLoading={ isBusy }
> >
<DataStoreControl <DataStoreControl
control={ TextControl } control={ TextControl }
@ -169,7 +196,7 @@ const AdvancedOptionsForm = ( { setCompleted } ) => {
onChange={ setClientSecret } onChange={ setClientSecret }
type="password" type="password"
/> />
<Button variant="secondary" onClick={ handleConnect }> <Button variant="secondary" onClick={ handleManualConnect }>
{ __( 'Connect Account', 'woocommerce-paypal-payments' ) } { __( 'Connect Account', 'woocommerce-paypal-payments' ) }
</Button> </Button>
</SettingsToggleBlock> </SettingsToggleBlock>

View file

@ -1,10 +1,8 @@
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import {
useOnboardingStepBusiness, import { OnboardingHooks } from '../../../../data';
useOnboardingStepProducts, import data from '../../../../utils/data';
} from '../../data';
import data from '../../utils/data';
const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => {
const isLastStep = () => currentStep + 1 === stepperOrder.length; const isLastStep = () => currentStep + 1 === stepperOrder.length;
@ -24,8 +22,8 @@ const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => {
} }
}; };
const { products, toggleProduct } = useOnboardingStepProducts(); const { products } = OnboardingHooks.useProducts();
const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); const { isCasualSeller } = OnboardingHooks.useBusiness();
let navigationTitle = ''; let navigationTitle = '';
let disabled = false; let disabled = false;

View file

@ -1,7 +1,7 @@
import Container from '../../ReusableComponents/Container'; import Container from '../../ReusableComponents/Container';
import { useOnboardingStep } from '../../../data'; import { OnboardingHooks } from '../../../data';
import { getSteps } from './availableSteps'; import { getSteps } from './availableSteps';
import Navigation from '../../ReusableComponents/Navigation'; import Navigation from './Components/Navigation';
const getCurrentStep = ( requestedStep, steps ) => { const getCurrentStep = ( requestedStep, steps ) => {
const isValidStep = ( step ) => const isValidStep = ( step ) =>
@ -15,7 +15,7 @@ const getCurrentStep = ( requestedStep, steps ) => {
}; };
const Onboarding = () => { const Onboarding = () => {
const { step, setStep, setCompleted, flags } = useOnboardingStep(); const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps();
const steps = getSteps( flags ); const steps = getSteps( flags );
const CurrentStepComponent = getCurrentStep( step, steps ); const CurrentStepComponent = getCurrentStep( step, steps );

View file

@ -1,20 +1,14 @@
import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBox from '../../ReusableComponents/SelectBox';
import { __ } from '@wordpress/i18n'; import { OnboardingHooks, BUSINESS_TYPES } from '../../../data';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import { useOnboardingStepBusiness } from '../../../data';
import { BUSINESS_TYPES } from '../../../data/constants';
const BUSINESS_RADIO_GROUP_NAME = 'business'; const BUSINESS_RADIO_GROUP_NAME = 'business';
const StepBusiness = ( { const StepBusiness = ( {} ) => {
setStep, const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness();
currentStep,
stepperOrder,
setCompleted,
} ) => {
const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness();
const handleSellerTypeChange = ( value ) => { const handleSellerTypeChange = ( value ) => {
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value ); setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value );
@ -40,23 +34,22 @@ const StepBusiness = ( {
/> />
<div className="ppcp-r-inner-container"> <div className="ppcp-r-inner-container">
<SelectBoxWrapper> <SelectBoxWrapper>
<SelectBox <SelectBox
title={ __( title={ __(
'Business', 'Business',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
description={ __( description={ __(
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.', 'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
name={ BUSINESS_RADIO_GROUP_NAME } name={ BUSINESS_RADIO_GROUP_NAME }
value={ BUSINESS_TYPES.BUSINESS } value={ BUSINESS_TYPES.BUSINESS }
changeCallback={ handleSellerTypeChange } changeCallback={ handleSellerTypeChange }
currentValue={ getCurrentValue() } currentValue={ getCurrentValue() }
checked={ isCasualSeller === false } checked={ isCasualSeller === false }
type="radio" type="radio"
> ></SelectBox>
</SelectBox>
<SelectBox <SelectBox
title={ __( title={ __(
'Personal Account', 'Personal Account',
@ -72,8 +65,7 @@ const StepBusiness = ( {
currentValue={ getCurrentValue() } currentValue={ getCurrentValue() }
checked={ isCasualSeller === true } checked={ isCasualSeller === true }
type="radio" type="radio"
> ></SelectBox>
</SelectBox>
</SelectBoxWrapper> </SelectBoxWrapper>
</div> </div>
</div> </div>

View file

@ -1,13 +1,9 @@
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Button, Icon } from '@wordpress/components'; import { Button, Icon } from '@wordpress/components';
const StepCompleteSetup = ( { import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
setStep,
currentStep, const StepCompleteSetup = ( { setCompleted } ) => {
stepperOrder,
setCompleted,
} ) => {
const ButtonIcon = () => ( const ButtonIcon = () => (
<Icon <Icon
icon={ () => ( icon={ () => (

View file

@ -1,19 +1,14 @@
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBox from '../../ReusableComponents/SelectBox';
import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper';
import { useOnboardingStepProducts } from '../../../data'; import { OnboardingHooks, PRODUCT_TYPES } from '../../../data';
import { PRODUCT_TYPES } from '../../../data/constants';
const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; const PRODUCTS_CHECKBOX_GROUP_NAME = 'products';
const StepProducts = ( { const StepProducts = () => {
setStep, const { products, setProducts } = OnboardingHooks.useProducts();
currentStep,
stepperOrder,
setCompleted,
} ) => {
const { products, toggleProduct } = useOnboardingStepProducts();
return ( return (
<div className="ppcp-r-page-products"> <div className="ppcp-r-page-products">
@ -33,7 +28,7 @@ const StepProducts = ( {
) } ) }
name={ PRODUCTS_CHECKBOX_GROUP_NAME } name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PRODUCT_TYPES.VIRTUAL } value={ PRODUCT_TYPES.VIRTUAL }
changeCallback={ toggleProduct } changeCallback={ setProducts }
currentValue={ products } currentValue={ products }
type="checkbox" type="checkbox"
> >
@ -75,7 +70,7 @@ const StepProducts = ( {
) } ) }
name={ PRODUCTS_CHECKBOX_GROUP_NAME } name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PRODUCT_TYPES.PHYSICAL } value={ PRODUCT_TYPES.PHYSICAL }
changeCallback={ toggleProduct } changeCallback={ setProducts }
currentValue={ products } currentValue={ products }
type="checkbox" type="checkbox"
> >
@ -102,7 +97,7 @@ const StepProducts = ( {
) } ) }
name={ PRODUCTS_CHECKBOX_GROUP_NAME } name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PRODUCT_TYPES.SUBSCRIPTIONS } value={ PRODUCT_TYPES.SUBSCRIPTIONS }
changeCallback={ toggleProduct } changeCallback={ setProducts }
currentValue={ products } currentValue={ products }
type="checkbox" type="checkbox"
> >

View file

@ -1,13 +1,13 @@
import { __, sprintf } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import Separator from '../../ReusableComponents/Separator'; import Separator from '../../ReusableComponents/Separator';
import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs'; import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs';
import AccordionSection from '../../ReusableComponents/AccordionSection';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
import AccordionSection from '../../ReusableComponents/AccordionSection';
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
return ( return (
@ -57,7 +57,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
className="onboarding-advanced-options" className="onboarding-advanced-options"
initiallyOpen={ false } id="advanced-options"
> >
<AdvancedOptionsForm setCompleted={ setCompleted } /> <AdvancedOptionsForm setCompleted={ setCompleted } />
</AccordionSection> </AccordionSection>

View file

@ -1,9 +1,9 @@
import { useOnboardingStep } from '../../data'; import { OnboardingHooks } from '../../data';
import Onboarding from './Onboarding/Onboarding'; import Onboarding from './Onboarding/Onboarding';
import SettingsScreen from './SettingsScreen'; import SettingsScreen from './SettingsScreen';
const Settings = () => { const Settings = () => {
const onboardingProgress = useOnboardingStep(); const onboardingProgress = OnboardingHooks.useSteps();
if ( ! onboardingProgress.isReady ) { if ( ! onboardingProgress.isReady ) {
// TODO: Use better loading state indicator. // TODO: Use better loading state indicator.

View file

@ -0,0 +1,19 @@
/**
* Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data.
SET_TRANSIENT: 'COMMON:SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
HYDRATE: 'COMMON:HYDRATE',
// 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',
};

View file

@ -0,0 +1,154 @@
/**
* Action Creators: Define functions to create action objects.
*
* These functions update state or trigger side effects (e.g., async operations).
* 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.
* @property {string} type - The action type.
* @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.
*
* @param {boolean} isReady
* @return {Action} The action.
*/
export const setIsReady = ( isReady ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { isReady },
} );
/**
* Transient. Changes the "saving" flag.
*
* @param {boolean} isSaving
* @return {Action} The action.
*/
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 {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 },
} );
/**
* Side effect. Saves the persistent details to the WP database.
*
* @return {Action} The action.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
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.
*
* @return {Action} The action.
*/
export const connectViaIdAndSecret = function* () {
const { clientId, clientSecret, useSandbox } =
yield select( STORE_NAME ).persistentData();
yield setIsBusy( true );
const result = yield {
type: ACTION_TYPES.DO_MANUAL_CONNECTION,
clientId,
clientSecret,
useSandbox,
};
yield setIsBusy( false );
return result;
};

View file

@ -0,0 +1,46 @@
/**
* Name of the module-store in the main Redux store.
*
* Helps to isolate data, used by reducer and selectors.
*
* @type {string}
*/
export const STORE_NAME = 'wc/paypal/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 = '/wc/v3/wc_paypal/common';
/**
* REST path to persist data of this module to the WP DB.
*
* Used by controls.
*
* @type {string}
*/
export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
/**
* REST path to perform the manual connection check, using client ID and secret,
*
* Used by: Controls
* See: ConnectManualRestEndpoint.php
*
* @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';

View file

@ -0,0 +1,80 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import {
REST_PERSIST_PATH,
REST_MANUAL_CONNECTION_PATH,
REST_SANDBOX_CONNECTION_PATH,
} from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
try {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( error ) {
console.error( 'Error saving data.', error );
}
},
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,
useSandbox,
} ) {
let result = null;
try {
result = await apiFetch( {
path: REST_MANUAL_CONNECTION_PATH,
method: 'POST',
data: {
clientId,
clientSecret,
useSandbox,
},
} );
} catch ( e ) {
result = {
success: false,
error: e,
};
}
return result;
},
};

View file

@ -0,0 +1,111 @@
/**
* 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.
*
* @file
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
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 useHooks = () => {
const {
persist,
setSandboxMode,
setManualConnectionMode,
setClientId,
setClientSecret,
connectViaSandbox,
connectViaIdAndSecret,
} = useDispatch( STORE_NAME );
// Transient accessors.
const isReady = useTransient( 'isReady' );
// Persistent accessors.
const clientId = usePersistent( 'clientId' );
const clientSecret = usePersistent( 'clientSecret' );
const isSandboxMode = usePersistent( 'useSandbox' );
const isManualConnectionMode = usePersistent( 'useManualConnection' );
const savePersistent = async ( setter, value ) => {
setter( value );
await persist();
};
return {
isReady,
isSandboxMode,
setSandboxMode: ( state ) => {
return savePersistent( setSandboxMode, state );
},
isManualConnectionMode,
setManualConnectionMode: ( state ) => {
return savePersistent( setManualConnectionMode, state );
},
clientId,
setClientId: ( value ) => {
return savePersistent( setClientId, value );
},
clientSecret,
setClientSecret: ( value ) => {
return savePersistent( setClientSecret, value );
},
connectViaSandbox,
connectViaIdAndSecret,
};
};
export const useBusyState = () => {
const { setIsBusy } = useDispatch( STORE_NAME );
const isBusy = useTransient( 'isBusy' );
return {
isBusy,
setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ),
};
};
export const useSandbox = () => {
const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks();
return { isSandboxMode, setSandboxMode, connectViaSandbox };
};
export const useManualConnection = () => {
const {
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
} = useHooks();
return {
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
connectViaIdAndSecret,
};
};

View file

@ -0,0 +1,24 @@
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 hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
};
export { hooks, selectors, STORE_NAME };

View file

@ -0,0 +1,45 @@
/**
* Reducer: Defines store structure and state updates for this module.
*
* Manages both transient (temporary) and persistent (saved) state.
* 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,
isBusy: 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 ),
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
setPersistent( state, payload.data ),
} );
export default commonReducer;

View file

@ -0,0 +1,36 @@
/**
* 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 (selector with same name must exist).
* 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 { STORE_NAME, REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
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'
)
);
}
},
};

View file

@ -0,0 +1,21 @@
/**
* 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.
*
* @file
*/
const EMPTY_OBJ = Object.freeze( {} );
const getState = ( state ) => state || EMPTY_OBJ;
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
export const transientData = ( state ) => {
const { data, ...transientState } = getState( state );
return transientState || EMPTY_OBJ;
};

View file

@ -1,6 +1,3 @@
export const NAMESPACE = '/wc/v3/wc_paypal';
export const STORE_NAME = 'wc/paypal';
export const BUSINESS_TYPES = { export const BUSINESS_TYPES = {
CASUAL_SELLER: 'casual_seller', CASUAL_SELLER: 'casual_seller',
BUSINESS: 'business', BUSINESS: 'business',

View file

@ -0,0 +1,47 @@
import { OnboardingStoreName } from './index';
export const addDebugTools = ( context, modules ) => {
if ( ! context || ! context?.debug ) {
return;
}
context.dumpStore = async () => {
/* eslint-disable no-console */
if ( ! console?.groupCollapsed ) {
console.error( 'console.groupCollapsed is not supported.' );
return;
}
modules.forEach( ( module ) => {
const storeName = module.STORE_NAME;
const storeSelector = `wp.data.select( '${ storeName }' )`;
console.group( `[STORE] ${ storeSelector }` );
const dumpStore = ( selector ) => {
const contents = wp.data.select( storeName )[ selector ]();
console.groupCollapsed( `.${ selector }()` );
console.table( contents );
console.groupEnd();
};
Object.keys( module.selectors ).forEach( dumpStore );
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();
};
};

View file

@ -1,7 +1,16 @@
import { STORE_NAME } from './constants'; import { addDebugTools } from './debug';
import { initStore } from './store'; import * as Onboarding from './onboarding';
import * as Common from './common';
initStore(); Onboarding.initStore();
Common.initStore();
export const WC_PAYPAL_STORE_NAME = STORE_NAME; export const OnboardingHooks = Onboarding.hooks;
export * from './onboarding/hooks'; export const CommonHooks = Common.hooks;
export const OnboardingStoreName = Onboarding.STORE_NAME;
export const CommonStoreName = Common.STORE_NAME;
export * from './constants';
addDebugTools( window.ppcpSettings, [ Onboarding, Common ] );

View file

@ -1,19 +1,18 @@
export default { /**
RESET_ONBOARDING: 'RESET_ONBOARDING', * Action Types: Define unique identifiers for actions across all store modules.
*
* @file
*/
export default {
// Transient data. // Transient data.
SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY', SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT',
SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING',
SET_MANUAL_CONNECTION_BUSY: 'SET_MANUAL_CONNECTION_BUSY',
// Persistent data. // Persistent data.
SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED', SET_PERSISTENT: 'ONBOARDING:SET_PERSISTENT',
SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS', RESET: 'ONBOARDING:RESET',
SET_ONBOARDING_STEP: 'SET_ONBOARDING_STEP', HYDRATE: 'ONBOARDING:HYDRATE',
SET_SANDBOX_MODE: 'SET_SANDBOX_MODE',
SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE', // Controls - always start with "DO_".
SET_CLIENT_ID: 'SET_CLIENT_ID', DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA',
SET_CLIENT_SECRET: 'SET_CLIENT_SECRET',
SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER',
SET_PRODUCTS: 'SET_PRODUCTS',
}; };

View file

@ -1,235 +1,103 @@
/**
* Action Creators: Define functions to create action objects.
*
* These functions update state or trigger side effects (e.g., async operations).
* Actions are categorized as Transient, Persistent, or Side effect.
*
* @file
*/
import { select } from '@wordpress/data'; import { select } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls';
import ACTION_TYPES from './action-types'; import ACTION_TYPES from './action-types';
import { NAMESPACE, STORE_NAME } from '../constants'; import { 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. * Special. Resets all values in the onboarding store to initial defaults.
* *
* @return {{type: string}} The action. * @return {Action} The action.
*/ */
export const resetOnboarding = () => { export const reset = () => ( { type: ACTION_TYPES.RESET } );
return { type: ACTION_TYPES.RESET_ONBOARDING };
};
/**
* Non-persistent. Marks the onboarding details as "ready", i.e., fully initialized.
*
* @param {boolean} isReady
* @return {{type: string, isReady}} The action.
*/
export const setIsReady = ( isReady ) => {
return {
type: ACTION_TYPES.SET_ONBOARDING_IS_READY,
isReady,
};
};
/**
* Non-persistent. Changes the "saving" flag.
*
* @param {boolean} isSaving
* @return {{type: string, isSaving}} The action.
*/
export const setIsSaving = ( isSaving ) => {
return {
type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING,
isSaving,
};
};
/**
* Non-persistent. Changes the "manual connection is busy" flag.
*
* @param {boolean} isBusy
* @return {{type: string, isBusy}} The action.
*/
export const setManualConnectionIsBusy = ( isBusy ) => {
return {
type: ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY,
isBusy,
};
};
/** /**
* Persistent. Set the full onboarding details, usually during app initialization. * Persistent. Set the full onboarding details, usually during app initialization.
* *
* @param {{data: {}, flags?: {}}} payload * @param {{data: {}, flags?: {}}} payload
* @return {{type: string, payload}} The action. * @return {Action} The action.
*/ */
export const setOnboardingDetails = ( payload ) => { export const hydrate = ( payload ) => ( {
return { type: ACTION_TYPES.HYDRATE,
type: ACTION_TYPES.SET_ONBOARDING_DETAILS, 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. * Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
* *
* @param {boolean} completed * @param {boolean} completed
* @return {{type: string, payload}} The action. * @return {Action} The action.
*/ */
export const setCompleted = ( completed ) => { export const setCompleted = ( completed ) => ( {
return { type: ACTION_TYPES.SET_PERSISTENT,
type: ACTION_TYPES.SET_ONBOARDING_COMPLETED, payload: { completed },
completed, } );
};
};
/** /**
* Persistent. Sets the onboarding wizard to a new step. * Persistent. Sets the onboarding wizard to a new step.
* *
* @param {number} step * @param {number} step
* @return {{type: string, step}} An action. * @return {Action} The action.
*/ */
export const setOnboardingStep = ( step ) => { export const setStep = ( step ) => ( {
return { type: ACTION_TYPES.SET_PERSISTENT,
type: ACTION_TYPES.SET_ONBOARDING_STEP, payload: { step },
step, } );
};
};
/**
* Persistent. Sets the sandbox mode on or off.
*
* @param {boolean} sandboxMode
* @return {{type: string, useSandbox}} An action.
*/
export const setSandboxMode = ( sandboxMode ) => {
return {
type: ACTION_TYPES.SET_SANDBOX_MODE,
useSandbox: sandboxMode,
};
};
/**
* Persistent. Toggles the "Manual Connection" mode on or off.
*
* @param {boolean} manualConnectionMode
* @return {{type: string, useManualConnection}} An action.
*/
export const setManualConnectionMode = ( manualConnectionMode ) => {
return {
type: ACTION_TYPES.SET_MANUAL_CONNECTION_MODE,
useManualConnection: manualConnectionMode,
};
};
/**
* Persistent. Changes the "client ID" value.
*
* @param {string} clientId
* @return {{type: string, clientId}} The action.
*/
export const setClientId = ( clientId ) => {
return {
type: ACTION_TYPES.SET_CLIENT_ID,
clientId,
};
};
/**
* Persistent. Changes the "client secret" value.
*
* @param {string} clientSecret
* @return {{type: string, clientSecret}} The action.
*/
export const setClientSecret = ( clientSecret ) => {
return {
type: ACTION_TYPES.SET_CLIENT_SECRET,
clientSecret,
};
};
/** /**
* Persistent. Sets the "isCasualSeller" value. * Persistent. Sets the "isCasualSeller" value.
* *
* @param {boolean} isCasualSeller * @param {boolean} isCasualSeller
* @return {{type: string, isCasualSeller}} The action. * @return {Action} The action.
*/ */
export const setIsCasualSeller = ( isCasualSeller ) => { export const setIsCasualSeller = ( isCasualSeller ) => ( {
return { type: ACTION_TYPES.SET_PERSISTENT,
type: ACTION_TYPES.SET_IS_CASUAL_SELLER, payload: { isCasualSeller },
isCasualSeller, } );
};
};
/** /**
* Persistent. Sets the "products" array. * Persistent. Sets the "products" array.
* *
* @param {string[]} products * @param {string[]} products
* @return {{type: string, products}} The action. * @return {Action} The action.
*/ */
export const setProducts = ( products ) => { export const setProducts = ( products ) => ( {
return { type: ACTION_TYPES.SET_PERSISTENT,
type: ACTION_TYPES.SET_PRODUCTS, payload: { products },
products, } );
};
/**
* Side effect. Triggers the persistence of onboarding data to the server.
*
* @return {Action} The action.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
}; };
/**
* Attempts to establish a connection using client ID and secret via the server-side
* connection endpoint.
*
* @return {Object} The server response object
*/
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;
}
/**
* Saves the persistent details to the WP database.
*
* @return {boolean} True, if the values were successfully saved.
*/
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;
}

View file

@ -0,0 +1,28 @@
/**
* Name of the Redux store module.
*
* Used by: Reducer, Selector, Index
*
* @type {string}
*/
export const STORE_NAME = 'wc/paypal/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 = '/wc/v3/wc_paypal/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 = '/wc/v3/wc_paypal/onboarding';

View file

@ -0,0 +1,27 @@
/**
* Controls: Implement side effects, typically asynchronous operations.
*
* Controls use ACTION_TYPES keys as identifiers.
* They are triggered by corresponding actions and handle external interactions.
*
* @file
*/
import apiFetch from '@wordpress/api-fetch';
import { REST_PERSIST_PATH } from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
},
};

View file

@ -1,163 +1,90 @@
/**
* 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.
*
* @file
*/
import { useSelect, useDispatch } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import { NAMESPACE, PRODUCT_TYPES, STORE_NAME } from '../constants';
import { getFlags } from './selectors';
const useOnboardingDetails = () => { import { PRODUCT_TYPES } from '../constants';
const { import { STORE_NAME } from './constants';
persist,
setOnboardingStep,
setCompleted,
setSandboxMode,
setManualConnectionMode,
setClientId,
setClientSecret,
setIsCasualSeller,
setProducts,
} = useDispatch( STORE_NAME );
// Transient accessors. const useTransient = ( key ) =>
const isSaving = useSelect( ( select ) => { useSelect(
return select( STORE_NAME ).getTransientData().isSaving; ( select ) => select( STORE_NAME ).transientData()?.[ key ],
}, [] ); [ key ]
);
const isReady = useSelect( ( select ) => { const usePersistent = ( key ) =>
return select( STORE_NAME ).getTransientData().isReady; useSelect(
} ); ( select ) => select( STORE_NAME ).persistentData()?.[ key ],
[ key ]
);
const isManualConnectionBusy = useSelect( ( select ) => { const useHooks = () => {
return select( STORE_NAME ).getTransientData().isManualConnectionBusy; const { persist, setStep, setCompleted, setIsCasualSeller, setProducts } =
}, [] ); useDispatch( STORE_NAME );
// Read-only flags. // Read-only flags.
const flags = useSelect( ( select ) => { const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
return select( STORE_NAME ).getFlags();
} ); // Transient accessors.
const isReady = useTransient( 'isReady' );
// Persistent accessors. // Persistent accessors.
const step = useSelect( ( select ) => { const step = usePersistent( 'step' );
return select( STORE_NAME ).getPersistentData().step || 0; const completed = usePersistent( 'completed' );
} ); const isCasualSeller = usePersistent( 'isCasualSeller' );
const products = usePersistent( 'products' );
const completed = useSelect( ( select ) => { const savePersistent = async ( setter, value ) => {
return select( STORE_NAME ).getPersistentData().completed;
} );
const clientId = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().clientId;
}, [] );
const clientSecret = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().clientSecret;
}, [] );
const isSandboxMode = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().useSandbox;
}, [] );
const isManualConnectionMode = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().useManualConnection;
}, [] );
const isCasualSeller = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().isCasualSeller;
}, [] );
const products = useSelect( ( select ) => {
return select( STORE_NAME ).getPersistentData().products || [];
}, [] );
const toggleProduct = ( list ) => {
const validProducts = list.filter( ( item ) =>
Object.values( PRODUCT_TYPES ).includes( item )
);
return setDetailAndPersist( setProducts, validProducts );
};
const setDetailAndPersist = async ( setter, value ) => {
setter( value ); setter( value );
await persist(); await persist();
}; };
return { return {
isSaving,
isReady,
isManualConnectionBusy,
step,
setStep: ( value ) => setDetailAndPersist( setOnboardingStep, value ),
completed,
setCompleted: ( state ) => setDetailAndPersist( setCompleted, state ),
isSandboxMode,
setSandboxMode: ( state ) =>
setDetailAndPersist( setSandboxMode, state ),
isManualConnectionMode,
setManualConnectionMode: ( state ) =>
setDetailAndPersist( setManualConnectionMode, state ),
clientId,
setClientId: ( value ) => setDetailAndPersist( setClientId, value ),
clientSecret,
setClientSecret: ( value ) =>
setDetailAndPersist( setClientSecret, value ),
isCasualSeller,
setIsCasualSeller: ( value ) =>
setDetailAndPersist( setIsCasualSeller, value ),
products,
toggleProduct,
flags, flags,
isReady,
step,
setStep: ( value ) => {
return savePersistent( setStep, value );
},
completed,
setCompleted: ( state ) => {
return savePersistent( setCompleted, state );
},
isCasualSeller,
setIsCasualSeller: ( value ) => {
return savePersistent( setIsCasualSeller, value );
},
products,
setProducts: ( activeProducts ) => {
const validProducts = activeProducts.filter( ( item ) =>
Object.values( PRODUCT_TYPES ).includes( item )
);
return savePersistent( setProducts, validProducts );
},
}; };
}; };
export const useOnboardingStepWelcome = () => { export const useBusiness = () => {
const { const { isCasualSeller, setIsCasualSeller } = useHooks();
isSaving,
isManualConnectionBusy,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
} = useOnboardingDetails();
return {
isSaving,
isManualConnectionBusy,
isSandboxMode,
setSandboxMode,
isManualConnectionMode,
setManualConnectionMode,
clientId,
setClientId,
clientSecret,
setClientSecret,
};
};
export const useOnboardingStepBusiness = () => {
const { isCasualSeller, setIsCasualSeller } = useOnboardingDetails();
return { isCasualSeller, setIsCasualSeller }; return { isCasualSeller, setIsCasualSeller };
}; };
export const useOnboardingStepProducts = () => { export const useProducts = () => {
const { products, toggleProduct } = useOnboardingDetails(); const { products, setProducts } = useHooks();
return { products, toggleProduct }; return { products, setProducts };
}; };
export const useOnboardingStep = () => { export const useSteps = () => {
const { isReady, step, setStep, completed, setCompleted, flags } = const { flags, isReady, step, setStep, completed, setCompleted } =
useOnboardingDetails(); useHooks();
return { isReady, step, setStep, completed, setCompleted, flags }; return { flags, isReady, step, setStep, completed, setCompleted };
};
export const useManualConnect = () => {
const { connectViaIdAndSecret } = useDispatch( STORE_NAME );
return {
connectManual: connectViaIdAndSecret,
};
}; };

View file

@ -1,6 +1,24 @@
import { createReduxStore, register } from '@wordpress/data';
import { controls as wpControls } from '@wordpress/data-controls';
import { STORE_NAME } from './constants';
import reducer from './reducer'; import reducer from './reducer';
import * as selectors from './selectors'; import * as selectors from './selectors';
import * as actions from './actions'; 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 }; export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,
} );
register( store );
};
export { hooks, selectors, STORE_NAME };

View file

@ -1,21 +1,19 @@
/**
* Reducer: Defines store structure and state updates for this module.
*
* Manages both transient (temporary) and persistent (saved) state.
* 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'; import ACTION_TYPES from './action-types';
const defaultState = { // Store structure.
isReady: false,
isSaving: false,
isManualConnectionBusy: false,
// Data persisted to the server. const defaultTransient = {
data: { isReady: false,
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. // Read only values, provided by the server.
flags: { flags: {
@ -25,83 +23,40 @@ const defaultState = {
}, },
}; };
export const onboardingReducer = ( const defaultPersistent = {
state = defaultState, completed: false,
{ type, ...action } step: 0,
) => { isCasualSeller: null, // null value will uncheck both options in the UI.
const setTransient = ( changes ) => { products: [],
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:
return setPersistent( defaultState.data );
// Transient data.
case ACTION_TYPES.SET_ONBOARDING_IS_READY:
return setTransient( { isReady: action.isReady } );
case ACTION_TYPES.SET_IS_SAVING_ONBOARDING:
return setTransient( { isSaving: action.isSaving } );
case ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY:
return setTransient( { isManualConnectionBusy: action.isBusy } );
// 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;
}
}; };
// Reducer logic.
const [ setTransient, setPersistent ] = createSetters(
defaultTransient,
defaultPersistent
);
const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
setTransient( 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( state, payload.data );
// Flags are not updated by `setPersistent()`.
if ( payload.flags ) {
newState.flags = { ...newState.flags, ...payload.flags };
}
return newState;
},
} );
export default onboardingReducer; export default onboardingReducer;

View file

@ -1,25 +1,36 @@
/**
* 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 (selector with same name must exist).
* Resolvers are called automatically when selectors request unavailable data.
*
* @file
*/
import { dispatch } from '@wordpress/data'; import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls'; import { apiFetch } from '@wordpress/data-controls';
import { NAMESPACE } from '../constants';
import { setIsReady, setOnboardingDetails } from './actions';
/** import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
* Retrieve settings from the site's REST API.
*/
export function* getPersistentData() {
const path = `${ NAMESPACE }/onboarding`;
try { export const resolvers = {
const result = yield apiFetch( { path } ); /**
yield setOnboardingDetails( result ); * Retrieve settings from the site's REST API.
yield setIsReady( true ); */
} catch ( e ) { *persistentData() {
yield dispatch( 'core/notices' ).createErrorNotice( try {
__( const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
'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'
)
);
}
},
};

View file

@ -1,22 +1,25 @@
/**
* 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.
*
* @file
*/
const EMPTY_OBJ = Object.freeze( {} ); const EMPTY_OBJ = Object.freeze( {} );
const getOnboardingState = ( state ) => { const getState = ( state ) => state || EMPTY_OBJ;
if ( ! state ) {
return EMPTY_OBJ;
}
return state.onboarding || EMPTY_OBJ; export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
}; };
export const getPersistentData = ( state ) => { export const transientData = ( state ) => {
return getOnboardingState( state ).data || EMPTY_OBJ; const { data, flags, ...transientState } = getState( state );
};
export const getTransientData = ( state ) => {
const { data, flags, ...transientState } = getOnboardingState( state );
return transientState || EMPTY_OBJ; return transientState || EMPTY_OBJ;
}; };
export const getFlags = ( state ) => { export const flags = ( state ) => {
return getOnboardingState( state ).flags || EMPTY_OBJ; return getState( state ).flags || EMPTY_OBJ;
}; };

View file

@ -1,58 +0,0 @@
import { createReduxStore, register, combineReducers } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
import { STORE_NAME } from './constants';
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 } );
} );
const reducer = combineReducers( {
onboarding: onboarding.reducer,
} );
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls,
actions,
selectors,
resolvers,
} );
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 */
};

View file

@ -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.payload ?? {} );
}
return state;
};
};

View file

@ -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;
};

View file

@ -9,11 +9,16 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings; namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array( return array(
'settings.url' => static function ( ContainerInterface $container ) : string { 'settings.url' => static function ( ContainerInterface $container ) : string {
@ -44,9 +49,15 @@ return array(
$can_use_card_payments $can_use_card_payments
); );
}, },
'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings {
return new CommonSettings();
},
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
}, },
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
return new CommonRestEndpoint( $container->get( 'settings.data.common' ) );
},
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint { 'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint {
return new ConnectManualRestEndpoint( return new ConnectManualRestEndpoint(
$container->get( 'api.paypal-host-production' ), $container->get( 'api.paypal-host-production' ),
@ -54,6 +65,11 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $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 { 'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
return array( return array(
'AR', 'AR',
@ -110,6 +126,35 @@ return array(
return in_array( $country, $eligible_countries, true ); 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;
},
'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { 'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
return new SwitchSettingsUiEndpoint( return new SwitchSettingsUiEndpoint(
$container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'woocommerce.logger.woocommerce' ),

View file

@ -0,0 +1,119 @@
<?php
/**
* Common settings class
*
* @package WooCommerce\PayPalCommerce\Settings\Data
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data;
use RuntimeException;
/**
* Class CommonSettings
*
* This class serves as a container for managing the common settings that
* are used and managed in various areas of the settings UI
*
* Those settings mainly describe connection details and are initially collected
* in the onboarding wizard, and also appear in the settings screen.
*/
class CommonSettings extends AbstractDataModel {
/**
* Option key where profile details are stored.
*
* @var string
*/
protected const OPTION_KEY = 'woocommerce-ppcp-data-common';
/**
* Get default values for the model.
*
* @return array
*/
protected function get_defaults() : array {
return array(
'use_sandbox' => false,
'use_manual_connection' => false,
'client_id' => '',
'client_secret' => '',
);
}
// -----
/**
* Gets the 'use sandbox' setting.
*
* @return bool
*/
public function get_sandbox() : bool {
return (bool) $this->data['use_sandbox'];
}
/**
* Sets the 'use sandbox' setting.
*
* @param bool $use_sandbox Whether to use sandbox mode.
*/
public function set_sandbox( bool $use_sandbox ) : void {
$this->data['use_sandbox'] = $use_sandbox;
}
/**
* Gets the 'use manual connection' setting.
*
* @return bool
*/
public function get_manual_connection() : bool {
return (bool) $this->data['use_manual_connection'];
}
/**
* Sets the 'use manual connection' setting.
*
* @param bool $use_manual_connection Whether to use manual connection.
*/
public function set_manual_connection( bool $use_manual_connection ) : void {
$this->data['use_manual_connection'] = $use_manual_connection;
}
/**
* Gets the client ID.
*
* @return string
*/
public function get_client_id() : string {
return $this->data['client_id'];
}
/**
* Sets the client ID.
*
* @param string $client_id The client ID.
*/
public function set_client_id( string $client_id ) : void {
$this->data['client_id'] = sanitize_text_field( $client_id );
}
/**
* Gets the client secret.
*
* @return string
*/
public function get_client_secret() : string {
return $this->data['client_secret'];
}
/**
* Sets the client secret.
*
* @param string $client_secret The client secret.
*/
public function set_client_secret( string $client_secret ) : void {
$this->data['client_secret'] = sanitize_text_field( $client_secret );
}
}

View file

@ -64,14 +64,10 @@ class OnboardingProfile extends AbstractDataModel {
*/ */
protected function get_defaults() : array { protected function get_defaults() : array {
return array( return array(
'completed' => false, 'completed' => false,
'step' => 0, 'step' => 0,
'use_sandbox' => false, 'is_casual_seller' => null,
'use_manual_connection' => false, 'products' => array(),
'client_id' => '',
'client_secret' => '',
'is_casual_seller' => null,
'products' => array(),
); );
} }
@ -113,78 +109,6 @@ class OnboardingProfile extends AbstractDataModel {
$this->data['step'] = $step; $this->data['step'] = $step;
} }
/**
* Gets the 'use sandbox' setting.
*
* @return bool
*/
public function get_sandbox() : bool {
return (bool) $this->data['use_sandbox'];
}
/**
* Sets the 'use sandbox' setting.
*
* @param bool $use_sandbox Whether to use sandbox mode.
*/
public function set_sandbox( bool $use_sandbox ) : void {
$this->data['use_sandbox'] = $use_sandbox;
}
/**
* Gets the 'use manual connection' setting.
*
* @return bool
*/
public function get_manual_connection() : bool {
return (bool) $this->data['use_manual_connection'];
}
/**
* Sets the 'use manual connection' setting.
*
* @param bool $use_manual_connection Whether to use manual connection.
*/
public function set_manual_connection( bool $use_manual_connection ) : void {
$this->data['use_manual_connection'] = $use_manual_connection;
}
/**
* Gets the client ID.
*
* @return string
*/
public function get_client_id() : string {
return $this->data['client_id'];
}
/**
* Sets the client ID.
*
* @param string $client_id The client ID.
*/
public function set_client_id( string $client_id ) : void {
$this->data['client_id'] = sanitize_text_field( $client_id );
}
/**
* Gets the client secret.
*
* @return string
*/
public function get_client_secret() : string {
return $this->data['client_secret'];
}
/**
* Sets the client secret.
*
* @param string $client_secret The client secret.
*/
public function set_client_secret( string $client_secret ) : void {
$this->data['client_secret'] = sanitize_text_field( $client_secret );
}
/** /**
* Gets the casual seller flag. * Gets the casual seller flag.
* *

View file

@ -0,0 +1,133 @@
<?php
/**
* REST endpoint to manage the common module.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
/**
* REST controller for "common" settings, which are used and modified by
* multiple components. Those settings mainly define connection details.
*
* This API acts as the intermediary between the "external world" and our
* internal data model.
*/
class CommonRestEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected string $rest_base = 'common';
/**
* The settings instance.
*
* @var CommonSettings
*/
protected CommonSettings $settings;
/**
* Field mapping for request to profile transformation.
*
* @var array
*/
private array $field_map = array(
'use_sandbox' => array(
'js_name' => 'useSandbox',
'sanitize' => 'to_boolean',
),
'use_manual_connection' => array(
'js_name' => 'useManualConnection',
'sanitize' => 'to_boolean',
),
'client_id' => array(
'js_name' => 'clientId',
'sanitize' => 'sanitize_text_field',
),
'client_secret' => array(
'js_name' => 'clientSecret',
'sanitize' => 'sanitize_text_field',
),
);
/**
* Constructor.
*
* @param CommonSettings $settings The settings instance.
*/
public function __construct( CommonSettings $settings ) {
$this->settings = $settings;
}
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_details' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_details' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Returns all common details from the DB.
*
* @return WP_REST_Response The common settings.
*/
public function get_details() : WP_REST_Response {
$js_data = $this->sanitize_for_javascript(
$this->settings->to_array(),
$this->field_map
);
return $this->return_success( $js_data );
}
/**
* Updates common details based on the request.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_REST_Response The new common settings.
*/
public function update_details( WP_REST_Request $request ) : WP_REST_Response {
$wp_data = $this->sanitize_for_wordpress(
$request->get_params(),
$this->field_map
);
$this->settings->from_array( $wp_data );
$this->settings->save();
return $this->get_details();
}
}

View file

@ -78,9 +78,9 @@ class ConnectManualRestEndpoint extends RestEndpoint {
/** /**
* ConnectManualRestEndpoint constructor. * ConnectManualRestEndpoint constructor.
* *
* @param string $live_host The API host for the live mode. * @param string $live_host The API host for the live mode.
* @param string $sandbox_host The API host for the sandbox mode. * @param string $sandbox_host The API host for the sandbox mode.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
string $live_host, string $live_host,
@ -126,47 +126,38 @@ class ConnectManualRestEndpoint extends RestEndpoint {
$use_sandbox = (bool) ( $data['use_sandbox'] ?? false ); $use_sandbox = (bool) ( $data['use_sandbox'] ?? false );
if ( empty( $client_id ) || empty( $client_secret ) ) { if ( empty( $client_id ) || empty( $client_secret ) ) {
return rest_ensure_response( return $this->return_error( 'No client ID or secret provided.' );
array(
'success' => false,
'message' => 'No client ID or secret provided.',
)
);
} }
try { try {
$payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox );
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
return rest_ensure_response( return $this->return_error( $exception->getMessage() );
array(
'success' => false,
'message' => $exception->getMessage(),
)
);
} }
$result = array( return $this->return_success(
'merchantId' => $payee->merchant_id, array(
'email' => $payee->email_address, 'merchantId' => $payee->merchant_id,
'success' => true, 'email' => $payee->email_address,
'success' => true,
)
); );
return rest_ensure_response( $result );
} }
/** /**
* Retrieves the payee object with the merchant data * Retrieves the payee object with the merchant data
* by creating a minimal PayPal order. * by creating a minimal PayPal order.
* *
* @param string $client_id The client ID.
* @param string $client_secret The client secret.
* @param bool $use_sandbox Whether to use the sandbox mode.
* @return stdClass The payee object.
* @throws Exception When failed to retrieve payee. * @throws Exception When failed to retrieve payee.
* *
* phpcs:disable Squiz.Commenting * phpcs:disable Squiz.Commenting
* phpcs:disable Generic.Commenting * phpcs:disable Generic.Commenting
*
* @param string $client_secret The client secret.
* @param bool $use_sandbox Whether to use the sandbox mode.
* @param string $client_id The client ID.
*
* @return stdClass The payee object.
*/ */
private function request_payee( private function request_payee(
string $client_id, string $client_id,
@ -176,24 +167,13 @@ class ConnectManualRestEndpoint extends RestEndpoint {
$host = $use_sandbox ? $this->sandbox_host : $this->live_host; $host = $use_sandbox ? $this->sandbox_host : $this->live_host;
$empty_settings = new class() implements ContainerInterface
{
public function get( string $id ) {
throw new NotFoundException();
}
public function has( string $id ) {
return false;
}
};
$bearer = new PayPalBearer( $bearer = new PayPalBearer(
new InMemoryCache(), new InMemoryCache(),
$host, $host,
$client_id, $client_id,
$client_secret, $client_secret,
$this->logger, $this->logger,
$empty_settings null
); );
$orders = new Orders( $orders = new Orders(

View file

@ -0,0 +1,105 @@
<?php
/**
* REST endpoint to manage the onboarding module.
*
* @package WooCommerce\PayPalCommerce\Settings\Endpoint
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
/**
* REST controller that generates merchant login URLs.
*/
class LoginLinkRestEndpoint extends RestEndpoint {
/**
* The base path for this REST controller.
*
* @var string
*/
protected string $rest_base = 'login_link';
/**
* Link generator list, with environment name as array key.
*
* @var ConnectionUrlGenerator[]
*/
protected array $url_generators;
/**
* Constructor.
*
* @param ConnectionUrlGenerator[] $url_generators Array of environment-specific URL generators.
*/
public function __construct( array $url_generators ) {
$this->url_generators = $url_generators;
}
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'get_login_url' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => array(
'environment' => array(
'required' => true,
'type' => 'string',
),
'products' => array(
'required' => true,
'type' => 'array',
'items' => array(
'type' => 'string',
),
'sanitize_callback' => function ( $products ) {
return array_map( 'sanitize_text_field', $products );
},
),
),
),
)
);
}
/**
* 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 $this->return_success( $url );
} catch ( \Exception $e ) {
return $this->return_error( $e->getMessage() );
}
}
}

View file

@ -41,35 +41,19 @@ class OnboardingRestEndpoint extends RestEndpoint {
* @var array * @var array
*/ */
private array $field_map = array( private array $field_map = array(
'completed' => array( 'completed' => array(
'js_name' => 'completed', 'js_name' => 'completed',
'sanitize' => 'to_boolean', 'sanitize' => 'to_boolean',
), ),
'step' => array( 'step' => array(
'js_name' => 'step', 'js_name' => 'step',
'sanitize' => 'to_number', 'sanitize' => 'to_number',
), ),
'use_sandbox' => array( 'is_casual_seller' => array(
'js_name' => 'useSandbox',
'sanitize' => 'to_boolean',
),
'use_manual_connection' => array(
'js_name' => 'useManualConnection',
'sanitize' => 'to_boolean',
),
'client_id' => array(
'js_name' => 'clientId',
'sanitize' => 'sanitize_text_field',
),
'client_secret' => array(
'js_name' => 'clientSecret',
'sanitize' => 'sanitize_text_field',
),
'is_casual_seller' => array(
'js_name' => 'isCasualSeller', 'js_name' => 'isCasualSeller',
'sanitize' => 'to_boolean', 'sanitize' => 'to_boolean',
), ),
'products' => array( 'products' => array(
'js_name' => 'products', 'js_name' => 'products',
), ),
); );
@ -147,9 +131,9 @@ class OnboardingRestEndpoint extends RestEndpoint {
$this->flag_map $this->flag_map
); );
return rest_ensure_response( return $this->return_success(
$js_data,
array( array(
'data' => $js_data,
'flags' => $js_flags, 'flags' => $js_flags,
) )
); );

View file

@ -10,14 +10,12 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint; namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WC_REST_Controller; use WC_REST_Controller;
use WP_REST_Response;
/** /**
* Base class for REST controllers in the settings module. * 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. * Endpoint namespace.
* *
@ -34,6 +32,54 @@ class RestEndpoint extends WC_REST_Controller {
return current_user_can( 'manage_woocommerce' ); 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. * Sanitizes parameters based on a field mapping.
* *

View file

@ -15,6 +15,8 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/** /**
* Class SwitchSettingsUiEndpoint * Class SwitchSettingsUiEndpoint
*
* Note: This is an ajax handler, not a REST endpoint
*/ */
class SwitchSettingsUiEndpoint { class SwitchSettingsUiEndpoint {

View file

@ -0,0 +1,227 @@
<?php
/**
* Generator service to build URLs to sign in to a PayPal account.
*
* @package WooCommerce\PayPalCommerce\Settings\Service
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl;
use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
/**
* Generator that builds the ISU connection URL.
*/
class ConnectionUrlGenerator {
/**
* The partner referrals endpoint.
*
* @var PartnerReferrals
*/
protected PartnerReferrals $partner_referrals;
/**
* The default partner referrals data.
*
* @var PartnerReferralsData
*/
protected PartnerReferralsData $referrals_data;
/**
* The cache
*
* @var Cache
*/
protected Cache $cache;
/**
* Which environment is used for the connection URL.
*
* @var string
*/
protected string $environment = '';
/**
* The logger
*
* @var LoggerInterface
*/
private $logger;
/**
* Constructor for the ConnectionUrlGenerator class.
*
* Initializes the cache and logger properties of the class.
*
* @param PartnerReferrals $partner_referrals PartnerReferrals for URL generation.
* @param PartnerReferralsData $referrals_data Default partner referrals data.
* @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(
PartnerReferrals $partner_referrals,
PartnerReferralsData $referrals_data,
Cache $cache,
string $environment,
?LoggerInterface $logger = null
) {
$this->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 );
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
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 );
$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 string The cached URL, or an empty string if no URL is found.
*/
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 $onboarding_url->get();
}
} catch ( Exception $e ) {
// No problem, return an empty string to generate a new URL.
$this->logger->warning( 'Failed to load onboarding URL from cache: ' . $cache_key );
}
return '';
}
/**
* 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 {
$query_args = array( 'displayMode' => 'minibrowser' );
$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( $query_args, $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 ) : void {
$onboarding_url->set( $url );
$onboarding_url->persist();
}
}

View file

@ -9,8 +9,7 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings; namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -26,7 +25,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
/** /**
* Returns whether the old settings UI should be loaded. * Returns whether the old settings UI should be loaded.
*/ */
public static function should_use_the_old_ui(): bool { public static function should_use_the_old_ui() : bool {
return apply_filters( return apply_filters(
'woocommerce_paypal_payments_should_use_the_old_ui', 'woocommerce_paypal_payments_should_use_the_old_ui',
(bool) get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === true (bool) get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === true
@ -89,7 +88,13 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$endpoint = $container->get( 'settings.switch-ui.endpoint' ); $endpoint = $container->get( 'settings.switch-ui.endpoint' );
assert( $endpoint instanceof SwitchSettingsUiEndpoint ); 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; return true;
} }
@ -170,13 +175,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
add_action( add_action(
'rest_api_init', 'rest_api_init',
static function () use ( $container ) : void { static function () use ( $container ) : void {
$onboarding_endpoint = $container->get( 'settings.rest.onboarding' ); $endpoints = array(
assert( $onboarding_endpoint instanceof OnboardingRestEndpoint ); $container->get( 'settings.rest.onboarding' ),
$onboarding_endpoint->register_routes(); $container->get( 'settings.rest.common' ),
$container->get( 'settings.rest.connect_manual' ),
$container->get( 'settings.rest.login_link' ),
);
$connect_manual_endpoint = $container->get( 'settings.rest.connect_manual' ); foreach ( $endpoints as $endpoint ) {
assert( $connect_manual_endpoint instanceof ConnectManualRestEndpoint ); assert( $endpoint instanceof RestEndpoint );
$connect_manual_endpoint->register_routes(); $endpoint->register_routes();
}
} }
); );

View file

@ -2671,9 +2671,9 @@
mime "^3.0.0" mime "^3.0.0"
web-vitals "^4.2.1" web-vitals "^4.2.1"
"@wordpress/element@^6.1.0": "@wordpress/element@*", "@wordpress/element@^6.1.0":
version "6.11.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== integrity sha512-UvHFYkT+EEaXEyEfw+iqLHRO9OwBjjsUydEMHcqntzkNcsYeAbmaL9V8R9ikXHLe6ftdbkwoXgF85xVPhVsL+Q==
dependencies: dependencies:
"@babel/runtime" "7.25.7" "@babel/runtime" "7.25.7"
@ -2715,6 +2715,15 @@
globals "^13.12.0" globals "^13.12.0"
requireindex "^1.2.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@*": "@wordpress/jest-console@*":
version "8.11.0" version "8.11.0"
resolved "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.11.0.tgz" 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" resolved "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz"
integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw== 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": "@wordpress/scripts@~30.0.0":
version "30.0.6" version "30.0.6"
resolved "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.0.6.tgz" 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" kind-of "^6.0.2"
shallow-clone "^3.0.0" 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: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"