diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 7b1deb4b4..041455441 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -19,7 +19,7 @@ hooks: pre-start: - exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}" web_environment: - - WP_VERSION=6.3.3 + - WP_VERSION=6.7.1 - WP_LOCALE=en_US - WP_TITLE=WooCommerce PayPal Payments - WP_MULTISITE=true diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 59be5215c..56ba72451 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -1,71 +1,116 @@ function( ContainerInterface $container ) : string { @@ -179,6 +179,22 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals { + + return new PartnerReferrals( + CONNECT_WOO_SANDBOX_URL, + new ConnectBearer(), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'api.endpoint.partner-referrals-production' => static function ( ContainerInterface $container ) : PartnerReferrals { + + return new PartnerReferrals( + CONNECT_WOO_URL, + new ConnectBearer(), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.endpoint.identity-token' => static function ( ContainerInterface $container ) : IdentityToken { $logger = $container->get( 'woocommerce.logger.woocommerce' ); $settings = $container->get( 'wcgateway.settings' ); @@ -572,6 +588,7 @@ return array( 'CZK', 'DKK', 'EUR', + 'HKD', 'HUF', 'ILS', 'JPY', @@ -584,6 +601,7 @@ return array( 'PLN', 'GBP', 'RUB', + 'SGD', 'SEK', 'CHF', 'THB', @@ -595,27 +613,32 @@ return array( * The matrix which countries and currency combinations can be used for DCC. */ 'api.dcc-supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { - $default_currencies = array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'THB', - 'TWD', - 'USD', + $default_currencies = apply_filters( + 'woocommerce_paypal_payments_supported_currencies', + array( + 'AUD', + 'BRL', + 'CAD', + 'CHF', + 'CZK', + 'DKK', + 'EUR', + 'HKD', + 'GBP', + 'HUF', + 'ILS', + 'JPY', + 'MXN', + 'NOK', + 'NZD', + 'PHP', + 'PLN', + 'SGD', + 'SEK', + 'THB', + 'TWD', + 'USD', + ) ); /** @@ -659,14 +682,7 @@ return array( 'ES' => $default_currencies, 'SE' => $default_currencies, 'GB' => $default_currencies, - 'US' => array( - 'AUD', - 'CAD', - 'EUR', - 'GBP', - 'JPY', - 'USD', - ), + 'US' => $default_currencies, 'NO' => $default_currencies, ) ); @@ -845,4 +861,22 @@ return array( $container->get( 'api.client-credentials-cache' ) ); }, + 'api.paypal-host-production' => static function( ContainerInterface $container ) : string { + return PAYPAL_API_URL; + }, + 'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string { + return PAYPAL_SANDBOX_API_URL; + }, + 'api.paypal-website-url-production' => static function( ContainerInterface $container ) : string { + return PAYPAL_URL; + }, + 'api.paypal-website-url-sandbox' => static function( ContainerInterface $container ) : string { + return PAYPAL_SANDBOX_URL; + }, + 'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string { + return CONNECT_WOO_MERCHANT_ID; + }, + 'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string { + return CONNECT_WOO_SANDBOX_MERCHANT_ID; + }, ); diff --git a/modules/ppcp-api-client/src/Authentication/PayPalBearer.php b/modules/ppcp-api-client/src/Authentication/PayPalBearer.php index f5cab579b..7f176c218 100644 --- a/modules/ppcp-api-client/src/Authentication/PayPalBearer.php +++ b/modules/ppcp-api-client/src/Authentication/PayPalBearer.php @@ -5,7 +5,7 @@ * @package WooCommerce\PayPalCommerce\ApiClient\Authentication */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\ApiClient\Authentication; @@ -28,7 +28,7 @@ class PayPalBearer implements Bearer { /** * The settings. * - * @var ContainerInterface + * @var ?ContainerInterface */ protected $settings; @@ -70,12 +70,12 @@ class PayPalBearer implements Bearer { /** * PayPalBearer constructor. * - * @param Cache $cache The cache. - * @param string $host The host. - * @param string $key The key. - * @param string $secret The secret. - * @param LoggerInterface $logger The logger. - * @param ContainerInterface $settings The settings. + * @param Cache $cache The cache. + * @param string $host The host. + * @param string $key The key. + * @param string $secret The secret. + * @param LoggerInterface $logger The logger. + * @param ?ContainerInterface $settings The settings. */ public function __construct( Cache $cache, @@ -83,7 +83,7 @@ class PayPalBearer implements Bearer { string $key, string $secret, LoggerInterface $logger, - ContainerInterface $settings + ?ContainerInterface $settings ) { $this->cache = $cache; @@ -97,27 +97,62 @@ class PayPalBearer implements Bearer { /** * Returns a bearer token. * - * @return Token * @throws RuntimeException When request fails. + * @return Token */ - public function bearer(): Token { + public function bearer() : Token { try { $bearer = Token::from_json( (string) $this->cache->get( self::CACHE_KEY ) ); + return ( $bearer->is_valid() ) ? $bearer : $this->newBearer(); } catch ( RuntimeException $error ) { 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. * - * @return Token * @throws RuntimeException When request fails. + * @return Token */ - private function newBearer(): Token { - $key = $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) ? $this->settings->get( 'client_id' ) : $this->key; - $secret = $this->settings->has( 'client_secret' ) && $this->settings->get( 'client_secret' ) ? $this->settings->get( 'client_secret' ) : $this->secret; + private function newBearer() : Token { + $key = $this->get_key(); + $secret = $this->get_secret(); $url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials'; $args = array( @@ -127,10 +162,7 @@ class PayPalBearer implements Bearer { 'Authorization' => 'Basic ' . base64_encode( $key . ':' . $secret ), ), ); - $response = $this->request( - $url, - $args - ); + $response = $this->request( $url, $args ); if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { $error = new RuntimeException( @@ -148,6 +180,7 @@ class PayPalBearer implements Bearer { $token = Token::from_json( $response['body'] ); $this->cache->set( self::CACHE_KEY, $token->as_json() ); + return $token; } } diff --git a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php index 0f188f025..c7f5ec131 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnerReferrals.php @@ -85,8 +85,7 @@ class PartnerReferrals { $error = new RuntimeException( __( 'Could not create referral.', 'woocommerce-paypal-payments' ) ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, @@ -95,6 +94,7 @@ class PartnerReferrals { ); throw $error; } + $json = json_decode( $response['body'] ); $status_code = (int) wp_remote_retrieve_response_code( $response ); if ( 201 !== $status_code ) { @@ -102,8 +102,7 @@ class PartnerReferrals { $json, $status_code ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, @@ -122,8 +121,7 @@ class PartnerReferrals { $error = new RuntimeException( __( 'Action URL not found.', 'woocommerce-paypal-payments' ) ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php index 538ca224d..e2c2a91e8 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentMethodTokensEndpoint.php @@ -61,19 +61,24 @@ class PaymentMethodTokensEndpoint { * Creates a setup token. * * @param PaymentSource $payment_source The payment source. + * @param string $customer_id PayPal customer ID. * * @return stdClass * * @throws RuntimeException When something when wrong with the request. * @throws PayPalApiException When something when wrong setting up the token. */ - public function setup_tokens( PaymentSource $payment_source ): stdClass { + public function setup_tokens( PaymentSource $payment_source, string $customer_id = '' ): stdClass { $data = array( 'payment_source' => array( $payment_source->name() => $payment_source->properties(), ), ); + if ( $customer_id ) { + $data['customer']['id'] = $customer_id; + } + $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v3/vault/setup-tokens'; @@ -109,19 +114,24 @@ class PaymentMethodTokensEndpoint { * Creates a payment token for the given payment source. * * @param PaymentSource $payment_source The payment source. + * @param string $customer_id PayPal customer ID. * * @return stdClass * * @throws RuntimeException When something when wrong with the request. * @throws PayPalApiException When something when wrong setting up the token. */ - public function create_payment_token( PaymentSource $payment_source ): stdClass { + public function create_payment_token( PaymentSource $payment_source, string $customer_id = '' ): stdClass { $data = array( 'payment_source' => array( $payment_source->name() => $payment_source->properties(), ), ); + if ( $customer_id ) { + $data['customer']['id'] = $customer_id; + } + $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v3/vault/payment-tokens'; diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 7642401b1..84231f8d8 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -127,7 +127,7 @@ class PurchaseUnitFactory { $description = ''; $custom_id = (string) $order->get_id(); $invoice_id = $this->prefix . $order->get_order_number(); - $soft_descriptor = $this->soft_descriptor; + $soft_descriptor = $this->sanitize_soft_descriptor( $this->soft_descriptor ); $purchase_unit = new PurchaseUnit( $amount, @@ -197,7 +197,7 @@ class PurchaseUnitFactory { } } $invoice_id = ''; - $soft_descriptor = $this->soft_descriptor; + $soft_descriptor = $this->sanitize_soft_descriptor( $this->soft_descriptor ); $purchase_unit = new PurchaseUnit( $amount, $items, @@ -233,7 +233,7 @@ class PurchaseUnitFactory { $description = ( isset( $data->description ) ) ? $data->description : ''; $custom_id = ( isset( $data->custom_id ) ) ? $data->custom_id : ''; $invoice_id = ( isset( $data->invoice_id ) ) ? $data->invoice_id : ''; - $soft_descriptor = ( isset( $data->soft_descriptor ) ) ? $data->soft_descriptor : $this->soft_descriptor; + $soft_descriptor = $this->sanitize_soft_descriptor( $data->soft_descriptor ?? $this->soft_descriptor ); $items = array(); if ( isset( $data->items ) && is_array( $data->items ) ) { $items = array_map( @@ -316,4 +316,22 @@ class PurchaseUnitFactory { $purchase_unit->set_sanitizer( $this->sanitizer ); } } + + /** + * Sanitizes a soft descriptor, ensuring it is limited to 22 chars. + * + * The soft descriptor in the DB is escaped using `wp_kses_post()` which + * escapes certain characters via `wp_kses_normalize_entities()`. This + * helper method reverts those normalized entities back to UTF characters. + * + * @param string $soft_descriptor Soft descriptor to sanitize. + * + * @return string The sanitized soft descriptor. + */ + private function sanitize_soft_descriptor( string $soft_descriptor ) : string { + $decoded = html_entity_decode( $soft_descriptor, ENT_QUOTES, 'UTF-8' ); + $sanitized = preg_replace( '/[^a-zA-Z0-9 *\-.]/', '', $decoded ) ?: ''; + + return substr( $sanitized, 0, 22 ) ?: ''; + } } diff --git a/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js b/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js index b831bd45b..ccd7da7ad 100644 --- a/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js +++ b/modules/ppcp-axo-block/resources/js/plugins/PayPalInsightsLoader.js @@ -1,7 +1,6 @@ import { registerPlugin } from '@wordpress/plugins'; import { useEffect, useCallback, useState, useRef } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; import PayPalInsights from '../../../../ppcp-axo/resources/js/Insights/PayPalInsights'; import { STORE_NAME } from '../stores/axoStore'; import usePayPalCommerceGateway from '../hooks/usePayPalCommerceGateway'; @@ -149,6 +148,7 @@ const usePaymentMethodTracking = ( axoConfig, eventTracking ) => { const isInitialMount = useRef( true ); const activePaymentMethod = useSelect( ( select ) => { + const { PAYMENT_STORE_KEY } = window.wc.wcBlocksData; return select( PAYMENT_STORE_KEY )?.getActivePaymentMethod(); }, [] ); diff --git a/modules/ppcp-axo-block/services.php b/modules/ppcp-axo-block/services.php index f91cd738a..6945215ba 100644 --- a/modules/ppcp-axo-block/services.php +++ b/modules/ppcp-axo-block/services.php @@ -39,8 +39,7 @@ return array( $container->get( 'onboarding.environment' ), $container->get( 'wcgateway.url' ), $container->get( 'axo.payment_method_selected_map' ), - $container->get( 'axo.supported-country-card-type-matrix' ), - $container->get( 'axo.shipping-wc-enabled-locations' ) + $container->get( 'axo.supported-country-card-type-matrix' ) ); }, ); diff --git a/modules/ppcp-axo-block/src/AxoBlockModule.php b/modules/ppcp-axo-block/src/AxoBlockModule.php index 1ebb068ed..c8216bf62 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration; /** * Class AxoBlockModule @@ -134,7 +135,6 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule } ); - // Enqueue the PayPal Insights script. add_action( 'wp_enqueue_scripts', function () use ( $c ) { @@ -187,6 +187,11 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule return; } + $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); + if ( ! $dcc_configuration->use_fastlane() ) { + return; + } + $module_url = $c->get( 'axoblock.url' ); $asset_version = $c->get( 'ppcp.asset-version' ); diff --git a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php index fa546d5ac..78bafdae3 100644 --- a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php +++ b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php @@ -93,13 +93,6 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { */ private $supported_country_card_type_matrix; - /** - * The list of WooCommerce enabled shipping locations. - * - * @var array - */ - private array $enabled_shipping_locations; - /** * AdvancedCardPaymentMethod constructor. * @@ -113,7 +106,6 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { * @param string $wcgateway_module_url The WcGateway module URL. * @param array $payment_method_selected_map Mapping of payment methods to the PayPal Insights 'payment_method_selected' types. * @param array $supported_country_card_type_matrix The supported country card type matrix for Axo. - * @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations. */ public function __construct( string $module_url, @@ -125,8 +117,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { Environment $environment, string $wcgateway_module_url, array $payment_method_selected_map, - array $supported_country_card_type_matrix, - array $enabled_shipping_locations + array $supported_country_card_type_matrix ) { $this->name = AxoGateway::ID; $this->module_url = $module_url; @@ -139,7 +130,6 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { $this->wcgateway_module_url = $wcgateway_module_url; $this->payment_method_selected_map = $payment_method_selected_map; $this->supported_country_card_type_matrix = $supported_country_card_type_matrix; - $this->enabled_shipping_locations = $enabled_shipping_locations; } /** * {@inheritDoc} @@ -237,7 +227,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { ), 'allowed_cards' => $this->supported_country_card_type_matrix, 'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(), - 'enabled_shipping_locations' => $this->enabled_shipping_locations, + 'enabled_shipping_locations' => apply_filters( 'woocommerce_paypal_payments_axo_shipping_wc_enabled_locations', array() ), 'style_options' => array( 'root' => array( 'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '', diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index e6767a29a..121b17805 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -70,8 +70,7 @@ return array( $container->get( 'api.shop.currency.getter' ), $container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'wcgateway.url' ), - $container->get( 'axo.supported-country-card-type-matrix' ), - $container->get( 'axo.shipping-wc-enabled-locations' ) + $container->get( 'axo.supported-country-card-type-matrix' ) ); }, @@ -329,33 +328,23 @@ return array( ); }, - 'axo.shipping-wc-enabled-locations' => static function ( ContainerInterface $container ): array { + 'axo.shipping-wc-enabled-locations' => static function ( ContainerInterface $container ) { $default_zone = new \WC_Shipping_Zone( 0 ); - $is_method_enabled = fn( \WC_Shipping_Method $method): bool => $method->enabled === 'yes'; - - $is_default_zone_enabled = ! empty( - array_filter( - $default_zone->get_shipping_methods(), - $is_method_enabled - ) - ); - - if ( $is_default_zone_enabled ) { + if ( ! empty( $default_zone->get_shipping_methods( true ) ) ) { return array(); } $shipping_zones = \WC_Shipping_Zones::get_zones(); - $get_zone_locations = fn( \WC_Shipping_Zone $zone): array => - ! empty( array_filter( $zone->get_shipping_methods(), $is_method_enabled ) ) + ! empty( $zone->get_shipping_methods( true ) ) ? array_map( fn( object $location): string => $location->code, $zone->get_zone_locations() ) : array(); - $enabled_locations = array_unique( + return array_unique( array_merge( ...array_map( $get_zone_locations, @@ -367,7 +356,5 @@ return array( ) ) ); - - return $enabled_locations; }, ); diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php index 6fafcb681..d02e27355 100644 --- a/modules/ppcp-axo/src/Assets/AxoManager.php +++ b/modules/ppcp-axo/src/Assets/AxoManager.php @@ -99,12 +99,6 @@ class AxoManager { * @var array */ private array $supported_country_card_type_matrix; - /** - * The list of WooCommerce enabled shipping locations. - * - * @var array - */ - private array $enabled_shipping_locations; /** * AxoManager constructor. @@ -120,7 +114,6 @@ class AxoManager { * @param LoggerInterface $logger The logger. * @param string $wcgateway_module_url The WcGateway module URL. * @param array $supported_country_card_type_matrix The supported country card type matrix for Axo. - * @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations. */ public function __construct( string $module_url, @@ -133,8 +126,7 @@ class AxoManager { CurrencyGetter $currency, LoggerInterface $logger, string $wcgateway_module_url, - array $supported_country_card_type_matrix, - array $enabled_shipping_locations + array $supported_country_card_type_matrix ) { $this->module_url = $module_url; @@ -147,7 +139,6 @@ class AxoManager { $this->currency = $currency; $this->logger = $logger; $this->wcgateway_module_url = $wcgateway_module_url; - $this->enabled_shipping_locations = $enabled_shipping_locations; $this->supported_country_card_type_matrix = $supported_country_card_type_matrix; } @@ -203,7 +194,7 @@ class AxoManager { return $data; } )( $this->insights_data ), 'allowed_cards' => $this->supported_country_card_type_matrix, 'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(), - 'enabled_shipping_locations' => $this->enabled_shipping_locations, + 'enabled_shipping_locations' => apply_filters( 'woocommerce_paypal_payments_axo_shipping_wc_enabled_locations', array() ), 'style_options' => array( 'root' => array( 'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '', diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index b055697ba..3ac0ff157 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -229,6 +229,16 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { } ); + /** + * Late loading locations because of trouble with some shipping plugins + */ + add_filter( + 'woocommerce_paypal_payments_axo_shipping_wc_enabled_locations', + function ( array $locations ) use ( $c ): array { + return array_merge( $locations, $c->get( 'axo.shipping-wc-enabled-locations' ) ); + } + ); + /** * Param types removed to avoid third-party issues. * diff --git a/modules/ppcp-blocks/src/BlocksModule.php b/modules/ppcp-blocks/src/BlocksModule.php index c5a2b29c5..28dbf5a04 100644 --- a/modules/ppcp-blocks/src/BlocksModule.php +++ b/modules/ppcp-blocks/src/BlocksModule.php @@ -143,10 +143,15 @@ class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule { add_filter( 'woocommerce_paypal_payments_sdk_components_hook', - function( array $components ) { - $components[] = 'buttons'; + function( array $components, string $context ) { + if ( str_ends_with( $context, '-block' ) ) { + $components[] = 'buttons'; + } + return $components; - } + }, + 10, + 2 ); return true; } diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 505fa60af..a4e094830 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -324,6 +324,10 @@ const bootstrap = () => { messagesBootstrap.init(); apmButtonsInit( PayPalCommerceGateway ); + + if ( ! renderer.useSmartButtons ) { + buttonsSpinner.unblock(); + } }; document.addEventListener( 'DOMContentLoaded', () => { diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index a85ad68ee..d314553bc 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -211,6 +211,7 @@ class CheckoutBootstap { const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== ''; + const useSmartButtons = this.renderer.useSmartButtons ?? true; const paypalButtonWrappers = { ...Object.entries( PayPalCommerceGateway.separate_buttons ).reduce( @@ -225,7 +226,8 @@ class CheckoutBootstap { this.standardOrderButtonSelector, ( isPaypal && isFreeTrial && hasVaultedPaypal ) || isNotOurGateway || - isSavedCard, + isSavedCard || + ( isPaypal && ! useSmartButtons ), 'ppcp-hidden' ); setVisible( '.ppcp-vaulted-paypal-details', isPaypal ); diff --git a/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js b/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js index b70403a50..b8736d0e1 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js @@ -6,16 +6,16 @@ const processAxoConfig = ( config ) => { const scriptOptions = {}; const sdkClientToken = config?.axo?.sdk_client_token; const uuid = uuidv4().replace( /-/g, '' ); - if ( sdkClientToken ) { + if ( sdkClientToken && config?.user?.is_logged !== true ) { scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken; scriptOptions[ 'data-client-metadata-id' ] = uuid; } return scriptOptions; }; -const processUserIdToken = ( config, sdkClientToken ) => { +const processUserIdToken = ( config ) => { const userIdToken = config?.save_payment_methods?.id_token; - return userIdToken && ! sdkClientToken + return userIdToken && config?.user?.is_logged === true ? { 'data-user-id-token': userIdToken } : {}; }; @@ -26,9 +26,6 @@ export const processConfig = ( config ) => { scriptOptions = merge( scriptOptions, config.script_attributes ); } const axoOptions = processAxoConfig( config ); - const userIdTokenOptions = processUserIdToken( - config, - axoOptions[ 'data-sdk-client-token' ] - ); + const userIdTokenOptions = processUserIdToken( config ); return merge.all( [ scriptOptions, axoOptions, userIdTokenOptions ] ); }; diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js index 2fd5feaa3..48134d2bc 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js +++ b/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js @@ -9,7 +9,7 @@ const scriptPromises = new Map(); const handleDataClientIdAttribute = async ( scriptOptions, config ) => { if ( config.data_client_id?.set_attribute && - config.vault_v3_enabled !== '1' + config.vault_v3_enabled !== true ) { return new Promise( ( resolve, reject ) => { dataClientIdAttributeHandler( diff --git a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js index 38f605cc7..00ae98a9c 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js @@ -75,7 +75,7 @@ export const loadPaypalScript = ( config, onLoaded, onError = null ) => { // Axo SDK options const sdkClientToken = config?.axo?.sdk_client_token; const uuid = uuidv4().replace( /-/g, '' ); - if ( sdkClientToken ) { + if ( sdkClientToken && config?.user?.is_logged !== true ) { scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken; scriptOptions[ 'data-client-metadata-id' ] = uuid; } @@ -96,7 +96,7 @@ export const loadPaypalScript = ( config, onLoaded, onError = null ) => { // Adds data-user-id-token to script options. const userIdToken = config?.save_payment_methods?.id_token; - if ( userIdToken && ! sdkClientToken ) { + if ( userIdToken && config?.user?.is_logged === true ) { scriptOptions[ 'data-user-id-token' ] = userIdToken; } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 9fa4708bc..005ee6248 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -7,6 +7,7 @@ import { handleShippingOptionsChange, handleShippingAddressChange, } from '../Helper/ShippingHandler.js'; +import { PaymentContext } from '../Helper/CheckoutMethodState'; class Renderer { constructor( @@ -28,6 +29,22 @@ class Renderer { this.reloadEventName = 'ppcp-reload-buttons'; } + /** + * Determine is PayPal smart buttons are used by inspecting the existing plugin configuration: + * If the url-param "components" contains a "buttons" element, smart buttons are enabled. + * + * @return {boolean} True, if smart buttons are present on the page. + */ + get useSmartButtons() { + if ( PaymentContext.Preview === this.defaultSettings?.context ) { + return true; + } + + const components = this.defaultSettings?.url_params?.components || ''; + + return components.split( ',' ).includes( 'buttons' ); + } + render( contextConfig, settingsOverride = {}, @@ -44,12 +61,14 @@ class Renderer { Object.keys( enabledSeparateGateways ).length !== 0; if ( ! hasEnabledSeparateGateways ) { - this.renderButtons( - settings.button.wrapper, - settings.button.style, - contextConfig, - hasEnabledSeparateGateways - ); + if ( this.useSmartButtons ) { + this.renderButtons( + settings.button.wrapper, + settings.button.style, + contextConfig, + hasEnabledSeparateGateways + ); + } } else { // render each button separately for ( const fundingSource of paypal @@ -141,7 +160,7 @@ class Renderer { // Check the condition and add the handler if needed if ( this.shouldEnableShippingCallback() ) { options.onShippingOptionsChange = ( data, actions ) => { - let shippingOptionsChange = + const shippingOptionsChange = ! this.isVenmoButtonClickedWhenVaultingIsEnabled( venmoButtonClicked ) @@ -152,10 +171,10 @@ class Renderer { ) : null; - return shippingOptionsChange + return shippingOptionsChange; }; options.onShippingAddressChange = ( data, actions ) => { - let shippingAddressChange = + const shippingAddressChange = ! this.isVenmoButtonClickedWhenVaultingIsEnabled( venmoButtonClicked ) @@ -166,7 +185,7 @@ class Renderer { ) : null; - return shippingAddressChange + return shippingAddressChange; }; } @@ -228,8 +247,13 @@ class Renderer { }; shouldEnableShippingCallback = () => { - let needShipping = this.defaultSettings.needShipping || this.defaultSettings.context === 'product' - return this.defaultSettings.should_handle_shipping_in_paypal && needShipping + const needShipping = + this.defaultSettings.needShipping || + this.defaultSettings.context === 'product'; + return ( + this.defaultSettings.should_handle_shipping_in_paypal && + needShipping + ); }; isAlreadyRendered( wrapper, fundingSource ) { diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 5cd0c85e1..6844a08d0 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -38,6 +38,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; +use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration; return array( 'button.client_id' => static function ( ContainerInterface $container ): string { @@ -108,9 +109,18 @@ return array( assert( $settings_status instanceof SettingsStatus ); if ( in_array( $context, array( 'checkout', 'pay-now' ), true ) ) { - if ( $container->get( 'wcgateway.use-place-order-button' ) - || ! $settings_status->is_smart_button_enabled_for_location( $context ) - ) { + $redirect_to_pay = $container->get( 'wcgateway.use-place-order-button' ); + if ( $redirect_to_pay ) { + // No smart buttons, redirect the current page to PayPal for payment. + return new DisabledSmartButton(); + } + + $no_smart_buttons = ! $settings_status->is_smart_button_enabled_for_location( $context ); + $dcc_configuration = $container->get( 'wcgateway.configuration.dcc' ); + assert( $dcc_configuration instanceof DCCGatewayConfiguration ); + + if ( $no_smart_buttons && ! $dcc_configuration->is_enabled() ) { + // Smart buttons disabled, and also not using advanced card payments. return new DisabledSmartButton(); } } diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index c116796b9..a08c20b27 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -1594,9 +1594,16 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages * * @internal Matches filter name in APM extension. * - * @param array $components The array of components already registered. + * @param array $components The array of components already registered. + * @param string $context The SmartButton context. */ - return apply_filters( 'woocommerce_paypal_payments_sdk_components_hook', $components ); + return array_unique( + (array) apply_filters( + 'woocommerce_paypal_payments_sdk_components_hook', + $components, + $this->context() + ) + ); } /** diff --git a/modules/ppcp-card-fields/src/CardFieldsModule.php b/modules/ppcp-card-fields/src/CardFieldsModule.php index bb8e055a8..f87655f72 100644 --- a/modules/ppcp-card-fields/src/CardFieldsModule.php +++ b/modules/ppcp-card-fields/src/CardFieldsModule.php @@ -46,12 +46,6 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu return true; } - $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); - assert( $dcc_configuration instanceof DCCGatewayConfiguration ); - if ( ! $dcc_configuration->is_enabled() ) { - return true; - } - /** * Param types removed to avoid third-party issues. * @@ -59,7 +53,10 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu */ add_filter( 'woocommerce_paypal_payments_sdk_components_hook', - function( $components ) { + function( $components ) use ( $c ) { + if ( ! $c->get( 'wcgateway.configuration.dcc' )->is_enabled() ) { + return $components; + } if ( in_array( 'hosted-fields', $components, true ) ) { $key = array_search( 'hosted-fields', $components, true ); if ( $key !== false ) { @@ -80,7 +77,10 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu * @psalm-suppress MissingClosureReturnType * @psalm-suppress MissingClosureParamType */ - function( $default_fields, $id ) { + function( $default_fields, $id ) use ( $c ) { + if ( ! $c->get( 'wcgateway.configuration.dcc' )->is_enabled() ) { + return $default_fields; + } if ( CreditCardGateway::ID === $id && apply_filters( 'woocommerce_paypal_payments_enable_cardholder_name_field', false ) ) { $default_fields['card-name-field'] = '

@@ -113,6 +113,9 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu add_filter( 'ppcp_create_order_request_body_data', function( array $data, string $payment_method ) use ( $c ): array { + if ( ! $c->get( 'wcgateway.configuration.dcc' )->is_enabled() ) { + return $data; + } // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $payment_method !== CreditCardGateway::ID ) { return $data; diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index b6abcea56..07a6da27f 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -115,4 +115,43 @@ return array( $container->get( 'api.bearer' ) ); }, + + /** + * Configuration for the new/old settings map. + * + * @returns SettingsMap[] + */ + 'compat.setting.new-to-old-map' => function( ContainerInterface $container ) : array { + $are_new_settings_enabled = $container->get( 'wcgateway.settings.admin-settings-enabled' ); + if ( ! $are_new_settings_enabled ) { + return array(); + } + + return array( + new SettingsMap( + $container->get( 'settings.data.common' ), + array( + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + ) + ), + new SettingsMap( + $container->get( 'settings.data.general' ), + array( + 'is_sandbox' => 'sandbox_on', + 'live_client_id' => 'client_id_production', + 'live_client_secret' => 'client_secret_production', + 'live_merchant_id' => 'merchant_id_production', + 'live_merchant_email' => 'merchant_email_production', + 'sandbox_client_id' => 'client_id_sandbox', + 'sandbox_client_secret' => 'client_secret_sandbox', + 'sandbox_merchant_id' => 'merchant_id_sandbox', + 'sandbox_merchant_email' => 'merchant_email_sandbox', + ) + ), + ); + }, + 'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper { + return new SettingsMapHelper( $container->get( 'compat.setting.new-to-old-map' ) ); + }, ); diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index 94a4d0d61..d52e13454 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -51,14 +51,24 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule { */ public function run( ContainerInterface $c ): bool { - $this->initialize_ppec_compat_layer( $c ); - $this->initialize_tracking_compat_layer( $c ); + add_action( + 'woocommerce_init', + function() use ( $c ) { + $this->initialize_ppec_compat_layer( $c ); + $this->initialize_tracking_compat_layer( $c ); + } + ); - $asset_loader = $c->get( 'compat.assets' ); - assert( $asset_loader instanceof CompatAssets ); + add_action( + 'init', + function() use ( $c ) { + $asset_loader = $c->get( 'compat.assets' ); + assert( $asset_loader instanceof CompatAssets ); - add_action( 'init', array( $asset_loader, 'register' ) ); - add_action( 'admin_enqueue_scripts', array( $asset_loader, 'enqueue' ) ); + $asset_loader->register(); + add_action( 'admin_enqueue_scripts', array( $asset_loader, 'enqueue' ) ); + } + ); $this->migrate_pay_later_settings( $c ); $this->migrate_smart_button_settings( $c ); @@ -72,11 +82,9 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule { $this->initialize_nyp_compat_layer(); } - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - $is_wc_bookings_active = $c->get( 'compat.wc_bookings.is_supported_plugin_version_active' ); if ( $is_wc_bookings_active ) { - $this->initialize_wc_bookings_compat_layer( $logger ); + $this->initialize_wc_bookings_compat_layer( $c ); } return true; @@ -427,13 +435,13 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule { /** * Sets up the compatibility layer for WooCommerce Bookings plugin. * - * @param LoggerInterface $logger The logger. + * @param ContainerInterface $container The logger. * @return void */ - protected function initialize_wc_bookings_compat_layer( LoggerInterface $logger ): void { + protected function initialize_wc_bookings_compat_layer( ContainerInterface $container ): void { add_action( 'woocommerce_paypal_payments_shipping_callback_woocommerce_order_created', - static function ( WC_Order $wc_order, WC_Cart $wc_cart ) use ( $logger ): void { + static function ( WC_Order $wc_order, WC_Cart $wc_cart ) use ( $container ): void { try { $cart_contents = $wc_cart->get_cart(); foreach ( $cart_contents as $cart_item ) { @@ -474,7 +482,7 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule { } } } catch ( Exception $exception ) { - $logger->warning( 'Failed to create booking for WooCommerce Bookings plugin: ' . $exception->getMessage() ); + $container->get( 'woocommerce.logger.woocommerce' )->warning( 'Failed to create booking for WooCommerce Bookings plugin: ' . $exception->getMessage() ); } }, 10, diff --git a/modules/ppcp-compat/src/SettingsMap.php b/modules/ppcp-compat/src/SettingsMap.php new file mode 100644 index 000000000..7c97646a0 --- /dev/null +++ b/modules/ppcp-compat/src/SettingsMap.php @@ -0,0 +1,63 @@ + + */ + private array $map; + + /** + * The constructor. + * + * @param AbstractDataModel $model The new settings model. + * @param array $map The map of the new setting key to the old setting keys. + */ + public function __construct( AbstractDataModel $model, array $map ) { + $this->model = $model; + $this->map = $map; + } + + /** + * The model. + * + * @return AbstractDataModel + */ + public function get_model(): AbstractDataModel { + return $this->model; + } + + /** + * The map of the new setting key to the old setting keys. + * + * @return array + */ + public function get_map(): array { + return $this->map; + } +} diff --git a/modules/ppcp-compat/src/SettingsMapHelper.php b/modules/ppcp-compat/src/SettingsMapHelper.php new file mode 100644 index 000000000..474c6f533 --- /dev/null +++ b/modules/ppcp-compat/src/SettingsMapHelper.php @@ -0,0 +1,70 @@ +settings_map = $settings_map; + } + + /** + * Retrieves the mapped value from the new settings. + * + * @param string $key The key. + * @return ?mixed the mapped value or Null if it doesn't exist. + */ + public function mapped_value( string $key ) { + if ( ! $this->has_mapped_key( $key ) ) { + return null; + } + + foreach ( $this->settings_map as $settings_map ) { + $mapped_key = array_search( $key, $settings_map->get_map(), true ); + $new_settings = $settings_map->get_model()->to_array(); + if ( ! empty( $new_settings[ $mapped_key ] ) ) { + return $new_settings[ $mapped_key ]; + } + } + + return null; + } + + /** + * Checks if the given key exists in the new settings. + * + * @param string $key The key. + * @return bool true if the given key exists in the new settings, otherwise false. + */ + public function has_mapped_key( string $key ) : bool { + foreach ( $this->settings_map as $settings_map ) { + if ( in_array( $key, $settings_map->get_map(), true ) ) { + return true; + } + } + + return false; + } +} diff --git a/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php b/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php index b66ad378f..1fac77a6e 100644 --- a/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php +++ b/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php @@ -44,12 +44,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo * {@inheritDoc} */ public function run( ContainerInterface $c ): bool { - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - - if ( ! self::should_add_local_apm_gateways( $settings ) ) { - return true; - } add_filter( 'woocommerce_payment_gateways', @@ -59,6 +53,9 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo * @psalm-suppress MissingClosureParamType */ function ( $methods ) use ( $c ) { + if ( ! self::should_add_local_apm_gateways( $c ) ) { + return $methods; + } $onboarding_state = $c->get( 'onboarding.state' ); if ( $onboarding_state->current_state() === State::STATE_START ) { return $methods; @@ -85,6 +82,9 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo * @psalm-suppress MissingClosureParamType */ function ( $methods ) use ( $c ) { + if ( ! self::should_add_local_apm_gateways( $c ) ) { + return $methods; + } if ( ! is_array( $methods ) ) { return $methods; } @@ -115,6 +115,9 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo add_action( 'woocommerce_blocks_payment_method_type_registration', function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { + if ( ! self::should_add_local_apm_gateways( $c ) ) { + return; + } $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' ); foreach ( $payment_methods as $key => $value ) { $payment_method_registry->register( $c->get( 'ppcp-local-apms.' . $key . '.payment-method' ) ); @@ -125,6 +128,9 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo add_filter( 'woocommerce_paypal_payments_localized_script_data', function ( array $data ) use ( $c ) { + if ( ! self::should_add_local_apm_gateways( $c ) ) { + return $data; + } $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' ); $default_disable_funding = $data['url_params']['disable-funding'] ?? ''; @@ -143,6 +149,9 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo * @psalm-suppress MissingClosureParamType */ function( $order_id ) use ( $c ) { + if ( ! self::should_add_local_apm_gateways( $c ) ) { + return; + } $order = wc_get_order( $order_id ); if ( ! $order instanceof WC_Order ) { return; @@ -175,6 +184,9 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo add_action( 'woocommerce_paypal_payments_payment_capture_completed_webhook_handler', function( WC_Order $wc_order, string $order_id ) use ( $c ) { + if ( ! self::should_add_local_apm_gateways( $c ) ) { + return; + } $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' ); if ( ! $this->is_local_apm( $wc_order->get_payment_method(), $payment_methods ) @@ -214,10 +226,12 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo /** * Check if the local APMs should be added to the available payment gateways. * - * @param Settings $settings PayPal gateway settings. + * @param ContainerInterface $container Container. * @return bool */ - private function should_add_local_apm_gateways( Settings $settings ): bool { + private function should_add_local_apm_gateways( ContainerInterface $container ): bool { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); return $settings->has( 'enabled' ) && $settings->get( 'enabled' ) === true && $settings->has( 'allow_local_apm_gateways' ) diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index 369e82824..56a49be8e 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -15,7 +15,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; @@ -25,7 +24,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; return array( - 'api.sandbox-host' => static function ( ContainerInterface $container ): string { + 'api.sandbox-host' => static function ( ContainerInterface $container ): string { $state = $container->get( 'onboarding.state' ); @@ -39,7 +38,7 @@ return array( } return CONNECT_WOO_SANDBOX_URL; }, - 'api.production-host' => static function ( ContainerInterface $container ): string { + 'api.production-host' => static function ( ContainerInterface $container ): string { $state = $container->get( 'onboarding.state' ); @@ -54,7 +53,7 @@ return array( } return CONNECT_WOO_URL; }, - 'api.host' => static function ( ContainerInterface $container ): string { + 'api.host' => static function ( ContainerInterface $container ): string { $environment = $container->get( 'onboarding.environment' ); /** @@ -66,25 +65,7 @@ return array( ? (string) $container->get( 'api.sandbox-host' ) : (string) $container->get( 'api.production-host' ); }, - 'api.paypal-host-production' => static function( ContainerInterface $container ) : string { - return PAYPAL_API_URL; - }, - 'api.paypal-host-sandbox' => static function( ContainerInterface $container ) : string { - return PAYPAL_SANDBOX_API_URL; - }, - 'api.paypal-website-url-production' => static function( ContainerInterface $container ) : string { - return PAYPAL_URL; - }, - 'api.paypal-website-url-sandbox' => static function( ContainerInterface $container ) : string { - return PAYPAL_SANDBOX_URL; - }, - 'api.partner_merchant_id-production' => static function( ContainerInterface $container ) : string { - return CONNECT_WOO_MERCHANT_ID; - }, - 'api.partner_merchant_id-sandbox' => static function( ContainerInterface $container ) : string { - return CONNECT_WOO_SANDBOX_MERCHANT_ID; - }, - 'api.paypal-host' => function( ContainerInterface $container ) : string { + 'api.paypal-host' => function( ContainerInterface $container ) : string { $environment = $container->get( 'onboarding.environment' ); /** * The current environment. @@ -97,7 +78,7 @@ return array( return $container->get( 'api.paypal-host-production' ); }, - 'api.paypal-website-url' => function( ContainerInterface $container ) : string { + 'api.paypal-website-url' => function( ContainerInterface $container ) : string { $environment = $container->get( 'onboarding.environment' ); assert( $environment instanceof Environment ); if ( $environment->current_environment_is( Environment::SANDBOX ) ) { @@ -107,7 +88,7 @@ return array( }, - 'api.bearer' => static function ( ContainerInterface $container ): Bearer { + 'api.bearer' => static function ( ContainerInterface $container ): Bearer { $state = $container->get( 'onboarding.state' ); @@ -134,16 +115,16 @@ return array( $settings ); }, - 'onboarding.state' => function( ContainerInterface $container ) : State { + 'onboarding.state' => function( ContainerInterface $container ) : State { $settings = $container->get( 'wcgateway.settings' ); return new State( $settings ); }, - 'onboarding.environment' => function( ContainerInterface $container ) : Environment { + 'onboarding.environment' => function( ContainerInterface $container ) : Environment { $settings = $container->get( 'wcgateway.settings' ); return new Environment( $settings ); }, - 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { + 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { $state = $container->get( 'onboarding.state' ); $login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' ); return new OnboardingAssets( @@ -156,14 +137,14 @@ return array( ); }, - 'onboarding.url' => static function ( ContainerInterface $container ): string { + 'onboarding.url' => static function ( ContainerInterface $container ): string { return plugins_url( '/modules/ppcp-onboarding/', dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller { + 'api.endpoint.login-seller-production' => static function ( ContainerInterface $container ) : LoginSeller { $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new LoginSeller( @@ -173,7 +154,7 @@ return array( ); }, - 'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller { + 'api.endpoint.login-seller-sandbox' => static function ( ContainerInterface $container ) : LoginSeller { $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new LoginSeller( @@ -183,7 +164,7 @@ return array( ); }, - 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { + 'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint { $request_data = $container->get( 'button.request-data' ); $login_seller_production = $container->get( 'api.endpoint.login-seller-production' ); @@ -203,7 +184,7 @@ return array( new Cache( 'ppcp-client-credentials-cache' ) ); }, - 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { + 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { return new UpdateSignupLinksEndpoint( $container->get( 'wcgateway.settings' ), $container->get( 'button.request-data' ), @@ -213,26 +194,10 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals { - - return new PartnerReferrals( - CONNECT_WOO_SANDBOX_URL, - new ConnectBearer(), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, - 'api.endpoint.partner-referrals-production' => static function ( ContainerInterface $container ) : PartnerReferrals { - - return new PartnerReferrals( - CONNECT_WOO_URL, - new ConnectBearer(), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, - 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { + 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { return new Cache( 'ppcp-paypal-signup-link' ); }, - 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { + 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { return array( 'production-ppcp', 'production-express_checkout', @@ -240,12 +205,12 @@ return array( 'sandbox-express_checkout', ); }, - 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { + 'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) { return new OnboardingSendOnlyNoticeRenderer( $container->get( 'wcgateway.send-only-message' ) ); }, - 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { + 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { $partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' ); $partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' ); $partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' ); @@ -261,14 +226,14 @@ return array( $logger ); }, - 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { + 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { return new OnboardingOptionsRenderer( $container->get( 'onboarding.url' ), $container->get( 'api.shop.country' ), $container->get( 'wcgateway.settings' ) ); }, - 'onboarding.rest' => static function( $container ) : OnboardingRESTController { + 'onboarding.rest' => static function( $container ) : OnboardingRESTController { return new OnboardingRESTController( $container ); }, ); diff --git a/modules/ppcp-onboarding/src/OnboardingModule.php b/modules/ppcp-onboarding/src/OnboardingModule.php index 3ec48fabf..023319827 100644 --- a/modules/ppcp-onboarding/src/OnboardingModule.php +++ b/modules/ppcp-onboarding/src/OnboardingModule.php @@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; +use WooCommerce\PayPalCommerce\Settings\SettingsModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -44,33 +45,32 @@ class OnboardingModule implements ServiceModule, ExtendingModule, ExecutableModu */ public function run( ContainerInterface $c ): bool { - if ( ! apply_filters( - // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores - 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', - getenv( 'PCP_SETTINGS_ENABLED' ) === '1' - ) ) { + add_action( + 'admin_enqueue_scripts', + function() use ( $c ) { + if ( + apply_filters( + // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', + getenv( 'PCP_SETTINGS_ENABLED' ) === '1' + ) && ! SettingsModule::should_use_the_old_ui() + ) { + return; + } - $asset_loader = $c->get( 'onboarding.assets' ); - /** - * The OnboardingAssets. - * - * @var OnboardingAssets $asset_loader - */ - add_action( - 'admin_enqueue_scripts', - array( - $asset_loader, - 'register', - ) - ); - add_action( - 'woocommerce_settings_checkout', - array( - $asset_loader, - 'enqueue', - ) - ); - } + $asset_loader = $c->get( 'onboarding.assets' ); + assert( $asset_loader instanceof OnboardingAssets ); + + $asset_loader->register(); + add_action( + 'woocommerce_settings_checkout', + array( + $asset_loader, + 'enqueue', + ) + ); + } + ); add_filter( 'woocommerce_form_field', diff --git a/modules/ppcp-order-tracking/src/OrderTrackingModule.php b/modules/ppcp-order-tracking/src/OrderTrackingModule.php index cdc44fbdb..ec9324948 100644 --- a/modules/ppcp-order-tracking/src/OrderTrackingModule.php +++ b/modules/ppcp-order-tracking/src/OrderTrackingModule.php @@ -55,23 +55,21 @@ class OrderTrackingModule implements ServiceModule, ExtendingModule, ExecutableM * @throws NotFoundException */ public function run( ContainerInterface $c ): bool { - $endpoint = $c->get( 'order-tracking.endpoint.controller' ); - assert( $endpoint instanceof OrderTrackingEndpoint ); - add_action( 'wc_ajax_' . OrderTrackingEndpoint::ENDPOINT, array( $endpoint, 'handle_request' ) ); + add_action( + 'wc_ajax_' . OrderTrackingEndpoint::ENDPOINT, + function() use ( $c ) { + $c->get( 'order-tracking.endpoint.controller' )->handle_request(); + } + ); $asset_loader = $c->get( 'order-tracking.assets' ); assert( $asset_loader instanceof OrderEditPageAssets ); - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); - - $bearer = $c->get( 'api.bearer' ); - add_action( 'init', - function() use ( $asset_loader, $bearer ) { - if ( ! $this->is_tracking_enabled( $bearer ) ) { + function() use ( $asset_loader, $c ) { + if ( ! $this->is_tracking_enabled( $c->get( 'api.bearer' ) ) ) { return; } @@ -80,8 +78,8 @@ class OrderTrackingModule implements ServiceModule, ExtendingModule, ExecutableM ); add_action( 'init', - function() use ( $asset_loader, $bearer ) { - if ( ! $this->is_tracking_enabled( $bearer ) ) { + function() use ( $asset_loader, $c ) { + if ( ! $this->is_tracking_enabled( $c->get( 'api.bearer' ) ) ) { return; } @@ -89,9 +87,6 @@ class OrderTrackingModule implements ServiceModule, ExtendingModule, ExecutableM } ); - $meta_box_renderer = $c->get( 'order-tracking.meta-box.renderer' ); - assert( $meta_box_renderer instanceof MetaBoxRenderer ); - add_action( 'add_meta_boxes', /** @@ -103,8 +98,8 @@ class OrderTrackingModule implements ServiceModule, ExtendingModule, ExecutableM * * @psalm-suppress MissingClosureParamType */ - function( string $post_type, $post_or_order_object ) use ( $meta_box_renderer, $bearer ) { - if ( ! $this->is_tracking_enabled( $bearer ) ) { + function( string $post_type, $post_or_order_object ) use ( $c ) { + if ( ! $this->is_tracking_enabled( $c->get( 'api.bearer' ) ) ) { return; } @@ -135,6 +130,9 @@ class OrderTrackingModule implements ServiceModule, ExtendingModule, ExecutableM ? wc_get_page_screen_id( 'shop-order' ) : 'shop_order'; + $meta_box_renderer = $c->get( 'order-tracking.meta-box.renderer' ); + assert( $meta_box_renderer instanceof MetaBoxRenderer ); + add_meta_box( 'ppcp_order-tracking', __( 'PayPal Package Tracking', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-paylater-block/src/PayLaterBlockModule.php b/modules/ppcp-paylater-block/src/PayLaterBlockModule.php index 896d98027..32d8dc762 100644 --- a/modules/ppcp-paylater-block/src/PayLaterBlockModule.php +++ b/modules/ppcp-paylater-block/src/PayLaterBlockModule.php @@ -71,12 +71,12 @@ class PayLaterBlockModule implements ServiceModule, ExtendingModule, ExecutableM return true; } - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - add_action( 'init', - function () use ( $c, $settings ): void { + function () use ( $c ): void { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + $script_handle = 'ppcp-paylater-block'; wp_register_script( $script_handle, diff --git a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php index c7fef6ceb..2c13bed71 100644 --- a/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php +++ b/modules/ppcp-paylater-configurator/src/PayLaterConfiguratorModule.php @@ -56,46 +56,46 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec * {@inheritDoc} */ public function run( ContainerInterface $c ) : bool { - $is_available = $c->get( 'paylater-configurator.is-available' ); - - if ( ! $is_available ) { - return true; - } - - $current_page_id = $c->get( 'wcgateway.current-ppcp-settings-page-id' ); - $is_wc_settings_page = $c->get( 'wcgateway.is-wc-settings-page' ); - $messaging_locations = $c->get( 'paylater-configurator.messaging-locations' ); - - $this->add_paylater_update_notice( $messaging_locations, $is_wc_settings_page, $current_page_id ); - - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - - add_action( - 'wc_ajax_' . SaveConfig::ENDPOINT, - static function () use ( $c ) { - $endpoint = $c->get( 'paylater-configurator.endpoint.save-config' ); - assert( $endpoint instanceof SaveConfig ); - $endpoint->handle_request(); - } - ); - - add_action( - 'wc_ajax_' . GetConfig::ENDPOINT, - static function () use ( $c ) { - $endpoint = $c->get( 'paylater-configurator.endpoint.get-config' ); - assert( $endpoint instanceof GetConfig ); - $endpoint->handle_request(); - } - ); - - if ( $current_page_id !== Settings::PAY_LATER_TAB_ID ) { - return true; - } add_action( 'init', - static function () use ( $c, $settings ) { + static function () use ( $c ) { + $is_available = $c->get( 'paylater-configurator.is-available' ); + if ( ! $is_available ) { + return; + } + + $current_page_id = $c->get( 'wcgateway.current-ppcp-settings-page-id' ); + $is_wc_settings_page = $c->get( 'wcgateway.is-wc-settings-page' ); + $messaging_locations = $c->get( 'paylater-configurator.messaging-locations' ); + + self::add_paylater_update_notice( $messaging_locations, $is_wc_settings_page, $current_page_id ); + + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + add_action( + 'wc_ajax_' . SaveConfig::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'paylater-configurator.endpoint.save-config' ); + assert( $endpoint instanceof SaveConfig ); + $endpoint->handle_request(); + } + ); + + add_action( + 'wc_ajax_' . GetConfig::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'paylater-configurator.endpoint.get-config' ); + assert( $endpoint instanceof GetConfig ); + $endpoint->handle_request(); + } + ); + + if ( $current_page_id !== Settings::PAY_LATER_TAB_ID ) { + return; + } + wp_enqueue_script( 'ppcp-paylater-configurator-lib', 'https://www.paypalobjects.com/merchant-library/merchant-configurator.js', @@ -165,7 +165,7 @@ class PayLaterConfiguratorModule implements ServiceModule, ExtendingModule, Exec * * @return void */ - private function add_paylater_update_notice( array $message_locations, bool $is_settings_page, string $current_page_id ) : void { + private static function add_paylater_update_notice( array $message_locations, bool $is_settings_page, string $current_page_id ) : void { // The message must be registered on any WC-Settings page, except for the Pay Later page. if ( ! $is_settings_page || Settings::PAY_LATER_TAB_ID === $current_page_id ) { return; diff --git a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php index fe547ea17..2a629b8f0 100644 --- a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php +++ b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php @@ -99,12 +99,11 @@ class PayLaterWCBlocksModule implements ServiceModule, ExtendingModule, Executab return true; } - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - add_action( 'init', - function () use ( $c, $settings ): void { + function () use ( $c ): void { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); $config_factory = $c->get( 'paylater-configurator.factory.config' ); assert( $config_factory instanceof ConfigFactory ); @@ -186,47 +185,52 @@ class PayLaterWCBlocksModule implements ServiceModule, ExtendingModule, Executab 2 ); - /** - * Cannot return false for this path. - * - * @psalm-suppress PossiblyFalseArgument - */ - if ( function_exists( 'register_block_type' ) ) { - register_block_type( - dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CartPayLaterMessagesBlock', - array( - 'render_callback' => function ( array $attributes ) use ( $c ) { - return PayLaterWCBlocksUtils::render_paylater_block( - $attributes['blockId'] ?? 'woocommerce-paypal-payments/cart-paylater-messages', - $attributes['ppcpId'] ?? 'ppcp-cart-paylater-messages', - 'cart', - $c - ); - }, - ) - ); - } + add_action( + 'init', + function () use ( $c ): void { + if ( ! function_exists( 'register_block_type' ) ) { + return; + } - /** - * Cannot return false for this path. - * - * @psalm-suppress PossiblyFalseArgument - */ - if ( function_exists( 'register_block_type' ) ) { - register_block_type( - dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CheckoutPayLaterMessagesBlock', - array( - 'render_callback' => function ( array $attributes ) use ( $c ) { - return PayLaterWCBlocksUtils::render_paylater_block( - $attributes['blockId'] ?? 'woocommerce-paypal-payments/checkout-paylater-messages', - $attributes['ppcpId'] ?? 'ppcp-checkout-paylater-messages', - 'checkout', - $c - ); - }, - ) - ); - } + /** + * Cannot return false for this path. + * + * @psalm-suppress PossiblyFalseArgument + */ + register_block_type( + dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CartPayLaterMessagesBlock', + array( + 'render_callback' => function ( array $attributes ) use ( $c ) { + return PayLaterWCBlocksUtils::render_paylater_block( + $attributes['blockId'] ?? 'woocommerce-paypal-payments/cart-paylater-messages', + $attributes['ppcpId'] ?? 'ppcp-cart-paylater-messages', + 'cart', + $c + ); + }, + ) + ); + + /** + * Cannot return false for this path. + * + * @psalm-suppress PossiblyFalseArgument + */ + register_block_type( + dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CheckoutPayLaterMessagesBlock', + array( + 'render_callback' => function ( array $attributes ) use ( $c ) { + return PayLaterWCBlocksUtils::render_paylater_block( + $attributes['blockId'] ?? 'woocommerce-paypal-payments/checkout-paylater-messages', + $attributes['ppcpId'] ?? 'ppcp-checkout-paylater-messages', + 'checkout', + $c + ); + }, + ) + ); + } + ); // This is a fallback for the default Cart block that haven't been saved with the inserted Pay Later messaging block. add_filter( @@ -271,7 +275,7 @@ class PayLaterWCBlocksModule implements ServiceModule, ExtendingModule, Executab if ( self::is_under_cart_totals_placement_enabled() ) { add_action( 'enqueue_block_editor_assets', - function () use ( $c, $settings ): void { + function () use ( $c ): void { $handle = 'ppcp-checkout-paylater-block-editor-inserter'; $path = $c->get( 'paylater-wc-blocks.url' ) . 'assets/js/cart-paylater-block-inserter.js'; diff --git a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php index 53d045454..48c3a71af 100644 --- a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php +++ b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php @@ -599,11 +599,11 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu } ); - $endpoint = $c->get( 'paypal-subscriptions.deactivate-plan-endpoint' ); - assert( $endpoint instanceof DeactivatePlanEndpoint ); add_action( 'wc_ajax_' . DeactivatePlanEndpoint::ENDPOINT, - array( $endpoint, 'handle_request' ) + function() use ( $c ) { + $c->get( 'paypal-subscriptions.deactivate-plan-endpoint' )->handle_request(); + } ); add_action( diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php index ae3d7dfa0..434a08925 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreatePaymentToken.php @@ -94,7 +94,9 @@ class CreatePaymentToken implements EndpointInterface { ) ); - $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source ); + $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + + $result = $this->payment_method_tokens_endpoint->create_payment_token( $payment_source, $customer_id ); if ( is_user_logged_in() && isset( $result->customer->id ) ) { $current_user_id = get_current_user_id(); diff --git a/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php b/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php index 7eb3a5ace..6952feb43 100644 --- a/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php +++ b/modules/ppcp-save-payment-methods/src/Endpoint/CreateSetupToken.php @@ -103,7 +103,9 @@ class CreateSetupToken implements EndpointInterface { ); } - $result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source ); + $customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true ); + + $result = $this->payment_method_tokens_endpoint->setup_tokens( $payment_source, $customer_id ); wp_send_json_success( $result ); return true; diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php index 406192a5d..c76162193 100644 --- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php +++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php @@ -63,33 +63,26 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut return true; } - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); - - $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); - assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); - add_action( 'woocommerce_paypal_payments_gateway_migrate_on_update', - function() use ( $settings, $billing_agreements_endpoint ) { + function() use ( $c ) { + $billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' ); + assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint ); + $reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled(); if ( $reference_transaction_enabled !== true ) { - $settings->set( 'vault_enabled', false ); - $settings->persist(); + $c->get( 'wcgateway.settings' )->set( 'vault_enabled', false ); + $c->get( 'wcgateway.settings' )->persist(); } } ); - if ( - ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) - && ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) - ) { - return true; - } - add_filter( 'woocommerce_paypal_payments_localized_script_data', function( array $localized_script_data ) use ( $c ) { + if ( ! self::vault_enabled( $c ) ) { + return $localized_script_data; + } $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) { @@ -109,12 +102,11 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut // Adds attributes needed to save payment method. add_filter( 'ppcp_create_order_request_body_data', - function( array $data, string $payment_method, array $request_data ) use ( $settings ): array { + function( array $data, string $payment_method, array $request_data ) use ( $c ): array { + if ( ! self::vault_enabled( $c ) ) { + return $data; + } if ( $payment_method === CreditCardGateway::ID ) { - if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) { - return $data; - } - $save_payment_method = $request_data['save_payment_method'] ?? false; if ( $save_payment_method ) { $data['payment_source'] = array( @@ -141,10 +133,6 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut } if ( $payment_method === PayPalGateway::ID ) { - if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) { - return $data; - } - $funding_source = $request_data['funding_source'] ?? null; if ( $funding_source && $funding_source === 'venmo' ) { @@ -197,6 +185,9 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'woocommerce_paypal_payments_after_order_processor', function( WC_Order $wc_order, Order $order ) use ( $c ) { + if ( ! self::vault_enabled( $c ) ) { + return; + } $payment_source = $order->payment_source(); assert( $payment_source instanceof PaymentSource ); @@ -259,13 +250,30 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut 2 ); - add_filter( 'woocommerce_paypal_payments_disable_add_payment_method', '__return_false' ); - add_filter( 'woocommerce_paypal_payments_should_render_card_custom_fields', '__return_false' ); + add_filter( + 'woocommerce_paypal_payments_disable_add_payment_method', + function ( bool $value ) use ( $c ): bool { + if ( ! self::vault_enabled( $c ) ) { + return $value; + } + return false; + } + ); + + add_filter( + 'woocommerce_paypal_payments_should_render_card_custom_fields', + function ( bool $value ) use ( $c ): bool { + if ( ! self::vault_enabled( $c ) ) { + return $value; + } + return false; + } + ); add_action( 'wp_enqueue_scripts', function() use ( $c ) { - if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) ) { + if ( ! is_user_logged_in() || ! ( $this->is_add_payment_method_page() || $this->is_subscription_change_payment_method_page() ) || ! self::vault_enabled( $c ) ) { return; } @@ -355,8 +363,8 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'woocommerce_add_payment_method_form_bottom', - function () { - if ( ! is_user_logged_in() || ! is_add_payment_method_page() ) { + function () use ( $c ) { + if ( ! is_user_logged_in() || ! is_add_payment_method_page() || ! self::vault_enabled( $c ) ) { return; } @@ -367,6 +375,9 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'wc_ajax_' . CreateSetupToken::ENDPOINT, static function () use ( $c ) { + if ( ! self::vault_enabled( $c ) ) { + return; + } $endpoint = $c->get( 'save-payment-methods.endpoint.create-setup-token' ); assert( $endpoint instanceof CreateSetupToken ); @@ -377,6 +388,9 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'wc_ajax_' . CreatePaymentToken::ENDPOINT, static function () use ( $c ) { + if ( ! self::vault_enabled( $c ) ) { + return; + } $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token' ); assert( $endpoint instanceof CreatePaymentToken ); @@ -387,6 +401,9 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'wc_ajax_' . CreatePaymentTokenForGuest::ENDPOINT, static function () use ( $c ) { + if ( ! self::vault_enabled( $c ) ) { + return; + } $endpoint = $c->get( 'save-payment-methods.endpoint.create-payment-token-for-guest' ); assert( $endpoint instanceof CreatePaymentTokenForGuest ); @@ -397,6 +414,9 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_action( 'woocommerce_paypal_payments_before_delete_payment_token', function( string $token_id ) use ( $c ) { + if ( ! self::vault_enabled( $c ) ) { + return; + } try { $endpoint = $c->get( 'api.endpoint.payment-tokens' ); assert( $endpoint instanceof PaymentTokensEndpoint ); @@ -419,13 +439,11 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_filter( 'woocommerce_paypal_payments_credit_card_gateway_supports', function( array $supports ) use ( $c ): array { - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof ContainerInterface ); - - if ( $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ) ) { - $supports[] = 'tokenization'; - $supports[] = 'add_payment_method'; + if ( ! self::vault_enabled( $c ) ) { + return $supports; } + $supports[] = 'tokenization'; + $supports[] = 'add_payment_method'; return $supports; } @@ -433,7 +451,10 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut add_filter( 'woocommerce_paypal_payments_save_payment_methods_eligible', - function() { + function( bool $value ) use ( $c ): bool { + if ( ! self::vault_enabled( $c ) ) { + return $value; + } return true; } ); @@ -481,4 +502,24 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut return $localized_script_data; } + + /** + * Checks whether the vault functionality is enabled based on configuration settings. + * + * @param ContainerInterface $container The dependency injection container from which settings can be retrieved. + * + * @return bool Returns true if either 'vault_enabled' or 'vault_enabled_dcc' settings are enabled; otherwise, false. + */ + private static function vault_enabled( ContainerInterface $container ): bool { + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + if ( + ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) + && ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) + ) { + return false; + } + return true; + } } diff --git a/modules/ppcp-settings/package.json b/modules/ppcp-settings/package.json index 7a6aa9277..0a1391b88 100644 --- a/modules/ppcp-settings/package.json +++ b/modules/ppcp-settings/package.json @@ -13,6 +13,7 @@ "@wordpress/scripts": "^30.3.0" }, "dependencies": { + "@paypal/react-paypal-js": "^8.7.0", "@woocommerce/settings": "^1.0.0", "react-select": "^5.8.3" } diff --git a/modules/ppcp-settings/resources/css/_global.scss b/modules/ppcp-settings/resources/css/_global.scss index a5eed96d0..3b405d6c1 100644 --- a/modules/ppcp-settings/resources/css/_global.scss +++ b/modules/ppcp-settings/resources/css/_global.scss @@ -58,10 +58,6 @@ h1, h2, h3, h4 { color: $color-black; } -a:not(.button) { - color: $color-blueberry; -} - .components-form-toggle.is-checked > .components-form-toggle__track { background-color: $color-blueberry; } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss index d65d1f184..427b4b1fb 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_badge-box.scss @@ -18,7 +18,7 @@ } } - &-image-badge { + .ppcp-r-badge-box__title-text:not(:empty) + .ppcp-r-badge-box__title-image-badge { margin-left: 7px; img { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 3528ad71f..4174e6a23 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -5,16 +5,16 @@ button.components-button, a.components-button { } &:disabled { - color: $color-white; + color: $color-gray-700; } - border-radius: 2px; - padding: 14px 17px; + border-radius: 50px; + padding: 15px 32px; height: auto; } &.is-primary { - @include font(13, 20, 400); + @include font(14, 18, 900); &:not(:disabled) { background-color: $color-blueberry; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss index c58f8b021..1a9cf102c 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss @@ -21,23 +21,25 @@ } } - &__checkbox-value { - @include hide-input-field; + &__checkbox { + position: relative; - &:not(:checked) + .ppcp-r__checkbox-presentation img { - display: none; + input { + margin: 0; + border-color: $color-gray-600; + + &:checked { + background-color: $color-blueberry; + border-color:$color-blueberry; + } } - &:checked { - + .ppcp-r__checkbox-presentation { - width: 20px; - height: 20px; - border: none; + .components-checkbox-control__input-container { + margin: 0; + } - img { - border-radius: 2px; - } - } + .components-base-control__field { + margin: 0; } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-modal.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-modal.scss index 4274a370a..df0cde379 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-modal.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-modal.scss @@ -15,7 +15,7 @@ &__content { margin-top: 60px; - padding:0 24px 28px 24px; + padding: 0 24px 28px 24px; } } @@ -96,6 +96,36 @@ margin-top: 2px; } } + + .components-radio-control { + .components-flex { + gap: 18px; + } + + label { + @include font(14, 20, 400); + color: $color-black; + } + + &__option { + gap: 18px; + } + + &__input { + border-color: $color-gray-700; + margin-right: 0; + &:checked { + border: 1px solid $color-gray-700; + background-color: $color-white; + + &::before { + transform: translate(3px, 3px); + border-width: 6px; + border-color: $color-blueberry; + } + } + } + } } .ppcp-r-modal__field-row--save button.is-primary { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss index 84e3a1f19..6c3976ed7 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_separator.scss @@ -2,13 +2,22 @@ display: flex; align-items: center; + &__space, &__line { - height: 1px; - background-color: $color-gray-600; + margin: 0; display: block; width: 100%; } + &__line { + background-color: $color-gray-400; + height: 1px; + } + + &__space { + margin-bottom: 48px; + } + &__text { color: $color-gray; @include font(12, 24, 500, 0.8px); diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss index e78c940ea..411d5a987 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_welcome-docs.scss @@ -4,17 +4,7 @@ &__title { text-align: center; @include font(20, 28, 700); - margin: 0 0 32px 0; - } - - &__description { - text-align: center; - @include font(14, 22, 400); - font-style: italic; - - a { - color: $color-gray-700; - } + margin: 32px 0 32px 0; } &__wrapper { diff --git a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss b/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss index e98813d14..7878ef729 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_onboarding-global.scss @@ -1,7 +1,8 @@ body:has(.ppcp-r-container--onboarding) { background-color: #fff !important; - .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout, .wrap.woocommerce form > h2, #screen-meta-links { + .notice, .nav-tab-wrapper.woo-nav-tab-wrapper, .woocommerce-layout__header, .wrap.woocommerce form > h2, #screen-meta-links { display: none !important; + visibility: hidden; } } diff --git a/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss b/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss index 5a00adc9d..cfef2e04f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_onboarding.scss @@ -1,6 +1,7 @@ @import './onboarding/step-welcome'; @import './onboarding/step-business'; @import './onboarding/step-products'; +@import './onboarding/step-payment-methods'; .ppcp-r-tabs.onboarding, .ppcp-r-container--onboarding { diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index cc8ec5a26..8cf4fe593 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -49,6 +49,17 @@ } } + .ppcp-r__checkbox { + .components-flex { + gap: 12px; + } + + label { + @include font(13, 20, 400); + color: $color-blueberry; + } + } + &__description { @include font(13, 20, 400); color: $color-blueberry; diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss new file mode 100644 index 000000000..4ab630733 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-payment-methods.scss @@ -0,0 +1,38 @@ +.ppcp-r-page-optional-payment-methods { + .ppcp-r-select-box:first-child { + .ppcp-r-select-box__title { + margin-bottom: 20px; + } + } +} + +.ppcp-r-optional-payment-methods { + &__wrapper { + .ppcp-r-badge-box { + margin: 0 0 24px 0; + &:last-child { + margin: 0; + } + } + + .ppcp-r-badge-box__description { + margin: 12px 0 0 0; + @include font(14, 20, 400); + } + } + + &__description { + margin: 32px 0 0 0; + text-align: center; + @include font(14, 22, 400); + font-style: italic; + + a { + color: $color-gray-700; + } + } + + &__separator { + margin: 0 0 24px 0; + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss index fec5f3483..3399a1bc9 100644 --- a/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss +++ b/modules/ppcp-settings/resources/css/components/screens/onboarding/_step-welcome.scss @@ -16,27 +16,22 @@ text-align: center; } - .ppcp-r-page-welcome-or-separator { - 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 { margin: 0 0 24px 0; } + .ppcp-r-toggle-block__toggled-content > button{ @include small-button; color: $color-white; border: none; } + .client-id-error { + color: #cc1818; + margin: -16px 0 24px; + @include font(11, 16, 450); + } + .onboarding-advanced-options { max-width: 800px; } diff --git a/modules/ppcp-settings/resources/css/components/screens/overview/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-styling.scss new file mode 100644 index 000000000..c75d6a4c9 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/overview/_tab-styling.scss @@ -0,0 +1,121 @@ +.ppcp-r-styling { + display: flex; + border: 1px solid $color-gray-200; + border-radius: 8px; + overflow: hidden; + + &__section:not(:last-child) { + border-bottom: 1px solid black; + padding-bottom: 24px; + margin-bottom: 28px; + border-bottom: 1px solid $color-gray-600; + } + + &__main-title { + @include font(14, 20, 600); + color: $color-gray-800; + margin: 0 0 8px 0; + display: block; + } + + &__description { + @include font(13, 20, 400); + color: $color-gray-800; + margin: 0 0 18px 0; + } + + &__settings { + width: 422px; + background-color: $color-white; + padding: 48px; + } + + &__preview { + width: calc(100% - 422px); + background-color: #FAF8F5; + display: flex; + align-items: center; + + &-inner { + width: 100%; + padding: 24px; + } + } + + &__section--rc { + .ppcp-r-styling__title { + @include font(13, 20, 600); + color: $color-black; + display: block; + margin: 0 0 18px 0; + } + } + + &__section--empty.ppcp-r-styling__section { + padding-bottom: 0; + margin-bottom: 0; + border-bottom: none; + } + + &__select { + label { + @include font(13, 16, 600); + color: $color-black; + margin: 0; + text-transform: none; + } + + select { + @include font(13, 20, 400); + } + } + + .ppcp-r__checkbox { + .components-checkbox-control { + &__label { + @include font(13, 20, 400); + color: $color-black; + } + } + + .components-flex { + gap: 12px; + } + } + + &__payment-method-checkboxes { + display: flex; + flex-direction: column; + gap: 24px; + } +} + +.ppcp-r { + &__horizontal-control { + .components-flex { + flex-direction: row; + justify-content: flex-start; + gap: 32px; + } + + + .components-radio-control { + &__option { + gap: 12px; + + input { + margin: 0; + } + + label { + @include font(13, 20, 400); + color: $color-black; + } + } + + input { + margin: 0; + } + } + } +} diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 8358238df..ba1bc6ba9 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -21,6 +21,7 @@ @import './components/reusable-components/welcome-docs'; @import './components/screens/onboarding'; @import './components/screens/settings'; + @import './components/screens/overview/tab-styling'; } @import './components/reusable-components/payment-method-modal'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index 05cc758db..23f01a09c 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,3 +1,4 @@ +import { useEffect } from '@wordpress/element'; import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; @@ -5,11 +6,33 @@ import { useState } from 'react'; const Accordion = ( { title, - initiallyOpen = false, + initiallyOpen = null, className = '', + id = '', children, } ) => { - const [ isOpen, setIsOpen ] = useState( initiallyOpen ); + const determineInitialState = () => { + if ( id && initiallyOpen === null ) { + return window.location.hash === `#${ id }`; + } + return !! initiallyOpen; + }; + + const [ isOpen, setIsOpen ] = useState( determineInitialState ); + + useEffect( () => { + const handleHashChange = () => { + if ( id && window.location.hash === `#${ id }` ) { + setIsOpen( true ); + } + }; + + window.addEventListener( 'hashchange', handleHashChange ); + + return () => { + window.removeEventListener( 'hashchange', handleHashChange ); + }; + }, [ id ] ); const toggleOpen = ( ev ) => { setIsOpen( ! isOpen ); @@ -26,7 +49,7 @@ const Accordion = ( { } return ( -

+
- + { } value={ clientId } onChange={ setClientId } + className={ + clientId && ! isValidClientId ? 'has-error' : '' + } /> + { clientId && ! isValidClientId && ( +

+ { __( + 'Please enter a valid Client ID', + 'woocommerce-paypal-payments' + ) } +

+ ) } { onChange={ setClientSecret } type="password" /> -
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js similarity index 91% rename from modules/ppcp-settings/resources/js/Components/ReusableComponents/Navigation.js rename to modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js index 18b13e976..5a1da25cb 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/Navigation.js @@ -1,10 +1,8 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { - useOnboardingStepBusiness, - useOnboardingStepProducts, -} from '../../data'; -import data from '../../utils/data'; + +import { OnboardingHooks } from '../../../../data'; +import data from '../../../../utils/data'; const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { const isLastStep = () => currentStep + 1 === stepperOrder.length; @@ -24,8 +22,8 @@ const Navigation = ( { setStep, setCompleted, currentStep, stepperOrder } ) => { } }; - const { products, toggleProduct } = useOnboardingStepProducts(); - const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); + const { products } = OnboardingHooks.useProducts(); + const { isCasualSeller } = OnboardingHooks.useBusiness(); let navigationTitle = ''; let disabled = false; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js index a91eabb48..30cd52ffe 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Onboarding.js @@ -1,7 +1,7 @@ import Container from '../../ReusableComponents/Container'; -import { useOnboardingStep } from '../../../data'; +import { OnboardingHooks } from '../../../data'; import { getSteps } from './availableSteps'; -import Navigation from '../../ReusableComponents/Navigation'; +import Navigation from './Components/Navigation'; const getCurrentStep = ( requestedStep, steps ) => { const isValidStep = ( step ) => @@ -15,7 +15,7 @@ const getCurrentStep = ( requestedStep, steps ) => { }; const Onboarding = () => { - const { step, setStep, setCompleted, flags } = useOnboardingStep(); + const { step, setStep, setCompleted, flags } = OnboardingHooks.useSteps(); const steps = getSteps( flags ); const CurrentStepComponent = getCurrentStep( step, steps ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js index 670a65ae6..a223686ff 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepBusiness.js @@ -1,20 +1,14 @@ +import { __ } from '@wordpress/i18n'; + import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; import SelectBox from '../../ReusableComponents/SelectBox'; -import { __ } from '@wordpress/i18n'; -import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; -import { useOnboardingStepBusiness } from '../../../data'; -import { BUSINESS_TYPES } from '../../../data/constants'; +import { OnboardingHooks, BUSINESS_TYPES } from '../../../data'; const BUSINESS_RADIO_GROUP_NAME = 'business'; -const StepBusiness = ( { - setStep, - currentStep, - stepperOrder, - setCompleted, -} ) => { - const { isCasualSeller, setIsCasualSeller } = useOnboardingStepBusiness(); +const StepBusiness = ( {} ) => { + const { isCasualSeller, setIsCasualSeller } = OnboardingHooks.useBusiness(); const handleSellerTypeChange = ( value ) => { setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === value ); @@ -40,23 +34,22 @@ const StepBusiness = ( { />
- - + - + >
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js index 5dc18f619..5f63f923e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepCompleteSetup.js @@ -1,31 +1,23 @@ -import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import { __ } from '@wordpress/i18n'; import { Button, Icon } from '@wordpress/components'; -const StepCompleteSetup = ( { - setStep, - currentStep, - stepperOrder, - setCompleted, -} ) => { +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; + +const StepCompleteSetup = ( { setCompleted } ) => { const ButtonIcon = () => ( ( - - + ) } /> diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js new file mode 100644 index 000000000..a9d2f6b9e --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepPaymentMethods.js @@ -0,0 +1,78 @@ +import { __, sprintf } from '@wordpress/i18n'; + +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; +import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; +import SelectBox from '../../ReusableComponents/SelectBox'; +import { OnboardingHooks } from '../../../data'; +import OptionalPaymentMethods from '../../ReusableComponents/OptionalPaymentMethods/OptionalPaymentMethods'; + +const OPM_RADIO_GROUP_NAME = 'optional-payment-methods'; + +const StepPaymentMethods = ( {} ) => { + const { + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, + } = OnboardingHooks.useOptionalPaymentMethods(); + const pricesBasedDescription = sprintf( + // translators: %s: Link to PayPal REST application guide + __( + '1Prices based on domestic transactions as of October 25th, 2024. Click here for full pricing details.', + 'woocommerce-paypal-payments' + ), + 'https://woocommerce.com/document/woocommerce-paypal-payments/#manual-credential-input ' + ); + + return ( +
+ +
+ + + } + name={ OPM_RADIO_GROUP_NAME } + value={ true } + changeCallback={ setAreOptionalPaymentMethodsEnabled } + currentValue={ areOptionalPaymentMethodsEnabled } + type="radio" + > + + +

+
+
+ ); +}; + +export default StepPaymentMethods; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js index 9a705122d..cbd642327 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepProducts.js @@ -1,19 +1,14 @@ -import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import { __ } from '@wordpress/i18n'; + +import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import SelectBox from '../../ReusableComponents/SelectBox'; import SelectBoxWrapper from '../../ReusableComponents/SelectBoxWrapper'; -import { useOnboardingStepProducts } from '../../../data'; -import { PRODUCT_TYPES } from '../../../data/constants'; +import { OnboardingHooks, PRODUCT_TYPES } from '../../../data'; const PRODUCTS_CHECKBOX_GROUP_NAME = 'products'; -const StepProducts = ( { - setStep, - currentStep, - stepperOrder, - setCompleted, -} ) => { - const { products, toggleProduct } = useOnboardingStepProducts(); +const StepProducts = () => { + const { products, setProducts } = OnboardingHooks.useProducts(); return (
@@ -33,7 +28,7 @@ const StepProducts = ( { ) } name={ PRODUCTS_CHECKBOX_GROUP_NAME } value={ PRODUCT_TYPES.VIRTUAL } - changeCallback={ toggleProduct } + changeCallback={ setProducts } currentValue={ products } type="checkbox" > @@ -75,7 +70,7 @@ const StepProducts = ( { ) } name={ PRODUCTS_CHECKBOX_GROUP_NAME } value={ PRODUCT_TYPES.PHYSICAL } - changeCallback={ toggleProduct } + changeCallback={ setProducts } currentValue={ products } type="checkbox" > @@ -102,7 +97,7 @@ const StepProducts = ( { ) } name={ PRODUCTS_CHECKBOX_GROUP_NAME } value={ PRODUCT_TYPES.SUBSCRIPTIONS } - changeCallback={ toggleProduct } + changeCallback={ setProducts } currentValue={ products } type="checkbox" > diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js index e7e0ccab7..c94c84935 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js @@ -1,13 +1,13 @@ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import OnboardingHeader from '../../ReusableComponents/OnboardingHeader'; import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons'; import Separator from '../../ReusableComponents/Separator'; import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs'; +import AccordionSection from '../../ReusableComponents/AccordionSection'; import AdvancedOptionsForm from './Components/AdvancedOptionsForm'; -import AccordionSection from '../../ReusableComponents/AccordionSection'; const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { return ( @@ -48,7 +48,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => { isFastlane={ true } isPayLater={ true } storeCountry={ 'us' } - storeCurrency={ 'usd' } + storeCurrency={ 'USD' } /> { 'woocommerce-paypal-payments' ) } className="onboarding-advanced-options" - initiallyOpen={ false } + id="advanced-options" > diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js index a5555180d..7e8ea1556 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js @@ -1,6 +1,7 @@ import StepWelcome from './StepWelcome'; import StepBusiness from './StepBusiness'; import StepProducts from './StepProducts'; +import StepPaymentMethods from './StepPaymentMethods'; import StepCompleteSetup from './StepCompleteSetup'; export const getSteps = ( flags ) => { @@ -8,6 +9,7 @@ export const getSteps = ( flags ) => { StepWelcome, StepBusiness, StepProducts, + StepPaymentMethods, StepCompleteSetup, ]; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/Modals/ModalAcdc.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/Modals/ModalAcdc.js index 82a65ccf6..3d924dcbe 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/Modals/ModalAcdc.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/Modals/ModalAcdc.js @@ -1,15 +1,28 @@ import PaymentMethodModal from '../../../ReusableComponents/PaymentMethodModal'; import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -import { - PayPalRdb, - PayPalRdbWithContent, -} from '../../../ReusableComponents/Fields'; import { useState } from '@wordpress/element'; +import { RadioControl } from '@wordpress/components'; -const THREED_SECURE_GROUP_NAME = 'threed-secure'; const ModalAcdc = ( { setModalIsVisible } ) => { const [ threeDSecure, setThreeDSecure ] = useState( 'no-3d-secure' ); + const acdcOptions = [ + { + label: __( 'No 3D Secure', 'woocommerce-paypal-payments' ), + value: 'no-3d-secure', + }, + { + label: __( 'Only when required', 'woocommerce-paypal-payments' ), + value: 'only-required-3d-secure', + }, + { + label: __( + 'Always require 3D Secure', + 'woocommerce-paypal-payments' + ), + value: 'always-3d-secure', + }, + ]; return ( { ) }

- - - -
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js index d37e9c721..1b471fe1e 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabSettings.js @@ -25,7 +25,6 @@ const TabSettings = () => { buttonLanguage: '', } ); const updateFormValue = ( key, value ) => { - console.log( key, value ); setSettings( { ...settings, [ key ]: value } ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js index c61b2b296..df2983aea 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Overview/TabStyling.js @@ -1,5 +1,338 @@ +import { __, sprintf } from '@wordpress/i18n'; +import { SelectControl, RadioControl } from '@wordpress/components'; +import { PayPalCheckboxGroup } from '../../ReusableComponents/Fields'; +import { useState, useMemo, useEffect } from '@wordpress/element'; +import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; + +import { + defaultLocationSettings, + paymentMethodOptions, + colorOptions, + shapeOptions, + buttonLayoutOptions, + buttonLabelOptions, +} from '../../../data/settings/tab-styling-data'; + const TabStyling = () => { - return
Styling tab
; + const [ location, setLocation ] = useState( 'cart' ); + const [ canRender, setCanRender ] = useState( false ); + const [ locationSettings, setLocationSettings ] = useState( { + ...defaultLocationSettings, + } ); + + // Sometimes buttons won't render. This fixes the timing problem. + useEffect( () => { + const handleDOMContentLoaded = () => setCanRender( true ); + if ( + document.readyState === 'interactive' || + document.readyState === 'complete' + ) { + handleDOMContentLoaded(); + } else { + document.addEventListener( + 'DOMContentLoaded', + handleDOMContentLoaded + ); + } + }, [] ); + + const currentLocationSettings = useMemo( () => { + return locationSettings[ location ]; + }, [ location, locationSettings ] ); + + const locationOptions = useMemo( () => { + return Object.keys( locationSettings ).reduce( + ( locationOptionsData, key ) => { + locationOptionsData.push( { + value: locationSettings[ key ].value, + label: locationSettings[ key ].label, + } ); + + return locationOptionsData; + }, + [] + ); + }, [] ); + + const updateButtonSettings = ( key, value ) => { + setLocationSettings( { + ...locationSettings, + [ location ]: { + ...currentLocationSettings, + settings: { + ...currentLocationSettings.settings, + [ key ]: value, + }, + }, + } ); + }; + + const updateButtonStyle = ( key, value ) => { + setLocationSettings( { + ...locationSettings, + [ location ]: { + ...currentLocationSettings, + settings: { + ...currentLocationSettings.settings, + style: { + ...currentLocationSettings.settings.style, + [ key ]: value, + }, + }, + }, + } ); + }; + + if ( ! canRender ) { + return <>; + } + + return ( +
+
+ + + + + + + + + + +
+
+
+ +
+
+
+ ); +}; + +const TabStylingSection = ( props ) => { + let sectionTitleClassName = 'ppcp-r-styling__section'; + + if ( props?.className ) { + sectionTitleClassName += ` ${ props.className }`; + } + + return ( +
+ { props.title } + { props?.description && ( +

+ ) } + { props.children } +

+ ); +}; + +const SectionIntro = () => { + const buttonStyleDescription = sprintf( + // translators: %s: Link to Classic checkout page + __( + 'Customize the appearance of the PayPal smart buttons on the [MISSING LINK]Classic Checkout page. Checkout Buttons must be enabled to display the PayPal gateway on the Checkout page.' + ), + '#' + ); + return ( + + ); +}; + +const SectionLocations = ( { locationOptions, location, setLocation } ) => { + return ( + + setLocation( newLocation ) } + label={ __( 'Locations', 'woocommerce-paypal-payments' ) } + options={ locationOptions } + /> + + ); +}; + +const SectionPaymentMethods = ( { + locationSettings, + updateButtonSettings, +} ) => { + return ( + +
+ + updateButtonSettings( 'paymentMethods', newValue ) + } + currentValue={ locationSettings.settings.paymentMethods } + /> +
+
+ ); +}; + +const SectionButtonLayout = ( { locationSettings, updateButtonStyle } ) => { + const buttonLayoutIsAllowed = + locationSettings.settings.style?.layout && + locationSettings.settings.style?.tagline === false; + return ( + buttonLayoutIsAllowed && ( + + + updateButtonStyle( 'layout', newValue ) + } + selected={ locationSettings.settings.style.layout } + options={ buttonLayoutOptions } + /> + + ) + ); +}; + +const SectionButtonShape = ( { locationSettings, updateButtonStyle } ) => { + return ( + + + updateButtonStyle( 'shape', newValue ) + } + selected={ locationSettings.settings.style.shape } + options={ shapeOptions } + /> + + ); +}; + +const SectionButtonLabel = ( { locationSettings, updateButtonStyle } ) => { + return ( + + + updateButtonStyle( 'label', newValue ) + } + value={ locationSettings.settings.style.label } + label={ __( 'Button Label', 'woocommerce-paypal-payments' ) } + options={ buttonLabelOptions } + /> + + ); +}; + +const SectionButtonColor = ( { locationSettings, updateButtonStyle } ) => { + return ( + + + updateButtonStyle( 'color', newValue ) + } + value={ locationSettings.settings.style.color } + options={ colorOptions } + /> + + ); +}; + +const SectionButtonTagline = ( { locationSettings, updateButtonStyle } ) => { + const taglineIsAllowed = + locationSettings.settings.style.hasOwnProperty( 'tagline' ) && + locationSettings.settings.style?.layout === 'horizontal'; + + return ( + taglineIsAllowed && ( + + { + updateButtonStyle( 'tagline', newValue ); + } } + currentValue={ locationSettings.settings.style.tagline } + /> + + ) + ); +}; + +const SectionButtonPreview = ( { locationSettings } ) => { + return ( + + + Error + + + ); }; export default TabStyling; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js index 2f2357951..e0634343c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js @@ -1,9 +1,9 @@ -import { useOnboardingStep } from '../../data'; +import { OnboardingHooks } from '../../data'; import Onboarding from './Onboarding/Onboarding'; import SettingsScreen from './SettingsScreen'; const Settings = () => { - const onboardingProgress = useOnboardingStep(); + const onboardingProgress = OnboardingHooks.useSteps(); if ( ! onboardingProgress.isReady ) { // TODO: Use better loading state indicator. diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js new file mode 100644 index 000000000..47de76afe --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/action-types.js @@ -0,0 +1,19 @@ +/** + * Action Types: Define unique identifiers for actions across all store modules. + * + * @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', +}; diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js new file mode 100644 index 000000000..619aaca5f --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/actions.js @@ -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; +}; diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js new file mode 100644 index 000000000..c7ea9b4c1 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/constants.js @@ -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'; diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js new file mode 100644 index 000000000..6de513e0b --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/controls.js @@ -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; + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js new file mode 100644 index 000000000..8be3857b0 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -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, + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/common/index.js b/modules/ppcp-settings/resources/js/data/common/index.js new file mode 100644 index 000000000..28c162f98 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/index.js @@ -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 }; diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js new file mode 100644 index 000000000..3f822468b --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/reducer.js @@ -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; diff --git a/modules/ppcp-settings/resources/js/data/common/resolvers.js b/modules/ppcp-settings/resources/js/data/common/resolvers.js new file mode 100644 index 000000000..ceebca53f --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/resolvers.js @@ -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' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js new file mode 100644 index 000000000..14334fcf3 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/common/selectors.js @@ -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; +}; diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js index e6f8f9de5..5654ad476 100644 --- a/modules/ppcp-settings/resources/js/data/constants.js +++ b/modules/ppcp-settings/resources/js/data/constants.js @@ -1,6 +1,3 @@ -export const NAMESPACE = '/wc/v3/wc_paypal'; -export const STORE_NAME = 'wc/paypal'; - export const BUSINESS_TYPES = { CASUAL_SELLER: 'casual_seller', BUSINESS: 'business', diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js new file mode 100644 index 000000000..b292d1920 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -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(); + }; +}; diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js index 1c95c4261..274aac790 100644 --- a/modules/ppcp-settings/resources/js/data/index.js +++ b/modules/ppcp-settings/resources/js/data/index.js @@ -1,7 +1,16 @@ -import { STORE_NAME } from './constants'; -import { initStore } from './store'; +import { addDebugTools } from './debug'; +import * as Onboarding from './onboarding'; +import * as Common from './common'; -initStore(); +Onboarding.initStore(); +Common.initStore(); -export const WC_PAYPAL_STORE_NAME = STORE_NAME; -export * from './onboarding/hooks'; +export const OnboardingHooks = Onboarding.hooks; +export const CommonHooks = Common.hooks; + +export const OnboardingStoreName = Onboarding.STORE_NAME; +export const CommonStoreName = Common.STORE_NAME; + +export * from './constants'; + +addDebugTools( window.ppcpSettings, [ Onboarding, Common ] ); diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js index 39472e2ff..2e16f8468 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js @@ -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. - SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY', - SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING', - SET_MANUAL_CONNECTION_BUSY: 'SET_MANUAL_CONNECTION_BUSY', + SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT', // Persistent data. - SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED', - SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS', - SET_ONBOARDING_STEP: 'SET_ONBOARDING_STEP', - SET_SANDBOX_MODE: 'SET_SANDBOX_MODE', - SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE', - SET_CLIENT_ID: 'SET_CLIENT_ID', - SET_CLIENT_SECRET: 'SET_CLIENT_SECRET', - SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER', - SET_PRODUCTS: 'SET_PRODUCTS', + SET_PERSISTENT: 'ONBOARDING:SET_PERSISTENT', + RESET: 'ONBOARDING:RESET', + HYDRATE: 'ONBOARDING:HYDRATE', + + // Controls - always start with "DO_". + DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA', }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 09229e63e..dcf401995 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -1,235 +1,116 @@ +/** + * 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 { apiFetch } from '@wordpress/data-controls'; + 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. * - * @return {{type: string}} The action. + * @return {Action} The action. */ -export const resetOnboarding = () => { - 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, - }; -}; +export const reset = () => ( { type: ACTION_TYPES.RESET } ); /** * Persistent. Set the full onboarding details, usually during app initialization. * * @param {{data: {}, flags?: {}}} payload - * @return {{type: string, payload}} The action. + * @return {Action} The action. */ -export const setOnboardingDetails = ( payload ) => { - return { - type: ACTION_TYPES.SET_ONBOARDING_DETAILS, - payload, - }; -}; +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 }, +} ); /** * Persistent.Set the "onboarding completed" flag which shows or hides the wizard. * * @param {boolean} completed - * @return {{type: string, payload}} The action. + * @return {Action} The action. */ -export const setCompleted = ( completed ) => { - return { - type: ACTION_TYPES.SET_ONBOARDING_COMPLETED, - completed, - }; -}; +export const setCompleted = ( completed ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { completed }, +} ); /** * Persistent. Sets the onboarding wizard to a new step. * * @param {number} step - * @return {{type: string, step}} An action. + * @return {Action} The action. */ -export const setOnboardingStep = ( step ) => { - return { - type: ACTION_TYPES.SET_ONBOARDING_STEP, - step, - }; -}; - -/** - * 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, - }; -}; +export const setStep = ( step ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { step }, +} ); /** * Persistent. Sets the "isCasualSeller" value. * * @param {boolean} isCasualSeller - * @return {{type: string, isCasualSeller}} The action. + * @return {Action} The action. */ -export const setIsCasualSeller = ( isCasualSeller ) => { - return { - type: ACTION_TYPES.SET_IS_CASUAL_SELLER, - isCasualSeller, - }; -}; +export const setIsCasualSeller = ( isCasualSeller ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { isCasualSeller }, +} ); + +/** + * Persistent. Sets the "areOptionalPaymentMethodsEnabled" value. + * + * @param {boolean} areOptionalPaymentMethodsEnabled + * @return {Action} The action. + */ +export const setAreOptionalPaymentMethodsEnabled = ( + areOptionalPaymentMethodsEnabled +) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { areOptionalPaymentMethodsEnabled }, +} ); /** * Persistent. Sets the "products" array. * * @param {string[]} products - * @return {{type: string, products}} The action. + * @return {Action} The action. */ -export const setProducts = ( products ) => { - return { - type: ACTION_TYPES.SET_PRODUCTS, - products, - }; +export const setProducts = ( products ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { 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; -} diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js new file mode 100644 index 000000000..4b33c6701 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js @@ -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'; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/controls.js b/modules/ppcp-settings/resources/js/data/onboarding/controls.js new file mode 100644 index 000000000..30f1cce48 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/onboarding/controls.js @@ -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 ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js index ff9052d69..4ae5bd947 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js @@ -1,163 +1,115 @@ -import { useSelect, useDispatch } from '@wordpress/data'; -import apiFetch from '@wordpress/api-fetch'; -import { NAMESPACE, PRODUCT_TYPES, STORE_NAME } from '../constants'; -import { getFlags } from './selectors'; +/** + * 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 + */ -const useOnboardingDetails = () => { +import { useSelect, useDispatch } from '@wordpress/data'; + +import { PRODUCT_TYPES } from '../constants'; +import { STORE_NAME } from './constants'; + +const useTransient = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).transientData()?.[ key ], + [ key ] + ); + +const usePersistent = ( key ) => + useSelect( + ( select ) => select( STORE_NAME ).persistentData()?.[ key ], + [ key ] + ); + +const useHooks = () => { const { persist, - setOnboardingStep, + setStep, setCompleted, - setSandboxMode, - setManualConnectionMode, - setClientId, - setClientSecret, setIsCasualSeller, + setAreOptionalPaymentMethodsEnabled, setProducts, } = useDispatch( STORE_NAME ); - // Transient accessors. - const isSaving = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isSaving; - }, [] ); - - const isReady = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isReady; - } ); - - const isManualConnectionBusy = useSelect( ( select ) => { - return select( STORE_NAME ).getTransientData().isManualConnectionBusy; - }, [] ); - // Read-only flags. - const flags = useSelect( ( select ) => { - return select( STORE_NAME ).getFlags(); - } ); + const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] ); + + // Transient accessors. + const isReady = useTransient( 'isReady' ); // Persistent accessors. - const step = useSelect( ( select ) => { - return select( STORE_NAME ).getPersistentData().step || 0; - } ); + const step = usePersistent( 'step' ); + const completed = usePersistent( 'completed' ); + const isCasualSeller = usePersistent( 'isCasualSeller' ); + const areOptionalPaymentMethodsEnabled = usePersistent( + 'areOptionalPaymentMethodsEnabled' + ); + const products = usePersistent( 'products' ); - const completed = useSelect( ( select ) => { - 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 ) => { + const savePersistent = async ( setter, value ) => { setter( value ); await persist(); }; 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, + isReady, + step, + setStep: ( value ) => { + return savePersistent( setStep, value ); + }, + completed, + setCompleted: ( state ) => { + return savePersistent( setCompleted, state ); + }, + isCasualSeller, + setIsCasualSeller: ( value ) => { + return savePersistent( setIsCasualSeller, value ); + }, + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled: ( value ) => { + return savePersistent( setAreOptionalPaymentMethodsEnabled, value ); + }, + products, + setProducts: ( activeProducts ) => { + const validProducts = activeProducts.filter( ( item ) => + Object.values( PRODUCT_TYPES ).includes( item ) + ); + return savePersistent( setProducts, validProducts ); + }, }; }; -export const useOnboardingStepWelcome = () => { - const { - 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(); +export const useBusiness = () => { + const { isCasualSeller, setIsCasualSeller } = useHooks(); return { isCasualSeller, setIsCasualSeller }; }; -export const useOnboardingStepProducts = () => { - const { products, toggleProduct } = useOnboardingDetails(); +export const useProducts = () => { + const { products, setProducts } = useHooks(); - return { products, toggleProduct }; + return { products, setProducts }; }; -export const useOnboardingStep = () => { - const { isReady, step, setStep, completed, setCompleted, flags } = - useOnboardingDetails(); - - return { isReady, step, setStep, completed, setCompleted, flags }; -}; - -export const useManualConnect = () => { - const { connectViaIdAndSecret } = useDispatch( STORE_NAME ); +export const useOptionalPaymentMethods = () => { + const { + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, + } = useHooks(); return { - connectManual: connectViaIdAndSecret, + areOptionalPaymentMethodsEnabled, + setAreOptionalPaymentMethodsEnabled, }; }; + +export const useSteps = () => { + const { flags, isReady, step, setStep, completed, setCompleted } = + useHooks(); + + return { flags, isReady, step, setStep, completed, setCompleted }; +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js index 0b07abf46..28c162f98 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/index.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js @@ -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 * as selectors from './selectors'; import * as actions from './actions'; -import * as resolvers from './resolvers'; +import * as hooks from './hooks'; +import { resolvers } from './resolvers'; +import { controls } from './controls'; -export { reducer, selectors, actions, resolvers }; +export const initStore = () => { + const store = createReduxStore( STORE_NAME, { + reducer, + controls: { ...wpControls, ...controls }, + actions, + selectors, + resolvers, + } ); + + register( store ); +}; + +export { hooks, selectors, STORE_NAME }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js index 5c1f59263..176d4875d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js @@ -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'; -const defaultState = { - isReady: false, - isSaving: false, - isManualConnectionBusy: false, +// Store structure. - // Data persisted to the server. - data: { - completed: false, - step: 0, - useSandbox: false, - useManualConnection: false, - clientId: '', - clientSecret: '', - isCasualSeller: null, // null value will uncheck both options in the UI. - products: [], - }, +const defaultTransient = { + isReady: false, // Read only values, provided by the server. flags: { @@ -25,83 +23,41 @@ const defaultState = { }, }; -export const onboardingReducer = ( - state = defaultState, - { type, ...action } -) => { - const setTransient = ( changes ) => { - const { data, ...transientChanges } = changes; - return { ...state, ...transientChanges }; - }; - - const setPersistent = ( changes ) => { - const validChanges = Object.keys( changes ).reduce( ( acc, key ) => { - if ( key in defaultState.data ) { - acc[ key ] = changes[ key ]; - } - return acc; - }, {} ); - - return { - ...state, - data: { ...state.data, ...validChanges }, - }; - }; - - switch ( type ) { - // Reset store to initial state. - case ACTION_TYPES.RESET_ONBOARDING: - 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; - } +const defaultPersistent = { + completed: false, + step: 0, + isCasualSeller: null, // null value will uncheck both options in the UI. + areOptionalPaymentMethodsEnabled: true, + products: [], }; +// 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; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js index 18f2a7528..bf7828dd3 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js @@ -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 { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; -import { NAMESPACE } from '../constants'; -import { setIsReady, setOnboardingDetails } from './actions'; -/** - * Retrieve settings from the site's REST API. - */ -export function* getPersistentData() { - const path = `${ NAMESPACE }/onboarding`; +import { STORE_NAME, REST_HYDRATE_PATH } from './constants'; - try { - const result = yield apiFetch( { path } ); - yield setOnboardingDetails( result ); - yield setIsReady( true ); - } catch ( e ) { - yield dispatch( 'core/notices' ).createErrorNotice( - __( - 'Error retrieving onboarding details.', - 'woocommerce-paypal-payments' - ) - ); - } -} +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 onboarding details.', + 'woocommerce-paypal-payments' + ) + ); + } + }, +}; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js index b7721b992..d4d57ef4d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js @@ -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 getOnboardingState = ( state ) => { - if ( ! state ) { - return EMPTY_OBJ; - } +const getState = ( state ) => state || EMPTY_OBJ; - return state.onboarding || EMPTY_OBJ; +export const persistentData = ( state ) => { + return getState( state ).data || EMPTY_OBJ; }; -export const getPersistentData = ( state ) => { - return getOnboardingState( state ).data || EMPTY_OBJ; -}; - -export const getTransientData = ( state ) => { - const { data, flags, ...transientState } = getOnboardingState( state ); +export const transientData = ( state ) => { + const { data, flags, ...transientState } = getState( state ); return transientState || EMPTY_OBJ; }; -export const getFlags = ( state ) => { - return getOnboardingState( state ).flags || EMPTY_OBJ; +export const flags = ( state ) => { + return getState( state ).flags || EMPTY_OBJ; }; diff --git a/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js b/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js new file mode 100644 index 000000000..92bb32015 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/settings/tab-styling-data.js @@ -0,0 +1,136 @@ +import { __ } from '@wordpress/i18n'; + +const cartAndExpressCheckoutSettings = { + paymentMethods: [], + style: { + shape: 'pill', + label: 'paypal', + color: 'gold', + }, +}; + +const settings = { + paymentMethods: [], + style: { + layout: 'vertical', + shape: cartAndExpressCheckoutSettings.style.shape, + label: cartAndExpressCheckoutSettings.style.label, + color: cartAndExpressCheckoutSettings.style.color, + tagline: false, + }, +}; + +export const defaultLocationSettings = { + cart: { + value: 'cart', + label: __( 'Cart', 'woocommerce-paypal-payments' ), + settings: { ...cartAndExpressCheckoutSettings }, + }, + 'classic-checkout': { + value: 'classic-checkout', + label: __( 'Classic Checkout', 'woocommerce-paypal-payments' ), + settings: { ...settings }, + }, + 'express-checkout': { + value: 'express-checkout', + label: __( 'Express Checkout', 'woocommerce-paypal-payments' ), + settings: { ...cartAndExpressCheckoutSettings }, + }, + 'mini-cart': { + value: 'mini-cart', + label: __( 'Mini Cart', 'woocommerce-paypel-payements' ), + settings: { ...settings }, + }, + 'product-page': { + value: 'product-page', + label: __( 'Product Page', 'woocommerce-paypal-payments' ), + settings: { ...settings }, + }, +}; + +export const paymentMethodOptions = [ + { + value: 'venmo', + label: __( 'Venmo', 'woocommerce-paypal-payments' ), + }, + { + value: 'paylater', + label: __( 'Pay Later', 'woocommerce-paypal-payments' ), + }, + { + value: 'card', + label: __( 'Debit or Credit Card', 'woocommerce-paypal-payments' ), + }, + { + value: 'googlepay', + label: __( 'Google Pay', 'woocommerce-paypal-payments' ), + }, + { + value: 'applepay', + label: __( 'Apple Pay', 'woocommerce-paypal-payments' ), + }, +]; + +export const buttonLabelOptions = [ + { + value: 'paypal', + label: __( 'PayPal', 'woocommerce-paypal-payments' ), + }, + { + value: 'checkout', + label: __( 'Checkout', 'woocommerce-paypal-payments' ), + }, + { + value: 'buynow', + label: __( 'PayPal Buy Now', 'woocommerce-paypal-payments' ), + }, + { + value: 'pay', + label: __( 'Pay with PayPal', 'woocommerce-paypal-payments' ), + }, +]; + +export const colorOptions = [ + { + value: 'gold', + label: __( 'Gold (Recommended)', 'woocommerce-paypal-payments' ), + }, + { + value: 'blue', + label: __( 'Blue', 'woocommerce-paypal-payments' ), + }, + { + value: 'silver', + label: __( 'Silver', 'woocommerce-paypal-payments' ), + }, + { + value: 'black', + label: __( 'Black', 'woocommerce-paypal-payments' ), + }, + { + value: 'white', + label: __( 'White', 'woocommerce-paypal-payments' ), + }, +]; + +export const buttonLayoutOptions = [ + { + label: __( 'Vertical', 'woocommerce-paypal-payments' ), + value: 'vertical', + }, + { + label: __( 'Horizontal', 'woocommerce-paypal-payments' ), + value: 'horizontal', + }, +]; + +export const shapeOptions = [ + { + value: 'pill', + label: __( 'Pill', 'woocommerce-paypal-payments' ), + }, + { + value: 'rect', + label: __( 'Rectangle', 'woocommerce-paypal-payments' ), + }, +]; diff --git a/modules/ppcp-settings/resources/js/data/store.js b/modules/ppcp-settings/resources/js/data/store.js deleted file mode 100644 index a4acaf548..000000000 --- a/modules/ppcp-settings/resources/js/data/store.js +++ /dev/null @@ -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 */ -}; diff --git a/modules/ppcp-settings/resources/js/data/utils.js b/modules/ppcp-settings/resources/js/data/utils.js new file mode 100644 index 000000000..45c652862 --- /dev/null +++ b/modules/ppcp-settings/resources/js/data/utils.js @@ -0,0 +1,75 @@ +/** + * Updates an object with new values, filtering based on allowed keys. + * + * Helper method used by createSetters. + * + * @param {Object} oldObject The original object to update. + * @param {Object} newValues The new values to apply. + * @param {Object} allowedKeys An object whose keys define the allowed keys to update. + * @return {Object} A new object with the allowed updates applied. + */ +const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( { + ...oldObject, + ...Object.keys( newValues ).reduce( ( acc, key ) => { + if ( key in allowedKeys ) { + acc[ key ] = newValues[ key ]; + } + return acc; + }, {} ), +} ); + +/** + * Creates setter functions for updating state. + * + * Only properties that are present in the "defaultTransient" or "defaultPersistent" + * arguments can be updated by the setters. Make sure that the default state defines + * ALL possible properties. + * + * @param {Object} defaultTransient Object defining initial transient values. + * @param {Object} defaultPersistent Object defining initial persistent values. + * @return {[Function, Function]} An array containing setTransient and setPersistent functions. + */ +export const createSetters = ( defaultTransient, defaultPersistent ) => { + const setTransient = ( oldState, newValues = {} ) => + updateObject( oldState, newValues, defaultTransient ); + + const setPersistent = ( oldState, newValues = {} ) => ( { + ...oldState, + data: updateObject( oldState.data, newValues, defaultPersistent ), + } ); + + return [ setTransient, setPersistent ]; +}; + +/** + * Creates a reducer function with predefined action handlers. + * + * @param {Object} defaultTransient Object defining initial transient values. + * @param {Object} defaultPersistent Object defining initial persistent values. + * @param {Object} handlers An object mapping action types to handler functions. + * @return {Function} A reducer function. + */ +export const createReducer = ( + defaultTransient, + defaultPersistent, + handlers +) => { + if ( Object.hasOwnProperty.call( defaultTransient, 'data' ) ) { + throw new Error( + 'The transient state cannot contain a "data" property.' + ); + } + + const initialState = { + ...defaultTransient, + data: defaultPersistent, + }; + + return function reducer( state = initialState, action ) { + if ( Object.hasOwnProperty.call( handlers, action.type ) ) { + return handlers[ action.type ]( state, action.payload ?? {} ); + } + + return state; + }; +}; diff --git a/modules/ppcp-settings/resources/js/switchSettingsUi.js b/modules/ppcp-settings/resources/js/switchSettingsUi.js new file mode 100644 index 000000000..55a9e6a01 --- /dev/null +++ b/modules/ppcp-settings/resources/js/switchSettingsUi.js @@ -0,0 +1,32 @@ +document.addEventListener('DOMContentLoaded', () => { + const config = ppcpSwitchSettingsUi; + const button = document.querySelector('.button.button-settings-switch-ui'); + + if ( ! typeof config || !button) { + return; + } + + button.addEventListener('click', () => { + fetch(config.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: config.nonce, + }), + }) + .then((response) => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then((data) => { + window.location.reload(); + }) + .catch((error) => { + console.error('Error:', error); + }); + }); +}); diff --git a/modules/ppcp-settings/resources/js/utils/badgeBoxUtils.js b/modules/ppcp-settings/resources/js/utils/badgeBoxUtils.js new file mode 100644 index 000000000..60c4da274 --- /dev/null +++ b/modules/ppcp-settings/resources/js/utils/badgeBoxUtils.js @@ -0,0 +1,18 @@ +import { __ } from '@wordpress/i18n'; + +const generatePriceText = ( type, selectedCountryPrice, storeCurrency ) => { + if ( ! selectedCountryPrice || ! selectedCountryPrice[ type ] ) { + console.warn( `Invalid type or price data for: ${ type }` ); + return ''; + } + + const percentage = selectedCountryPrice[ type ].toFixed( 2 ); + const fixedFee = `${ selectedCountryPrice.currencySymbol }${ selectedCountryPrice.fixedFee }`; + + return __( + `from ${ percentage }% + ${ fixedFee } ${ storeCurrency }1`, + 'woocommerce-paypal-payments' + ); +}; + +export default generatePriceText; diff --git a/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js new file mode 100644 index 000000000..193efd584 --- /dev/null +++ b/modules/ppcp-settings/resources/js/utils/countryPriceInfo.js @@ -0,0 +1,75 @@ +export const countryPriceInfo = { + us: { + currencySymbol: '$', + fixedFee: 0.49, + checkout: 3.49, + ccf: 2.59, + dw: 2.59, + apm: 2.59, + fastlane: 2.59, + standardCardFields: 2.99, + }, + uk: { + currencySymbol: '£', + fixedFee: 0.3, + checkout: 2.9, + ccf: 1.2, + dw: 1.2, + apm: 1.2, + standardCardFields: 1.2, + }, + ca: { + currencySymbol: '$', + fixedFee: 0.3, + checkout: 2.9, + ccf: 2.7, + dw: 2.7, + apm: 2.9, + standardCardFields: 2.9, + }, + au: { + currencySymbol: '$', + fixedFee: 0.3, + checkout: 2.6, + ccf: 1.75, + dw: 1.75, + apm: 2.6, + standardCardFields: 2.6, + }, + fr: { + currencySymbol: '€', + fixedFee: 0.35, + checkout: 2.9, + ccf: 1.2, + dw: 1.2, + apm: 1.2, + standardCardFields: 1.2, + }, + it: { + currencySymbol: '€', + fixedFee: 0.35, + checkout: 3.4, + ccf: 1.2, + dw: 1.2, + apm: 1.2, + standardCardFields: 1.2, + }, + de: { + currencySymbol: '€', + fixedFee: 0.39, + checkout: 2.99, + ccf: 2.99, + dw: 2.99, + apm: 2.99, + standardCardFields: 2.99, + }, + es: { + currencySymbol: '€', + fixedFee: 0.35, + checkout: 2.9, + ccf: 1.2, + dw: 1.2, + apm: 1.2, + standardCardFields: 1.2, + }, +}; diff --git a/modules/ppcp-settings/resources/js/utils/window.js b/modules/ppcp-settings/resources/js/utils/window.js new file mode 100644 index 000000000..165874302 --- /dev/null +++ b/modules/ppcp-settings/resources/js/utils/window.js @@ -0,0 +1,42 @@ +/** + * Opens the provided URL, preferably in a popup window. + * + * Popups are usually only supported on desktop devices, when the browser is not in fullscreen mode. + * + * @param {string} url + * @param {Object} options + * @param {string} options.name + * @param {number} options.width + * @param {number} options.height + * @param {boolean} options.resizeable + * @return {null|Window} Popup window instance, or null. + */ +export const openPopup = ( + url, + { name = '_blank', width = 450, height = 720, resizeable = false } = {} +) => { + width = Math.max( 100, Math.min( window.screen.width - 40, width ) ); + height = Math.max( 100, Math.min( window.screen.height - 40, height ) ); + + const left = ( window.screen.width - width ) / 2; + const top = ( window.screen.height - height ) / 2; + + const features = [ + `width=${ width }`, + `height=${ height }`, + `left=${ left }`, + `top=${ top }`, + `resizable=${ resizeable ? 'yes' : 'no' }`, + `scrollbars=yes`, + `status=no`, + ]; + + const popup = window.open( url, name, features.join( ',' ) ); + + if ( popup && ! popup.closed ) { + popup.focus(); + return popup; + } + + return null; +}; diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php index 80df22370..d213aa4c0 100644 --- a/modules/ppcp-settings/services.php +++ b/modules/ppcp-settings/services.php @@ -9,10 +9,17 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; -use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings; +use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile; +use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\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( 'settings.url' => static function ( ContainerInterface $container ) : string { @@ -43,14 +50,29 @@ return array( $can_use_card_payments ); }, + 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings { + return new GeneralSettings(); + }, + 'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings { + return new CommonSettings(); + }, 'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint { 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 { return new ConnectManualRestEndpoint( $container->get( 'api.paypal-host-production' ), $container->get( 'api.paypal-host-sandbox' ), - $container->get( 'woocommerce.logger.woocommerce' ) + $container->get( 'woocommerce.logger.woocommerce' ), + $container->get( 'settings.data.general' ) + ); + }, + '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 { @@ -109,4 +131,39 @@ return array( return in_array( $country, $eligible_countries, true ); }, + 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache { + return new Cache( 'ppcp-paypal-signup-link' ); + }, + 'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array { + // Define available environments. + $environments = array( + 'production' => array( + 'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-production' ), + ), + 'sandbox' => array( + 'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-sandbox' ), + ), + ); + + $generators = array(); + + // Instantiate URL generators for each environment. + foreach ( $environments as $environment => $config ) { + $generators[ $environment ] = new ConnectionUrlGenerator( + $config['partner_referrals'], + $container->get( 'api.repository.partner-referrals-data' ), + $container->get( 'settings.service.signup-link-cache' ), + $environment, + $container->get( 'woocommerce.logger.woocommerce' ) + ); + } + + return $generators; + }, + 'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint { + return new SwitchSettingsUiEndpoint( + $container->get( 'woocommerce.logger.woocommerce' ), + $container->get( 'button.request-data' ), + ); + }, ); diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php new file mode 100644 index 000000000..8f7dd1ddf --- /dev/null +++ b/modules/ppcp-settings/src/Data/CommonSettings.php @@ -0,0 +1,119 @@ + 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 ); + } +} diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index e1f9e16b4..03a0a7d1c 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -64,14 +64,11 @@ class OnboardingProfile extends AbstractDataModel { */ protected function get_defaults() : array { return array( - 'completed' => false, - 'step' => 0, - 'use_sandbox' => false, - 'use_manual_connection' => false, - 'client_id' => '', - 'client_secret' => '', - 'is_casual_seller' => null, - 'products' => array(), + 'completed' => false, + 'step' => 0, + 'is_casual_seller' => null, + 'are_optional_payment_methods_enabled' => true, + 'products' => array(), ); } @@ -113,78 +110,6 @@ class OnboardingProfile extends AbstractDataModel { $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. * @@ -203,6 +128,15 @@ class OnboardingProfile extends AbstractDataModel { $this->data['is_casual_seller'] = $casual_seller; } + /** + * Sets the optional payment methods flag. + * + * @param bool|null $are_optional_payment_methods_enabled Whether the PayPal optional payment methods are enabled. + */ + public function set_are_optional_payment_methods_enabled( ?bool $are_optional_payment_methods_enabled ) : void { + $this->data['are_optional_payment_methods_enabled'] = $are_optional_payment_methods_enabled; + } + /** * Gets the active product types for this store. * diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php new file mode 100644 index 000000000..c7345148e --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -0,0 +1,133 @@ + 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(); + } +} diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php index af81b90ca..7046342a2 100644 --- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php @@ -10,17 +10,16 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; use Exception; -use Psr\Log\LoggerInterface; -use RuntimeException; use stdClass; +use RuntimeException; +use Psr\Log\LoggerInterface; +use WP_REST_Request; +use WP_REST_Response; +use WP_REST_Server; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders; use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache; -use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; -use WP_REST_Server; -use WP_REST_Response; -use WP_REST_Request; +use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; /** * REST controller for connection via manual credentials input. @@ -55,6 +54,13 @@ class ConnectManualRestEndpoint extends RestEndpoint { */ protected $rest_base = 'connect_manual'; + /** + * Settings instance. + * + * @var GeneralSettings + */ + private $settings = null; + /** * Field mapping for request. * @@ -78,19 +84,21 @@ class ConnectManualRestEndpoint extends RestEndpoint { /** * 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 LoggerInterface $logger The logger. + * @param LoggerInterface $logger The logger. + * @param GeneralSettings $settings Settings instance. */ public function __construct( string $live_host, string $sandbox_host, - LoggerInterface $logger + LoggerInterface $logger, + GeneralSettings $settings ) { - $this->live_host = $live_host; $this->sandbox_host = $sandbox_host; $this->logger = $logger; + $this->settings = $settings; } /** @@ -126,47 +134,52 @@ class ConnectManualRestEndpoint extends RestEndpoint { $use_sandbox = (bool) ( $data['use_sandbox'] ?? false ); if ( empty( $client_id ) || empty( $client_secret ) ) { - return rest_ensure_response( - array( - 'success' => false, - 'message' => 'No client ID or secret provided.', - ) - ); + return $this->return_error( 'No client ID or secret provided.' ); } try { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); } catch ( Exception $exception ) { - return rest_ensure_response( - array( - 'success' => false, - 'message' => $exception->getMessage(), - ) - ); - + return $this->return_error( $exception->getMessage() ); } - $result = array( - 'merchantId' => $payee->merchant_id, - 'email' => $payee->email_address, - 'success' => true, - ); + if ( $use_sandbox ) { + $this->settings->set_is_sandbox( true ); + $this->settings->set_sandbox_client_id( $client_id ); + $this->settings->set_sandbox_client_secret( $client_secret ); + $this->settings->set_sandbox_merchant_id( $payee->merchant_id ); + $this->settings->set_sandbox_merchant_email( $payee->email_address ); + } else { + $this->settings->set_is_sandbox( false ); + $this->settings->set_live_client_id( $client_id ); + $this->settings->set_live_client_secret( $client_secret ); + $this->settings->set_live_merchant_id( $payee->merchant_id ); + $this->settings->set_live_merchant_email( $payee->email_address ); + } + $this->settings->save(); - return rest_ensure_response( $result ); + return $this->return_success( + array( + 'merchantId' => $payee->merchant_id, + 'email' => $payee->email_address, + ) + ); } /** * Retrieves the payee object with the merchant data * by creating a minimal PayPal order. * - * @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. * * phpcs:disable Squiz.Commenting * phpcs:disable Generic.Commenting + * + * @param string $client_secret The client secret. + * @param bool $use_sandbox Whether to use the sandbox mode. + * @param string $client_id The client ID. + * + * @return stdClass The payee object. */ private function request_payee( string $client_id, @@ -176,24 +189,13 @@ class ConnectManualRestEndpoint extends RestEndpoint { $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( new InMemoryCache(), $host, $client_id, $client_secret, $this->logger, - $empty_settings + null ); $orders = new Orders( diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php new file mode 100644 index 000000000..8ed204383 --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -0,0 +1,105 @@ +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() ); + } + } +} diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 6c59b1622..02e7c80cd 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -41,35 +41,23 @@ class OnboardingRestEndpoint extends RestEndpoint { * @var array */ private array $field_map = array( - 'completed' => array( + 'completed' => array( 'js_name' => 'completed', 'sanitize' => 'to_boolean', ), - 'step' => array( + 'step' => array( 'js_name' => 'step', 'sanitize' => 'to_number', ), - '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', - ), - 'is_casual_seller' => array( + 'is_casual_seller' => array( 'js_name' => 'isCasualSeller', 'sanitize' => 'to_boolean', ), - 'products' => array( + 'are_optional_payment_methods_enabled' => array( + 'js_name' => 'areOptionalPaymentMethodsEnabled', + 'sanitize' => 'to_boolean', + ), + 'products' => array( 'js_name' => 'products', ), ); @@ -147,9 +135,9 @@ class OnboardingRestEndpoint extends RestEndpoint { $this->flag_map ); - return rest_ensure_response( + return $this->return_success( + $js_data, array( - 'data' => $js_data, 'flags' => $js_flags, ) ); diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index 08191276b..76626ac0c 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -10,14 +10,12 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings\Endpoint; use WC_REST_Controller; +use WP_REST_Response; /** * Base class for REST controllers in the settings module. - * - * 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. * @@ -34,6 +32,54 @@ class RestEndpoint extends WC_REST_Controller { return current_user_can( 'manage_woocommerce' ); } + /** + * Returns a successful REST API response. + * + * @param mixed $data The main response data. + * @param array $extra Optional, additional response data. + * + * @return WP_REST_Response The successful response. + */ + protected function return_success( $data, array $extra = array() ) : WP_REST_Response { + $response = array( + 'success' => true, + 'data' => $data, + ); + + if ( $extra ) { + foreach ( $extra as $key => $value ) { + if ( isset( $response[ $key ] ) ) { + continue; + } + + $response[ $key ] = $value; + } + } + + return rest_ensure_response( $response ); + } + + /** + * Returns an error REST API response. + * + * @param string $reason The reason for the error. + * @param mixed $details Optional details about the error. + * + * @return WP_REST_Response The error response. + */ + protected function return_error( string $reason, $details = null ) : WP_REST_Response { + $response = array( + 'success' => false, + 'message' => $reason, + ); + + if ( ! is_null( $details ) ) { + $response['details'] = $details; + } + + return rest_ensure_response( $response ); + } + /** * Sanitizes parameters based on a field mapping. * diff --git a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php new file mode 100644 index 000000000..244c26dfe --- /dev/null +++ b/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php @@ -0,0 +1,81 @@ +logger = $logger; + $this->request_data = $request_data; + } + + /** + * Handles the request. + */ + public function handle_request(): void { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not an admin.', 403 ); + return; + } + + try { + $this->request_data->read_request( $this->nonce() ); + update_option( self::OPTION_NAME_SHOULD_USE_OLD_UI, false ); + + wp_send_json_success(); + } catch ( Exception $error ) { + wp_send_json_error( array( 'message' => $error->getMessage() ), 500 ); + } + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } +} diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php new file mode 100644 index 000000000..6e91aba3a --- /dev/null +++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php @@ -0,0 +1,227 @@ +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(); + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index c2e688c7f..7c9dca2f8 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -9,8 +9,8 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Settings; -use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint; -use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint; +use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; @@ -22,6 +22,16 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; class SettingsModule implements ServiceModule, ExecutableModule { use ModuleClassNameIdTrait; + /** + * Returns whether the old settings UI should be loaded. + */ + public static function should_use_the_old_ui() : bool { + return apply_filters( + 'woocommerce_paypal_payments_should_use_the_old_ui', + (bool) get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === true + ); + } + /** * {@inheritDoc} */ @@ -33,6 +43,62 @@ class SettingsModule implements ServiceModule, ExecutableModule { * {@inheritDoc} */ public function run( ContainerInterface $container ) : bool { + if ( self::should_use_the_old_ui() ) { + add_filter( + 'woocommerce_paypal_payments_inside_settings_page_header', + static fn() : string => sprintf( + '%s', + esc_html__( 'Switch to new settings UI', 'woocommerce-paypal-payments' ) + ) + ); + + add_action( + 'admin_enqueue_scripts', + static function () use ( $container ) { + $module_url = $container->get( 'settings.url' ); + + /** + * Require resolves. + * + * @psalm-suppress UnresolvableInclude + */ + $script_asset_file = require dirname( realpath( __FILE__ ) ?: '', 2 ) . '/assets/switchSettingsUi.asset.php'; + + wp_register_script( + 'ppcp-switch-settings-ui', + untrailingslashit( $module_url ) . '/assets/switchSettingsUi.js', + $script_asset_file['dependencies'], + $script_asset_file['version'], + true + ); + + wp_localize_script( + 'ppcp-switch-settings-ui', + 'ppcpSwitchSettingsUi', + array( + 'endpoint' => \WC_AJAX::get_endpoint( SwitchSettingsUiEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( SwitchSettingsUiEndpoint::nonce() ), + ) + ); + + wp_enqueue_script( 'ppcp-switch-settings-ui' ); + } + ); + + $endpoint = $container->get( 'settings.switch-ui.endpoint' ); + assert( $endpoint instanceof SwitchSettingsUiEndpoint ); + + add_action( + 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT, + array( + $endpoint, + 'handle_request', + ) + ); + + return true; + } + add_action( 'admin_enqueue_scripts', /** @@ -109,13 +175,17 @@ class SettingsModule implements ServiceModule, ExecutableModule { add_action( 'rest_api_init', static function () use ( $container ) : void { - $onboarding_endpoint = $container->get( 'settings.rest.onboarding' ); - assert( $onboarding_endpoint instanceof OnboardingRestEndpoint ); - $onboarding_endpoint->register_routes(); + $endpoints = array( + $container->get( 'settings.rest.onboarding' ), + $container->get( 'settings.rest.common' ), + $container->get( 'settings.rest.connect_manual' ), + $container->get( 'settings.rest.login_link' ), + ); - $connect_manual_endpoint = $container->get( 'settings.rest.connect_manual' ); - assert( $connect_manual_endpoint instanceof ConnectManualRestEndpoint ); - $connect_manual_endpoint->register_routes(); + foreach ( $endpoints as $endpoint ) { + assert( $endpoint instanceof RestEndpoint ); + $endpoint->register_routes(); + } } ); diff --git a/modules/ppcp-settings/webpack.config.js b/modules/ppcp-settings/webpack.config.js index 93cd4afb5..a3a6d709d 100644 --- a/modules/ppcp-settings/webpack.config.js +++ b/modules/ppcp-settings/webpack.config.js @@ -7,6 +7,11 @@ module.exports = { ...{ entry: { index: path.resolve( process.cwd(), 'resources/js', 'index.js' ), + switchSettingsUi: path.resolve( + process.cwd(), + 'resources/js', + 'switchSettingsUi.js' + ), style: path.resolve( process.cwd(), 'resources/css', 'style.scss' ), }, }, diff --git a/modules/ppcp-settings/yarn.lock b/modules/ppcp-settings/yarn.lock index 29b840895..a3b2a6adb 100644 --- a/modules/ppcp-settings/yarn.lock +++ b/modules/ppcp-settings/yarn.lock @@ -1874,6 +1874,28 @@ "@parcel/watcher-win32-ia32" "2.5.0" "@parcel/watcher-win32-x64" "2.5.0" +"@paypal/paypal-js@^8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-8.1.2.tgz#3b6199e1c9e3b2a3e444309e0d0ff63556e3ef86" + integrity sha512-EKshGSWRxLWU1NyPB9P1TiOkPajVmpTo5I9HuZKoSv8y2uk0XIskXqMkAJ/Y9qAg9iJyP102Jb/atX63tTy24w== + dependencies: + promise-polyfill "^8.3.0" + +"@paypal/react-paypal-js@^8.7.0": + version "8.7.0" + resolved "https://registry.yarnpkg.com/@paypal/react-paypal-js/-/react-paypal-js-8.7.0.tgz#26b9dc9b881827afcb2fb8dea5078a9c90e24128" + integrity sha512-KW4EdL8hlLWBY6f9l1csxQ8Szh5bhMFLxl+DV29jHNwdlM6z3s+w4LZQ7GtgHktSrlNBHgblNNvwLNpcDtsZgg== + dependencies: + "@paypal/paypal-js" "^8.1.2" + "@paypal/sdk-constants" "^1.0.122" + +"@paypal/sdk-constants@^1.0.122": + version "1.0.150" + resolved "https://registry.yarnpkg.com/@paypal/sdk-constants/-/sdk-constants-1.0.150.tgz#26b9f3b980bc1c73846ae0c1af99719e490dff48" + integrity sha512-ETm/mtiTBw4gHPdKo3GXzSQyXfZKevSg+ujfEvZbLT9gCc/YFmTBnWDroqmzOeuSXnf2Ll4bBSp3xbr47NQbUQ== + dependencies: + hi-base32 "^0.5.0" + "@pkgr/core@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" @@ -6588,6 +6610,11 @@ hey-listen@^1.0.8: resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== +hi-base32@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" + integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA== + highlight-words-core@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.3.tgz#781f37b2a220bf998114e4ef8c8cb6c7a4802ea8" @@ -9365,6 +9392,11 @@ progress@2.0.3, progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-polyfill@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63" + integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg== + prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" diff --git a/modules/ppcp-uninstall/services.php b/modules/ppcp-uninstall/services.php index ca67dc71b..51ff935af 100644 --- a/modules/ppcp-uninstall/services.php +++ b/modules/ppcp-uninstall/services.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Uninstall; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; +use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint; use WooCommerce\PayPalCommerce\Uninstall\Assets\ClearDatabaseAssets; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; @@ -34,6 +35,7 @@ return array( WebhookSimulation::OPTION_ID, WebhookRegistrar::KEY, 'ppcp_payment_tokens_migration_initialized', + SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI, ); }, diff --git a/modules/ppcp-uninstall/src/UninstallModule.php b/modules/ppcp-uninstall/src/UninstallModule.php index 2cb2dce52..524460327 100644 --- a/modules/ppcp-uninstall/src/UninstallModule.php +++ b/modules/ppcp-uninstall/src/UninstallModule.php @@ -43,33 +43,29 @@ class UninstallModule implements ServiceModule, ExtendingModule, ExecutableModul * {@inheritDoc} */ public function run( ContainerInterface $container ): bool { - $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); - if ( Settings::CONNECTION_TAB_ID === $page_id ) { - $this->registerClearDatabaseAssets( $container->get( 'uninstall.clear-db-assets' ) ); - } + add_action( + 'init', + static function () use ( $container ) { + $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); + if ( Settings::CONNECTION_TAB_ID === $page_id ) { + $container->get( 'uninstall.clear-db-assets' )->register(); + add_action( 'admin_enqueue_scripts', array( $container->get( 'uninstall.clear-db-assets' ), 'enqueue' ) ); + } - $request_data = $container->get( 'button.request-data' ); - $clear_db = $container->get( 'uninstall.clear-db' ); - $clear_db_endpoint = $container->get( 'uninstall.clear-db-endpoint' ); - $option_names = $container->get( 'uninstall.ppcp-all-option-names' ); - $scheduled_action_names = $container->get( 'uninstall.ppcp-all-scheduled-action-names' ); - $action_names = $container->get( 'uninstall.ppcp-all-action-names' ); + $request_data = $container->get( 'button.request-data' ); + $clear_db = $container->get( 'uninstall.clear-db' ); + $clear_db_endpoint = $container->get( 'uninstall.clear-db-endpoint' ); + $option_names = $container->get( 'uninstall.ppcp-all-option-names' ); + $scheduled_action_names = $container->get( 'uninstall.ppcp-all-scheduled-action-names' ); + $action_names = $container->get( 'uninstall.ppcp-all-action-names' ); - $this->handleClearDbAjaxRequest( $request_data, $clear_db, $clear_db_endpoint, $option_names, $scheduled_action_names, $action_names ); + self::handleClearDbAjaxRequest( $request_data, $clear_db, $clear_db_endpoint, $option_names, $scheduled_action_names, $action_names ); + } + ); return true; } - /** - * Registers the assets for clear database functionality. - * - * @param ClearDatabaseAssets $asset_loader The clear database functionality asset loader. - */ - protected function registerClearDatabaseAssets( ClearDatabaseAssets $asset_loader ): void { - add_action( 'init', array( $asset_loader, 'register' ) ); - add_action( 'admin_enqueue_scripts', array( $asset_loader, 'enqueue' ) ); - } - /** * Handles the AJAX request to clear the database. * @@ -80,7 +76,7 @@ class UninstallModule implements ServiceModule, ExtendingModule, ExecutableModul * @param string[] $scheduled_action_names The list of scheduled action names. * @param string[] $action_names The list of action names. */ - protected function handleClearDbAjaxRequest( + protected static function handleClearDbAjaxRequest( RequestData $request_data, ClearDatabaseInterface $clear_db, string $nonce, diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index 041c61c35..37074b41e 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -51,10 +51,16 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule * @throws NotFoundException When service could not be found. */ public function run( ContainerInterface $container ): bool { - $listener = $container->get( 'vaulting.customer-approval-listener' ); - assert( $listener instanceof CustomerApprovalListener ); - $listener->listen(); + add_action( + 'woocommerce_init', + function() use ( $container ) { + $listener = $container->get( 'vaulting.customer-approval-listener' ); + assert( $listener instanceof CustomerApprovalListener ); + + $listener->listen(); + } + ); $subscription_helper = $container->get( 'wc-subscriptions.helper' ); add_action( @@ -166,6 +172,10 @@ class VaultingModule implements ServiceModule, ExtendingModule, ExecutableModule add_action( 'wp', function() use ( $container ) { + if ( $container->get( 'vaulting.vault-v3-enabled' ) ) { + return; + } + global $wp; if ( isset( $wp->query_vars['delete-payment-method'] ) ) { diff --git a/modules/ppcp-wc-gateway/assets/images/paypal.svg b/modules/ppcp-wc-gateway/assets/images/paypal.svg index 9aa54566c..b8a369b15 100644 --- a/modules/ppcp-wc-gateway/assets/images/paypal.svg +++ b/modules/ppcp-wc-gateway/assets/images/paypal.svg @@ -1,3 +1,3 @@ - - + + diff --git a/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss b/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss index 768f95c75..537d035b4 100644 --- a/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss +++ b/modules/ppcp-wc-gateway/resources/css/gateway-settings.scss @@ -8,7 +8,7 @@ } } -.ppcp-preview { +.ppcp-preview:not(.ppcp-r-styling__preview) { width: var(--box-width, 100%); padding: 15px; border: 1px solid lightgray; diff --git a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js index 82152b2ad..b9c11b486 100644 --- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js +++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js @@ -11,6 +11,7 @@ import { isVisible, } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; +import { PaymentContext } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; document.addEventListener( 'DOMContentLoaded', () => { function disableAll( nodeList ) { @@ -134,11 +135,17 @@ document.addEventListener( 'DOMContentLoaded', () => { function createButtonPreview( settingsCallback ) { const render = ( settings ) => { - const wrapperSelector = - Object.values( settings.separate_buttons ).length > 0 - ? Object.values( settings.separate_buttons )[ 0 ].wrapper - : settings.button.wrapper; + const previewSettings = { + context: PaymentContext.Preview, + ...settings, + }; + + const { button, separate_buttons } = previewSettings; + const wrapperSelector = ( + Object.values( separate_buttons )[ 0 ] ?? button + )?.wrapper; const wrapper = document.querySelector( wrapperSelector ); + if ( ! wrapper ) { return; } @@ -146,7 +153,7 @@ document.addEventListener( 'DOMContentLoaded', () => { const renderer = new Renderer( null, - settings, + previewSettings, ( data, actions ) => actions.reject(), null ); @@ -155,7 +162,7 @@ document.addEventListener( 'DOMContentLoaded', () => { renderer.render( {} ); jQuery( document ).trigger( 'ppcp_paypal_render_preview', - settings + previewSettings ); } catch ( err ) { console.error( err ); diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 94f787b4d..9447c981e 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -24,6 +24,7 @@ use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\State; +use WooCommerce\PayPalCommerce\Settings\SettingsModule; use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction; use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment; @@ -336,7 +337,8 @@ return array( $container->get( 'wcgateway.button.default-locations' ), $container->get( 'wcgateway.settings.dcc-gateway-title.default' ), $container->get( 'wcgateway.settings.pay-later.default-button-locations' ), - $container->get( 'wcgateway.settings.pay-later.default-messaging-locations' ) + $container->get( 'wcgateway.settings.pay-later.default-messaging-locations' ), + $container->get( 'compat.settings.settings_map_helper' ) ); } ), @@ -2067,6 +2069,6 @@ return array( }, 'wcgateway.settings.admin-settings-enabled' => static function( ContainerInterface $container ): bool { - return $container->has( 'settings.url' ); + return $container->has( 'settings.url' ) && ! SettingsModule::should_use_the_old_ui(); }, ); diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index 2256cfc0f..bfda515ac 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -174,23 +174,17 @@ class SettingsPageAssets { * @return void */ public function register_assets(): void { - add_action( - 'admin_enqueue_scripts', - function() { - if ( ! is_admin() || wp_doing_ajax() ) { - return; - } + if ( ! is_admin() || wp_doing_ajax() ) { + return; + } - if ( $this->is_settings_page ) { - $this->register_admin_assets(); - } - - if ( $this->is_paypal_payment_method_page ) { - $this->register_paypal_admin_assets(); - } - } - ); + if ( $this->is_settings_page ) { + $this->register_admin_assets(); + } + if ( $this->is_paypal_payment_method_page ) { + $this->register_paypal_admin_assets(); + } } /** diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 6e005babc..710246d9f 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -482,18 +482,22 @@ return function ( ContainerInterface $container, array $fields ): array { ), ), 'soft_descriptor' => array( - 'title' => __( 'Soft Descriptor', 'woocommerce-paypal-payments' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement. Text field, max value of 22 characters.', 'woocommerce-paypal-payments' ), - 'maxlength' => 22, - 'default' => '', - 'screens' => array( + 'title' => __( 'Soft Descriptor', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement. Text field, max value of 22 characters (letters, numbers, spaces, asterisks, dots and hyphens).', 'woocommerce-paypal-payments' ), + 'maxlength' => 22, + 'default' => '', + 'custom_attributes' => array( + 'pattern' => '[a-zA-Z0-9\s\*.\-]*', + 'title' => __( 'Please use only letters, numbers, spaces, asterisks, dots and hyphens.', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, ), 'prefix' => array( 'title' => __( 'Invoice prefix', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php b/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php index 73bd843d8..4a304e335 100644 --- a/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/HeaderRenderer.php @@ -76,6 +76,7 @@ class HeaderRenderer { . __( 'Submit a bug', 'woocommerce-paypal-payments' ) . ' + ' . apply_filters( 'woocommerce_paypal_payments_inside_settings_page_header', '' ) . '
diff --git a/modules/ppcp-wc-gateway/src/Settings/Settings.php b/modules/ppcp-wc-gateway/src/Settings/Settings.php index e083a91d1..d5465eb72 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Settings.php +++ b/modules/ppcp-wc-gateway/src/Settings/Settings.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use WooCommerce\PayPalCommerce\Compat\SettingsMapHelper; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -56,24 +57,34 @@ class Settings implements ContainerInterface { */ protected $default_dcc_gateway_title; + /** + * A helper for mapping the new/old settings. + * + * @var SettingsMapHelper + */ + protected SettingsMapHelper $settings_map_helper; + /** * Settings constructor. * - * @param string[] $default_button_locations The list of selected default button locations. - * @param string $default_dcc_gateway_title The default ACDC gateway title. - * @param string[] $default_pay_later_button_locations The list of selected default pay later button locations. - * @param string[] $default_pay_later_messaging_locations The list of selected default pay later messaging locations. + * @param string[] $default_button_locations The list of selected default button locations. + * @param string $default_dcc_gateway_title The default ACDC gateway title. + * @param string[] $default_pay_later_button_locations The list of selected default pay later button locations. + * @param string[] $default_pay_later_messaging_locations The list of selected default pay later messaging locations. + * @param SettingsMapHelper $settings_map_helper A helper for mapping the new/old settings. */ public function __construct( array $default_button_locations, string $default_dcc_gateway_title, array $default_pay_later_button_locations, - array $default_pay_later_messaging_locations + array $default_pay_later_messaging_locations, + SettingsMapHelper $settings_map_helper ) { $this->default_button_locations = $default_button_locations; $this->default_dcc_gateway_title = $default_dcc_gateway_title; $this->default_pay_later_button_locations = $default_pay_later_button_locations; $this->default_pay_later_messaging_locations = $default_pay_later_messaging_locations; + $this->settings_map_helper = $settings_map_helper; } /** @@ -88,7 +99,8 @@ class Settings implements ContainerInterface { if ( ! $this->has( $id ) ) { throw new NotFoundException(); } - return $this->settings[ $id ]; + + return $this->settings_map_helper->mapped_value( $id ) ?? $this->settings[ $id ]; } /** @@ -99,6 +111,10 @@ class Settings implements ContainerInterface { * @return bool */ public function has( $id ) { + if ( $this->settings_map_helper->has_mapped_key( $id ) ) { + return true; + } + $this->load(); return array_key_exists( $id, $this->settings ); } diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index e8c35a5f6..9d57421ae 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -181,34 +181,43 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul } ); - if ( $c->has( 'wcgateway.url' ) ) { - $settings_status = $c->get( 'wcgateway.settings.status' ); - assert( $settings_status instanceof SettingsStatus ); + add_action( + 'admin_enqueue_scripts', + function () use ( $c ) { + if ( ! is_admin() || wp_doing_ajax() ) { + return; + } + if ( ! $c->has( 'wcgateway.url' ) ) { + return; + } + $settings_status = $c->get( 'wcgateway.settings.status' ); + assert( $settings_status instanceof SettingsStatus ); - $settings = $c->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings ); + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); - $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); - assert( $dcc_configuration instanceof DCCGatewayConfiguration ); + $dcc_configuration = $c->get( 'wcgateway.configuration.dcc' ); + assert( $dcc_configuration instanceof DCCGatewayConfiguration ); - $assets = new SettingsPageAssets( - $c->get( 'wcgateway.url' ), - $c->get( 'ppcp.asset-version' ), - $c->get( 'wc-subscriptions.helper' ), - $c->get( 'button.client_id_for_admin' ), - $c->get( 'api.shop.currency.getter' ), - $c->get( 'api.shop.country' ), - $c->get( 'onboarding.environment' ), - $settings_status->is_pay_later_button_enabled(), - $settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(), - $c->get( 'wcgateway.settings.funding-sources' ), - $c->get( 'wcgateway.is-ppcp-settings-page' ), - $dcc_configuration->is_enabled(), - $c->get( 'api.endpoint.billing-agreements' ), - $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) - ); - $assets->register_assets(); - } + $assets = new SettingsPageAssets( + $c->get( 'wcgateway.url' ), + $c->get( 'ppcp.asset-version' ), + $c->get( 'wc-subscriptions.helper' ), + $c->get( 'button.client_id_for_admin' ), + $c->get( 'api.shop.currency.getter' ), + $c->get( 'api.shop.country' ), + $c->get( 'onboarding.environment' ), + $settings_status->is_pay_later_button_enabled(), + $settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(), + $c->get( 'wcgateway.settings.funding-sources' ), + $c->get( 'wcgateway.is-ppcp-settings-page' ), + $dcc_configuration->is_enabled(), + $c->get( 'api.endpoint.billing-agreements' ), + $c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' ) + ); + $assets->register_assets(); + } + ); add_filter( Repository::NOTICES_FILTER, diff --git a/modules/ppcp-webhooks/src/WebhookModule.php b/modules/ppcp-webhooks/src/WebhookModule.php index 22ae0fc19..f1f7a03b4 100644 --- a/modules/ppcp-webhooks/src/WebhookModule.php +++ b/modules/ppcp-webhooks/src/WebhookModule.php @@ -56,8 +56,6 @@ class WebhookModule implements ServiceModule, FactoryModule, ExtendingModule, Ex * {@inheritDoc} */ public function run( ContainerInterface $container ): bool { - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); add_action( 'rest_api_init', @@ -127,38 +125,35 @@ class WebhookModule implements ServiceModule, FactoryModule, ExtendingModule, Ex } ); - $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); - if ( Settings::CONNECTION_TAB_ID === $page_id ) { - $asset_loader = $container->get( 'webhook.status.assets' ); - assert( $asset_loader instanceof WebhooksStatusPageAssets ); - add_action( - 'init', - array( $asset_loader, 'register' ) - ); - add_action( - 'admin_enqueue_scripts', - array( $asset_loader, 'enqueue' ) - ); - - try { - $webhooks = $container->get( 'webhook.status.registered-webhooks' ); - $state = $container->get( 'onboarding.state' ); - if ( empty( $webhooks ) && $state->current_state() >= State::STATE_ONBOARDED ) { - $registrar = $container->get( 'webhook.registrar' ); - assert( $registrar instanceof WebhookRegistrar ); - - // Looks like we cannot call rest_url too early. - add_action( - 'init', - function () use ( $registrar ) { - $registrar->register(); - } - ); + add_action( + 'init', + function () use ( $container ) { + $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); + if ( Settings::CONNECTION_TAB_ID !== $page_id ) { + return; + } + + $asset_loader = $container->get( 'webhook.status.assets' ); + assert( $asset_loader instanceof WebhooksStatusPageAssets ); + $asset_loader->register(); + add_action( + 'admin_enqueue_scripts', + array( $asset_loader, 'enqueue' ) + ); + + try { + $webhooks = $container->get( 'webhook.status.registered-webhooks' ); + $state = $container->get( 'onboarding.state' ); + if ( empty( $webhooks ) && $state->current_state() >= State::STATE_ONBOARDED ) { + $registrar = $container->get( 'webhook.registrar' ); + assert( $registrar instanceof WebhookRegistrar ); + $registrar->register(); + } + } catch ( Exception $exception ) { + $container->get( 'woocommerce.logger.woocommerce' )->error( 'Failed to load webhooks list: ' . $exception->getMessage() ); } - } catch ( Exception $exception ) { - $logger->error( 'Failed to load webhooks list: ' . $exception->getMessage() ); } - } + ); add_action( 'woocommerce_paypal_payments_gateway_migrate', diff --git a/package.json b/package.json index 104236764..6dc952728 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.9.4", + "version": "2.9.5", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index 4dd03591f..ca2b8a27c 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: woocommerce, automattic, syde Tags: woocommerce, paypal, payments, ecommerce, credit card Requires at least: 6.3 -Tested up to: 6.6 +Tested up to: 6.7 Requires PHP: 7.4 -Stable tag: 2.9.4 +Stable tag: 2.9.5 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -179,6 +179,24 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == += 2.9.5 - xxxx-xx-xx = +Fix - Early translation loading triggers `Function _load_textdomain_just_in_time was called incorrectly.` notice #2816 +Fix - ACDC card fields not loading and payment not successful when Classic Checkout Smart Button Location disabled #2852 +Fix - ACDC gateway does not appear for guests when is Fastlane enabled and a subscription product is in the cart #2745 +Fix - "Voide authorization" button does not appear for Apple Pay/Google Pay orders when payment buttons are separated #2752 +Fix - Additional payment tokens saved with new customer_id #2820 +Fix - Vaulted payment method may not be displayed in PayPal button for return buyer #2809 +Fix - Conflict with EasyShip plugin due to shipping methods loading too early #2845 +Fix - Restore accidentally removed ACDC currencies #2838 +Enhancement - Native gateway icon for PayPal & Pay upon Invoice gateways #2712 +Enhancement - Allow disabling specific card types for Fastlane #2704 +Enhancement - Fastlane Insights SDK implementation for block Checkout #2737 +Enhancement - Hide split local APMs in Payments settings tab when PayPal is not enabled #2703 +Enhancement - Do not load split local APMs on Checkout when PayPal is not enabled #2792 +Enhancement - Add support for Button Options in the Block Checkout for Apple Pay & Google Pay buttons #2797 #2772 +Enhancement - Disable “Add payment method” button while saving ACDC payment #2794 +Enhancement - Sanitize soft_descriptor field #2846 #2854 + = 2.9.4 - 2024-11-11 = * Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755 * Fix - Set "Sold individually" only for subscription connected to PayPal #2710 diff --git a/src/FilePathPluginFactory.php b/src/FilePathPluginFactory.php index f561b14d5..e62fe1add 100644 --- a/src/FilePathPluginFactory.php +++ b/src/FilePathPluginFactory.php @@ -56,16 +56,19 @@ class FilePathPluginFactory implements FilePathPluginFactoryInterface { ); } - if ( ! function_exists( 'get_plugin_data' ) ) { - /** - * Skip check for WP files. - * - * @psalm-suppress MissingFile - */ - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } + $default_headers = array( + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'TextDomain' => 'Text Domain', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'RequiresPlugins' => 'Requires Plugins', + ); + + $plugin_data = \get_file_data( $filePath, $default_headers, 'plugin' ); - $plugin_data = get_plugin_data( $filePath, false ); if ( empty( $plugin_data ) ) { throw new UnexpectedValueException( sprintf( @@ -98,7 +101,7 @@ class FilePathPluginFactory implements FilePathPluginFactoryInterface { $this->create_version( $plugin_data['Version'] ), $base_dir, $base_name, - $plugin_data['Title'], + $plugin_data['PluginURI'], $plugin_data['Description'], $text_domain, $this->create_version( $plugin_data['RequiresPHP'] ), diff --git a/src/Plugin.php b/src/Plugin.php index b889cffad..546965a20 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -46,11 +46,11 @@ class Plugin implements PluginInterface { protected $base_name; /** - * The plugin title. + * The plugin URI. * * @var string */ - protected $title; + protected $plugin_uri; /** * The plugin description. @@ -87,7 +87,7 @@ class Plugin implements PluginInterface { * @param VersionInterface $version The plugin version. * @param string $base_dir The path to the plugin base directory. * @param string $base_name The plugin base name. - * @param string $title The plugin title. + * @param string $plugin_uri The plugin URI. * @param string $description The plugin description. * @param string $text_domain The text domain of this plugin. * @param VersionInterface $min_php_version The minimal version of PHP required by this plugin. @@ -98,7 +98,7 @@ class Plugin implements PluginInterface { VersionInterface $version, string $base_dir, string $base_name, - string $title, + string $plugin_uri, string $description, string $text_domain, VersionInterface $min_php_version, @@ -109,7 +109,7 @@ class Plugin implements PluginInterface { $this->version = $version; $this->base_dir = $base_dir; $this->base_name = $base_name; - $this->title = $title; + $this->plugin_uri = $plugin_uri; $this->text_domain = $text_domain; $this->min_php_version = $min_php_version; $this->min_wp_version = $min_wp_version; @@ -126,7 +126,27 @@ class Plugin implements PluginInterface { * The plugin description. */ public function getDescription(): string { - return $this->description; + + $allowed_tags = array( + 'abbr' => array( 'title' => true ), + 'acronym' => array( 'title' => true ), + 'code' => true, + 'em' => true, + 'strong' => true, + 'a' => array( + 'href' => true, + 'title' => true, + ), + ); + + // phpcs:disable + $text = \__( $this->description, $this->text_domain ); + + /** + * @psalm-suppress InvalidArgument + */ + return wp_kses( $text, $allowed_tags ); + // phpcs:enable } /** @@ -157,11 +177,18 @@ class Plugin implements PluginInterface { return $this->text_domain; } + /** + * The plugin URI. + */ + public function getUri(): string { + return esc_url( $this->plugin_uri ); + } + /** * The plugin title. */ public function getTitle(): string { - return $this->title; + return '' . $this->getName() . ''; } /** diff --git a/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php index 6bcfb4ba7..044661988 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/PaymentTokenEndpointTest.php @@ -141,7 +141,10 @@ class PaymentTokenEndpointTest extends TestCase $this->sut->for_user($id); } - public function testDeleteToken() + /** + * @doesNotPerformAssertions + */ + public function testDeleteToken() { $paymentToken = Mockery::mock(PaymentToken::class); $paymentToken->shouldReceive('id') diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index 4e6c859a1..9e743085a 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -31,6 +31,7 @@ class TestCase extends \PHPUnit\Framework\TestCase when('wc_string_to_bool')->alias(function ($string) { return is_bool( $string ) ? $string : ( 'yes' === strtolower( $string ) || 1 === $string || 'true' === strtolower( $string ) || '1' === $string ); }); + when('get_file_data')->justReturn(['Version' => '1.0','PluginURI' => 'https://github.com/woocommerce/woocommerce-paypal-payments']); when('get_plugin_data')->justReturn(['Version' => '1.0']); when('plugin_basename')->justReturn('woocommerce-paypal-payments/woocommerce-paypal-payments.php'); when('get_transient')->returnArg(); diff --git a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php b/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php deleted file mode 100644 index 5bb29b95a..000000000 --- a/tests/PHPUnit/WcGateway/Assets/SettingsPagesAssetsTest.php +++ /dev/null @@ -1,50 +0,0 @@ -justReturn(true); - when('wp_doing_ajax') - ->justReturn(false); - - $testee->register_assets(); - - self::assertSame(has_action('admin_enqueue_scripts', "function()"), 10); - } -} diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php index efa1b7de6..74f6ed074 100644 --- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php +++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php @@ -22,6 +22,9 @@ class SettingsListenerTest extends ModularTestCase parent::setUp(); } + /** + * @doesNotPerformAssertions + */ public function testListen() { $settings = Mockery::mock(Settings::class); diff --git a/tests/e2e/PHPUnit/SettingsTest.php b/tests/e2e/PHPUnit/SettingsTest.php new file mode 100644 index 000000000..b776f2e0f --- /dev/null +++ b/tests/e2e/PHPUnit/SettingsTest.php @@ -0,0 +1,70 @@ +createMock(AbstractDataModel::class); + $commonSettingsModel->method('to_array')->willReturn([ + 'use_sandbox' => 'yes', + 'client_id' => 'abc123', + 'client_secret' => 'secret123', + ]); + + $generalSettingsModel = $this->createMock(AbstractDataModel::class); + $generalSettingsModel->method('to_array')->willReturn([ + 'is_sandbox' => 'no', + 'live_client_id' => 'live_id_123', + 'live_client_secret' => 'live_secret_123', + ]); + + $settingsMap = [ + new SettingsMap( + $commonSettingsModel, + [ + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + ] + ), + new SettingsMap( + $generalSettingsModel, + [ + 'is_sandbox' => 'sandbox_on', + 'live_client_id' => 'client_id_production', + 'live_client_secret' => 'client_secret_production', + ] + ), + ]; + + $settingsMapHelper = new SettingsMapHelper($settingsMap); + + $this->settings = new Settings( + ['cart', 'checkout'], + 'PayPal Credit Gateway', + ['checkout'], + ['cart'], + $settingsMapHelper + ); + } + + public function testGetMappedValue() { + $value = $this->settings->get('sandbox_on'); + + $this->assertEquals('no', $value); + } + + public function testGetThrowsNotFoundExceptionForInvalidKey() { + $this->expectException(NotFoundException::class); + + $this->settings->get('invalid_key'); + } +} + diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index bff122d62..1544895ca 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,14 +3,14 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.9.4 + * Version: 2.9.5 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.4 * Requires Plugins: woocommerce * WC requires at least: 6.9 - * WC tested up to: 9.3 + * WC tested up to: 9.4 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -26,7 +26,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' ); define( 'PAYPAL_URL', 'https://www.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2024-11-05' ); +define( 'PAYPAL_INTEGRATION_DATE', '2024-12-02' ); define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); @@ -90,32 +90,28 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); 'plugins_loaded', function () { init(); + add_action( + 'init', + function () { + $current_plugin_version = (string) PPCP::container()->get( 'ppcp.plugin' )->getVersion(); + $installed_plugin_version = get_option( 'woocommerce-ppcp-version' ); + if ( $installed_plugin_version !== $current_plugin_version ) { + /** + * The hook fired when the plugin is installed or updated. + */ + do_action( 'woocommerce_paypal_payments_gateway_migrate', $installed_plugin_version ); - if ( ! function_exists( 'get_plugin_data' ) ) { - /** - * Skip check for WP files. - * - * @psalm-suppress MissingFile - */ - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - $plugin_data = get_plugin_data( __DIR__ . '/woocommerce-paypal-payments.php', false ); - $plugin_version = $plugin_data['Version'] ?? null; - $installed_plugin_version = get_option( 'woocommerce-ppcp-version' ); - if ( $installed_plugin_version !== $plugin_version ) { - /** - * The hook fired when the plugin is installed or updated. - */ - do_action( 'woocommerce_paypal_payments_gateway_migrate', $installed_plugin_version ); - - if ( $installed_plugin_version ) { - /** - * The hook fired when the plugin is updated. - */ - do_action( 'woocommerce_paypal_payments_gateway_migrate_on_update' ); - } - update_option( 'woocommerce-ppcp-version', $plugin_version ); - } + if ( $installed_plugin_version ) { + /** + * The hook fired when the plugin is updated. + */ + do_action( 'woocommerce_paypal_payments_gateway_migrate_on_update' ); + } + update_option( 'woocommerce-ppcp-version', $current_plugin_version ); + } + }, + -1 + ); } ); register_activation_hook( diff --git a/yarn.lock b/yarn.lock index 8d605e524..f808c687c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2671,9 +2671,9 @@ mime "^3.0.0" web-vitals "^4.2.1" -"@wordpress/element@^6.1.0": +"@wordpress/element@*", "@wordpress/element@^6.1.0": version "6.11.0" - resolved "https://registry.npmjs.org/@wordpress/element/-/element-6.11.0.tgz" + resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-6.11.0.tgz#7bc3e453a95bb806a707b4dc617373afa108af19" integrity sha512-UvHFYkT+EEaXEyEfw+iqLHRO9OwBjjsUydEMHcqntzkNcsYeAbmaL9V8R9ikXHLe6ftdbkwoXgF85xVPhVsL+Q== dependencies: "@babel/runtime" "7.25.7" @@ -2715,6 +2715,15 @@ globals "^13.12.0" requireindex "^1.2.0" +"@wordpress/icons@^10.11.0": + version "10.11.0" + resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-10.11.0.tgz#0beedef8ee49c135412fb81fc59440dd48d652aa" + integrity sha512-RMetpFwUIeh3sVj2+p6+QX5AW8pF7DvQzxH9jUr8YjaF2iLE64vy6m0cZz/X8xkSktHrXMuPJIr7YIVF20TEyw== + dependencies: + "@babel/runtime" "7.25.7" + "@wordpress/element" "*" + "@wordpress/primitives" "*" + "@wordpress/jest-console@*": version "8.11.0" resolved "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.11.0.tgz" @@ -2749,6 +2758,15 @@ resolved "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz" integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw== +"@wordpress/primitives@*": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-4.11.0.tgz#7bc24c07ed11057340832791c1c21e75a5181194" + integrity sha512-CoBXbh0mOSxcZtuzL7gK3RVumFx71DXQBfd3IkbRHuuVxa+2hI4KDuFyomSsbjQDshHsfuVrKUvuT3UGt6pdpQ== + dependencies: + "@babel/runtime" "7.25.7" + "@wordpress/element" "*" + clsx "^2.1.1" + "@wordpress/scripts@~30.0.0": version "30.0.6" resolved "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.0.6.tgz" @@ -3727,6 +3745,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"