diff --git a/.env.integration b/.env.integration new file mode 100644 index 000000000..8ed93a99d --- /dev/null +++ b/.env.integration @@ -0,0 +1,40 @@ +PPCP_INTEGRATION_WP_DIR=${ROOT_DIR}/.ddev/wordpress + +BASEURL="https://woocommerce-paypal-payments.ddev.site" +AUTHORIZATION="Bearer ABC123" + +CHECKOUT_URL="/checkout" +CHECKOUT_PAGE_ID=7 +CART_URL="/cart" +BLOCK_CHECKOUT_URL="/checkout-block" +BLOCK_CHECKOUT_PAGE_ID=22 +BLOCK_CART_URL="/cart-block" + +PRODUCT_URL="/product/prod" +PRODUCT_ID=123 + +SUBSCRIPTION_URL="/product/sub" + +PAYPAL_SUBSCRIPTIONS_PRODUCT_ID=252 + +APM_ID="sofort" + +WP_MERCHANT_USER="admin" +WP_MERCHANT_PASSWORD="admin" + +WP_CUSTOMER_USER="customer" +WP_CUSTOMER_PASSWORD="password" + +CUSTOMER_EMAIL="customer@example.com" +CUSTOMER_PASSWORD="password" +CUSTOMER_FIRST_NAME="John" +CUSTOMER_LAST_NAME="Doe" +CUSTOMER_COUNTRY="DE" +CUSTOMER_ADDRESS="street 1" +CUSTOMER_POSTCODE="12345" +CUSTOMER_CITY="city" +CUSTOMER_PHONE="1234567890" + +CREDIT_CARD_NUMBER="1234567890" +CREDIT_CARD_EXPIRATION="01/2042" +CREDIT_CARD_CVV="123" diff --git a/.env.e2e.example b/.env.integration.example similarity index 93% rename from .env.e2e.example rename to .env.integration.example index 98ad27c23..d4d541f6a 100644 --- a/.env.e2e.example +++ b/.env.integration.example @@ -1,4 +1,4 @@ -PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress +PPCP_INTEGRATION_WP_DIR=${ROOT_DIR}/.ddev/wordpress BASEURL="https://woocommerce-paypal-payments.ddev.site" AUTHORIZATION="Bearer ABC123" diff --git a/.github/workflows/e2e.yml b/.github/workflows/integration.yml similarity index 71% rename from .github/workflows/e2e.yml rename to .github/workflows/integration.yml index 0dfb99455..716a4a0b7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/integration.yml @@ -1,4 +1,4 @@ -name: e2e tests +name: Integration tests on: workflow_dispatch @@ -7,8 +7,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['7.4', '8.2'] - wc-versions: ['6.9.4', '7.7.2'] + php-versions: ['7.4'] + wc-versions: ['9.7.1'] name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: @@ -30,10 +30,10 @@ jobs: run: ddev orchestrate -f - name: Create config - run: cp -n .env.e2e.example .env.e2e + run: cp -n .env.integration.example .env.integration - name: Setup tests - run: ddev php tests/e2e/PHPUnit/setup.php + run: ddev php tests/integration/PHPUnit/setup.php - name: Run PHPUnit - run: ddev exec phpunit -c tests/e2e/phpunit.xml.dist + run: ddev exec phpunit -c tests/integration/phpunit.xml.dist diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 56ba72451..27484ec28 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -94,6 +94,45 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = return 0; } +/** + * Retrieves the number of times a filter has been applied during the current request. + * + * @since 6.1.0 + * + * @global int[] $wp_filters Stores the number of times each filter was triggered. + * + * @param string $hook_name The name of the filter hook. + * @return int The number of times the filter hook has been applied. + */ +function did_filter( $hook_name ) { + return 0; +} + +/** + * Returns whether or not a filter hook is currently being processed. + * + * The function current_filter() only returns the most recent filter being executed. + * did_filter() returns the number of times a filter has been applied during + * the current request. + * + * This function allows detection for any filter currently being executed + * (regardless of whether it's the most recent filter to fire, in the case of + * hooks called from hook callbacks) to be verified. + * + * @since 3.9.0 + * + * @see current_filter() + * @see did_filter() + * @global string[] $wp_current_filter Current filter. + * + * @param string|null $hook_name Optional. Filter hook to check. Defaults to null, + * which checks if any filter is currently being run. + * @return bool Whether the filter is currently in the stack. + */ +function doing_filter( $hook_name = null ) { + return false; +} + /** * HTML API: WP_HTML_Tag_Processor class */ diff --git a/changelog.txt b/changelog.txt index 2e1d3420c..fc791d0dd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,21 @@ *** Changelog *** += 3.0.0 - 2025-03-17 = +* Enhancement - Redesigned settings UI for new users #2908 +* Enhancement - Enable Fastlane by default on new store setups when eligible #3199 +* Enhancement - Enable support for advanced card payments and features for Hong Kong & Singapore #3089 +* Fix - Dependency conflict with more recent psr/log versions on PHP8+ #2993 +* Fix - PayPal Checkout Gateway subscription migration layer not renewing subscriptions #2699 +* Fix - Fatal error when gateway settings initialized too early by third-party plugin #2766 +* Fix - Next Payment date for Subscriptions not updating when processing a PayPal Subscriptions renewal order #2959 +* Fix - Changing the subscription payment method to ACDC triggers error #2891 +* Fix - Standard Card button not appearing in standalone gateway for free trial subscription products #2935 +* Fix - Validation error when using Trustly payment method #3031 +* Fix - Error in continuation mode due to wrong gateway selection on Checkout block #2996 +* Fix - Error in error in PayLaterConfigurator #2989 +* Tweak - Removed currency requirement for Vault v3 #2919 +* Tweak - Update plugin author from WooCommerce to PayPal + = 2.9.6 - 2025-01-06 = * Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955 * Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872 diff --git a/composer.json b/composer.json index 17ecd3eda..8443e53f1 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "autoload-dev": { "psr-4": { "WooCommerce\\PayPalCommerce\\": "tests/PHPUnit/", - "WooCommerce\\PayPalCommerce\\Tests\\E2e\\": "tests/e2e/PHPUnit/" + "WooCommerce\\PayPalCommerce\\Tests\\Integration\\": "tests/integration/PHPUnit/" } }, "minimum-stability": "dev", diff --git a/modules.php b/modules.php index 21a4e645e..0d65644f2 100644 --- a/modules.php +++ b/modules.php @@ -91,9 +91,12 @@ return function ( string $root_dir ): iterable { $modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); } + $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' ); + $preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' ); + if ( apply_filters( 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', - getenv( 'PCP_SETTINGS_ENABLED' ) === '1' + $show_new_ux || $preview_new_ux ) ) { $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); } diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 272bfdaf7..bfadb2cda 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -80,12 +80,11 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; return array( 'api.host' => static function( ContainerInterface $container ) : string { - $environment = $container->get( 'onboarding.environment' ); + $environment = $container->get( 'settings.environment' ); assert( $environment instanceof Environment ); if ( $environment->is_sandbox() ) { @@ -671,6 +670,7 @@ return array( 'FR' => $default_currencies, 'DE' => $default_currencies, 'GR' => $default_currencies, + 'HK' => $default_currencies, 'HU' => $default_currencies, 'IE' => $default_currencies, 'IT' => $default_currencies, @@ -688,6 +688,7 @@ return array( 'PT' => $default_currencies, 'RO' => $default_currencies, 'SK' => $default_currencies, + 'SG' => $default_currencies, 'SI' => $default_currencies, 'ES' => $default_currencies, 'SE' => $default_currencies, @@ -736,6 +737,7 @@ return array( 'FR' => $mastercard_visa_amex, 'GB' => $mastercard_visa_amex, 'GR' => $mastercard_visa_amex, + 'HK' => $mastercard_visa_amex, 'HU' => $mastercard_visa_amex, 'IE' => $mastercard_visa_amex, 'IT' => $mastercard_visa_amex, @@ -765,6 +767,7 @@ return array( 'SE' => $mastercard_visa_amex, 'SI' => $mastercard_visa_amex, 'SK' => $mastercard_visa_amex, + 'SG' => $mastercard_visa_amex, 'JP' => array( 'mastercard' => array(), 'visa' => array(), diff --git a/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php index b7624d2de..2a5f321bc 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php @@ -160,7 +160,7 @@ class PartnersEndpoint { $this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY ); - $status = $this->seller_status_factory->from_paypal_reponse( $json ); + $status = $this->seller_status_factory->from_paypal_response( $json ); return $status; } } diff --git a/modules/ppcp-api-client/src/Entity/SellerStatus.php b/modules/ppcp-api-client/src/Entity/SellerStatus.php index 5b972efc7..fd948b276 100644 --- a/modules/ppcp-api-client/src/Entity/SellerStatus.php +++ b/modules/ppcp-api-client/src/Entity/SellerStatus.php @@ -28,15 +28,23 @@ class SellerStatus { */ private $capabilities; + /** + * Merchant country on PayPal. + * + * @var string + */ + private string $country; + /** * SellerStatus constructor. * * @param SellerStatusProduct[] $products The products. * @param SellerStatusCapability[] $capabilities The capabilities. + * @param string $country Merchant country on PayPal. * * @psalm-suppress RedundantConditionGivenDocblockType */ - public function __construct( array $products, array $capabilities ) { + public function __construct( array $products, array $capabilities, string $country = '' ) { foreach ( $products as $key => $product ) { if ( is_a( $product, SellerStatusProduct::class ) ) { continue; @@ -52,6 +60,7 @@ class SellerStatus { $this->products = $products; $this->capabilities = $capabilities; + $this->country = $country; } /** @@ -73,7 +82,16 @@ class SellerStatus { } /** - * Returns the enitity as array. + * Returns merchant's country on PayPal. + * + * @return string + */ + public function country() : string { + return $this->country; + } + + /** + * Returns the entity as array. * * @return array */ @@ -95,6 +113,7 @@ class SellerStatus { return array( 'products' => $products, 'capabilities' => $capabilities, + 'country' => $this->country, ); } } diff --git a/modules/ppcp-api-client/src/Factory/SellerStatusFactory.php b/modules/ppcp-api-client/src/Factory/SellerStatusFactory.php index 9f1ff31f4..4182b5815 100644 --- a/modules/ppcp-api-client/src/Factory/SellerStatusFactory.php +++ b/modules/ppcp-api-client/src/Factory/SellerStatusFactory.php @@ -25,7 +25,7 @@ class SellerStatusFactory { * * @return SellerStatus */ - public function from_paypal_reponse( \stdClass $json ) : SellerStatus { + public function from_paypal_response( \stdClass $json ) : SellerStatus { $products = array_map( function( $json ) : SellerStatusProduct { $product = new SellerStatusProduct( @@ -49,6 +49,6 @@ class SellerStatusFactory { isset( $json->capabilities ) ? (array) $json->capabilities : array() ); - return new SellerStatus( $products, $capabilities ); + return new SellerStatus( $products, $capabilities, $json->country ?? '' ); } } diff --git a/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php b/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php index af8bae174..d214c9df7 100644 --- a/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php +++ b/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php @@ -5,7 +5,7 @@ * @package WooCommerce\PayPalCommerce\ApiClient\Repository */ -declare(strict_types=1); +declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\ApiClient\Repository; @@ -18,43 +18,21 @@ class PartnerReferralsData { /** * The DCC Applies Helper object. * + * @deprecated Deprecates with the new UI. In this class, the products are + * always explicit, and should not be deducted from the + * DccApplies state at this point. + * Remove this with the legacy UI code. * @var DccApplies */ - private $dcc_applies; - - /** - * The list of products ('PPCP', 'EXPRESS_CHECKOUT'). - * - * @var string[] - */ - private $products; + private DccApplies $dcc_applies; /** * PartnerReferralsData constructor. * * @param DccApplies $dcc_applies The DCC Applies helper. */ - public function __construct( - DccApplies $dcc_applies - ) { + public function __construct( DccApplies $dcc_applies ) { $this->dcc_applies = $dcc_applies; - $this->products = array( - $this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT', - ); - } - - /** - * Returns a new copy of this object with the given value set. - * - * @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT'). - * @return static - */ - public function with_products( array $products ): self { - $obj = clone $this; - - $obj->products = $products; - - return $obj; } /** @@ -62,82 +40,120 @@ class PartnerReferralsData { * * @return string */ - public function nonce(): string { + public function nonce() : string { return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG'; } /** * Returns the data. * + * @param string[] $products The list of products to use ('PPCP', 'EXPRESS_CHECKOUT'). + * Default is based on DCC availability. + * @param string $onboarding_token A security token to finalize the onboarding process. + * @param bool $use_subscriptions If the merchant requires subscription features. + * @param bool $use_card_payments If the merchant wants to process credit card payments. * @return array */ - public function data(): array { + public function data( array $products = array(), string $onboarding_token = '', bool $use_subscriptions = null, bool $use_card_payments = true ) : array { + if ( ! $products ) { + $products = array( + $this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT', + ); + } + /** - * Returns the partners referrals data. + * Filter the return-URL, which is called at the end of the OAuth onboarding + * process, when the merchant clicks the "Return to your shop" button. */ - return apply_filters( - 'ppcp_partner_referrals_data', - array( - 'partner_config_override' => array( - /** - * Returns the URL which will be opened at the end of onboarding. - */ - 'return_url' => apply_filters( - 'woocommerce_paypal_payments_partner_config_override_return_url', - admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) - ), - /** - * Returns the description of the URL which will be opened at the end of onboarding. - */ - 'return_url_description' => apply_filters( - 'woocommerce_paypal_payments_partner_config_override_return_url_description', - __( 'Return to your shop.', 'woocommerce-paypal-payments' ) - ), - 'show_add_credit_card' => true, + $return_url = apply_filters( + 'woocommerce_paypal_payments_partner_config_override_return_url', + admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) + ); + + /** + * Filter the label of the "Return to your shop" button. + * It's displayed on the very last page of the onboarding popup. + */ + $return_url_label = apply_filters( + 'woocommerce_paypal_payments_partner_config_override_return_url_description', + __( 'Return to your shop.', 'woocommerce-paypal-payments' ) + ); + + $capabilities = array(); + $first_party_features = array( + 'PAYMENT', + 'REFUND', + 'ADVANCED_TRANSACTIONS_SEARCH', + 'TRACKING_SHIPMENT_READWRITE', + ); + + if ( true === $use_subscriptions ) { + $capabilities[] = 'PAYPAL_WALLET_VAULTING_ADVANCED'; + $first_party_features[] = 'BILLING_AGREEMENT'; + } + + // Backwards compatibility. Keep those features in the legacy UI (null-value). + // Move this into the previous condition, once legacy code is removed. + if ( false !== $use_subscriptions ) { + $first_party_features[] = 'FUTURE_PAYMENT'; + $first_party_features[] = 'VAULT'; + } + + if ( false === $use_subscriptions ) { + // Only use "ADVANCED_VAULTING" product for onboarding with subscriptions. + $products = array_filter( + $products, + static fn( $product ) => $product !== 'ADVANCED_VAULTING' + ); + } + + $payload = array( + 'partner_config_override' => array( + 'return_url' => $return_url, + 'return_url_description' => $return_url_label, + 'show_add_credit_card' => $use_card_payments, + ), + 'products' => $products, + 'capabilities' => $capabilities, + 'legal_consents' => array( + array( + 'type' => 'SHARE_DATA_CONSENT', + 'granted' => true, ), - 'products' => $this->products, - 'legal_consents' => array( - array( - 'type' => 'SHARE_DATA_CONSENT', - 'granted' => true, - ), - ), - 'operations' => array( - array( - 'operation' => 'API_INTEGRATION', - 'api_integration_preference' => array( - 'rest_api_integration' => array( - 'integration_method' => 'PAYPAL', - 'integration_type' => 'FIRST_PARTY', - 'first_party_details' => array( - 'features' => array( - 'PAYMENT', - 'FUTURE_PAYMENT', - 'REFUND', - 'ADVANCED_TRANSACTIONS_SEARCH', - 'VAULT', - 'TRACKING_SHIPMENT_READWRITE', - ), - 'seller_nonce' => $this->nonce(), - ), + ), + 'operations' => array( + array( + 'operation' => 'API_INTEGRATION', + 'api_integration_preference' => array( + 'rest_api_integration' => array( + 'integration_method' => 'PAYPAL', + 'integration_type' => 'FIRST_PARTY', + 'first_party_details' => array( + 'features' => $first_party_features, + 'seller_nonce' => $this->nonce(), ), ), ), ), - ) + ), ); - } - /** - * Append the validation token to the return_url - * - * @param array $data The referral data. - * @param string $token The token to be appended. - * @return array - */ - public function append_onboarding_token( array $data, string $token ): array { - $data['partner_config_override']['return_url'] = - add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] ); - return $data; + /** + * Filter the final partners referrals data collection. + */ + $payload = apply_filters( 'ppcp_partner_referrals_data', $payload ); + + // An empty array is not permitted. + if ( isset( $payload['capabilities'] ) && ! $payload['capabilities'] ) { + unset( $payload['capabilities'] ); + } + + // Add the nonce in the end, to maintain backwards compatibility of filters. + $payload['partner_config_override']['return_url'] = add_query_arg( + array( 'ppcpToken' => $onboarding_token ), + $payload['partner_config_override']['return_url'] + ); + + return $payload; } } diff --git a/modules/ppcp-applepay/extensions.php b/modules/ppcp-applepay/extensions.php index 53068c506..6fcbc12f4 100644 --- a/modules/ppcp-applepay/extensions.php +++ b/modules/ppcp-applepay/extensions.php @@ -42,7 +42,7 @@ return array( assert( $display_manager instanceof DisplayManager ); // Domain registration. - $env = $container->get( 'onboarding.environment' ); + $env = $container->get( 'settings.environment' ); assert( $env instanceof Environment ); $domain_registration_url = 'https://www.paypal.com/uccservicing/apm/applepay'; diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index 586a300ae..f78623efb 100644 --- a/modules/ppcp-applepay/services.php +++ b/modules/ppcp-applepay/services.php @@ -20,7 +20,6 @@ use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies; use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; return array( @@ -191,6 +190,7 @@ return array( 'FR', // France 'DE', // Germany 'GR', // Greece + 'HK', // Hong Kong 'HU', // Hungary 'IE', // Ireland 'IT', // Italy @@ -204,6 +204,7 @@ return array( 'PL', // Poland 'PT', // Portugal 'RO', // Romania + 'SG', // Singapore 'SK', // Slovakia 'SI', // Slovenia 'ES', // Spain @@ -233,6 +234,7 @@ return array( 'CZK', // Czech Koruna 'DKK', // Danish Krone 'EUR', // Euro + 'HKD', // Hong Kong Dollar 'GBP', // British Pound Sterling 'HUF', // Hungarian Forint 'ILS', // Israeli New Shekel @@ -242,6 +244,7 @@ return array( 'NZD', // New Zealand Dollar 'PHP', // Philippine Peso 'PLN', // Polish Zloty + 'SGD', // Singapur-Dollar 'SEK', // Swedish Krona 'THB', // Thai Baht 'TWD', // New Taiwan Dollar @@ -260,15 +263,15 @@ return array( }, 'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string { - $state = $container->get( 'onboarding.state' ); - if ( $state->current_state() < State::STATE_ONBOARDED ) { + $is_connected = $container->get( 'settings.flag.is-connected' ); + if ( ! $is_connected ) { return ''; } $product_status = $container->get( 'applepay.apple-product-status' ); assert( $product_status instanceof AppleProductStatus ); - $environment = $container->get( 'onboarding.environment' ); + $environment = $container->get( 'settings.environment' ); assert( $environment instanceof Environment ); $enabled = $product_status->is_active(); diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index 8015e0a4d..df0d06adf 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -368,7 +368,7 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule if ( ! $button->is_enabled() ) { return; } - $env = $c->get( 'onboarding.environment' ); + $env = $c->get( 'settings.environment' ); assert( $env instanceof Environment ); $is_sandobx = $env->current_environment_is( Environment::SANDBOX ); $this->load_domain_association_file( $is_sandobx ); diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 22663d605..045c234cb 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -1018,6 +1018,10 @@ class ApplePayButton implements ButtonInterface { * @return void */ public function enqueue(): void { + if ( ! $this->is_enabled() ) { + return; + } + wp_register_script( 'wc-ppcp-applepay', untrailingslashit( $this->module_url ) . '/assets/js/boot.js', diff --git a/modules/ppcp-applepay/src/Assets/AppleProductStatus.php b/modules/ppcp-applepay/src/Assets/AppleProductStatus.php index 61668c26d..c7ff102fe 100644 --- a/modules/ppcp-applepay/src/Assets/AppleProductStatus.php +++ b/modules/ppcp-applepay/src/Assets/AppleProductStatus.php @@ -89,6 +89,7 @@ class AppleProductStatus extends ProductStatus { } } + // Settings used as a cache; `settings->set` is compatible with new UI. if ( $has_capability ) { $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); } else { diff --git a/modules/ppcp-axo-block/services.php b/modules/ppcp-axo-block/services.php index 6945215ba..b93513148 100644 --- a/modules/ppcp-axo-block/services.php +++ b/modules/ppcp-axo-block/services.php @@ -36,7 +36,7 @@ return array( fn(): SmartButtonInterface => $container->get( 'button.smart-button' ), $container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.configuration.dcc' ), - $container->get( 'onboarding.environment' ), + $container->get( 'settings.environment' ), $container->get( 'wcgateway.url' ), $container->get( 'axo.payment_method_selected_map' ), $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 af94976a3..cd5b0fa25 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -107,11 +107,9 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule 'woocommerce_blocks_payment_method_type_registration', function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { /* - * Only register the method if we are not in the admin - * (to avoid two Debit & Credit Cards gateways in the - * checkout block in the editor: one from ACDC one from Axo). + * Only register the method if we are not in the admin or the customer is not logged in. */ - if ( ! is_admin() ) { + if ( ! is_user_logged_in() ) { $payment_method_registry->register( $c->get( 'axoblock.method' ) ); } } diff --git a/modules/ppcp-axo/extensions.php b/modules/ppcp-axo/extensions.php index d41829726..a834dcb0d 100644 --- a/modules/ppcp-axo/extensions.php +++ b/modules/ppcp-axo/extensions.php @@ -10,7 +10,6 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Axo; use WooCommerce\PayPalCommerce\Axo\Helper\NoticeRenderer; -use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager; diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php index ca427700d..7db0b0dc5 100644 --- a/modules/ppcp-axo/services.php +++ b/modules/ppcp-axo/services.php @@ -12,7 +12,7 @@ namespace WooCommerce\PayPalCommerce\Axo; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies; -use WooCommerce\PayPalCommerce\Axo\Helper\SettingsNoticeGenerator; +use WooCommerce\PayPalCommerce\Axo\Helper\CompatibilityChecker; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -38,8 +38,8 @@ return array( ); }, - 'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator { - return new SettingsNoticeGenerator( $container->get( 'axo.fastlane-incompatible-plugin-names' ) ); + 'axo.helpers.compatibility-checker' => static function ( ContainerInterface $container ) : CompatibilityChecker { + return new CompatibilityChecker( $container->get( 'axo.fastlane-incompatible-plugin-names' ) ); }, // If AXO is configured and onboarded. @@ -66,7 +66,7 @@ return array( $container->get( 'ppcp.asset-version' ), $container->get( 'session.handler' ), $container->get( 'wcgateway.settings' ), - $container->get( 'onboarding.environment' ), + $container->get( 'settings.environment' ), $container->get( 'axo.insights' ), $container->get( 'wcgateway.settings.status' ), $container->get( 'api.shop.currency.getter' ), @@ -89,7 +89,7 @@ return array( $container->get( 'api.factory.purchase-unit' ), $container->get( 'api.factory.shipping-preference' ), $container->get( 'wcgateway.transaction-url-provider' ), - $container->get( 'onboarding.environment' ), + $container->get( 'settings.environment' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, @@ -190,29 +190,44 @@ return array( ); }, 'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string { - $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); - assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); + $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' ); + assert( $compatibility_checker instanceof CompatibilityChecker ); $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - return $settings_notice_generator->generate_settings_conflict_notice( $settings ); + return $compatibility_checker->generate_settings_conflict_notice( $settings ); }, 'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string { - $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); - assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); + $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' ); + assert( $compatibility_checker instanceof CompatibilityChecker ); - return $settings_notice_generator->generate_checkout_notice(); + return $compatibility_checker->generate_checkout_notice(); + }, + + 'axo.checkout-config-notice.raw' => static function ( ContainerInterface $container ) : string { + $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' ); + assert( $compatibility_checker instanceof CompatibilityChecker ); + + return $compatibility_checker->generate_checkout_notice( true ); }, 'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string { - $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); - assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); + $settings_notice_generator = $container->get( 'axo.helpers.compatibility-checker' ); + assert( $settings_notice_generator instanceof CompatibilityChecker ); return $settings_notice_generator->generate_incompatible_plugins_notice(); }, + 'axo.incompatible-plugins-notice.raw' => static function ( ContainerInterface $container ) : string { + $settings_notice_generator = new CompatibilityChecker( + $container->get( 'axo.fastlane-incompatible-plugin-names' ) + ); + + return $settings_notice_generator->generate_incompatible_plugins_notice( true ); + }, + 'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string { $dcc_configuration = $container->get( 'wcgateway.configuration.dcc' ); assert( $dcc_configuration instanceof DCCGatewayConfiguration ); diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index 21ee84a35..5624273a3 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -475,7 +475,7 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { * @return void */ private function add_feature_detection_tag( bool $axo_enabled ) { - $show_tag = is_checkout() || is_cart() || is_shop(); + $show_tag = is_home() || is_checkout() || is_cart() || is_shop(); if ( ! $show_tag ) { return; diff --git a/modules/ppcp-axo/src/Helper/CompatibilityChecker.php b/modules/ppcp-axo/src/Helper/CompatibilityChecker.php new file mode 100644 index 000000000..f06b3e144 --- /dev/null +++ b/modules/ppcp-axo/src/Helper/CompatibilityChecker.php @@ -0,0 +1,254 @@ +incompatible_plugin_names = $incompatible_plugin_names; + $this->checkout_compatibility = array( + 'has_elementor_checkout' => null, + 'has_classic_checkout' => null, + 'has_block_checkout' => null, + ); + } + + /** + * Checks if the checkout uses Elementor. + * + * @return bool Whether the checkout uses Elementor. + */ + protected function has_elementor_checkout(): bool { + if ( $this->checkout_compatibility['has_elementor_checkout'] === null ) { + $this->checkout_compatibility['has_elementor_checkout'] = CartCheckoutDetector::has_elementor_checkout(); + } + + return $this->checkout_compatibility['has_elementor_checkout']; + } + + /** + * Checks if the checkout uses classic checkout. + * + * @return bool Whether the checkout uses classic checkout. + */ + protected function has_classic_checkout(): bool { + if ( $this->checkout_compatibility['has_classic_checkout'] === null ) { + $this->checkout_compatibility['has_classic_checkout'] = CartCheckoutDetector::has_classic_checkout(); + } + + return $this->checkout_compatibility['has_classic_checkout']; + } + + /** + * Checks if the checkout uses block checkout. + * + * @return bool Whether the checkout uses block checkout. + */ + protected function has_block_checkout(): bool { + if ( $this->checkout_compatibility['has_block_checkout'] === null ) { + $this->checkout_compatibility['has_block_checkout'] = CartCheckoutDetector::has_block_checkout(); + } + + return $this->checkout_compatibility['has_block_checkout']; + } + + /** + * Checks if DCC is enabled. + * + * @param Settings $settings The plugin settings container. + * @return bool Whether DCC is enabled. + */ + protected function is_dcc_enabled( Settings $settings ): bool { + if ( $this->is_dcc_enabled === null ) { + try { + $this->is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ); + } catch ( NotFoundException $ignored ) { + $this->is_dcc_enabled = false; + } + } + + return $this->is_dcc_enabled; + } + + /** + * Generates the full HTML of the notification. + * + * @param string $message HTML of the inner message contents. + * @param bool $is_error Whether the provided message is an error. Affects the notice color. + * @param bool $raw_message Whether to return raw message without HTML wrappers. + * + * @return string The full HTML code of the notification, or an empty string, or raw message. + */ + private function render_notice( string $message, bool $is_error = false, bool $raw_message = false ) : string { + if ( ! $message ) { + return ''; + } + + if ( $raw_message ) { + return $message; + } + + return sprintf( + '

%2$s

', + $is_error ? 'ppcp-notice-error' : '', + $message + ); + } + + /** + * Check if there aren't any incompatibilities that would prevent Fastlane from working properly. + * + * @return bool Whether the setup is compatible. + */ + public function is_fastlane_compatible(): bool { + // Check for incompatible plugins. + if ( ! empty( $this->incompatible_plugin_names ) ) { + return false; + } + + // Check for checkout page incompatibilities. + if ( $this->has_elementor_checkout() ) { + return false; + } + + if ( ! $this->has_classic_checkout() && ! $this->has_block_checkout() ) { + return false; + } + + // No incompatibilities found. + return true; + } + + /** + * Generates the checkout notice. + * + * @param bool $raw_message Whether to return raw message without HTML wrappers. + * @return string + */ + public function generate_checkout_notice( bool $raw_message = false ): string { + $notice_content = ''; + + // Check for checkout incompatibilities. + $has_checkout_incompatibility = $this->has_elementor_checkout() || + ( ! $this->has_classic_checkout() && ! $this->has_block_checkout() ); + + if ( ! $has_checkout_incompatibility ) { + return ''; + } + + $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); + $block_checkout_docs_link = __( + 'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/#using-the-cart-and-checkout-blocks', + 'woocommerce-paypal-payments' + ); + + if ( $this->has_elementor_checkout() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ + __( + 'Warning: The Checkout page of your store currently uses the Elementor Checkout widget. To enable Fastlane and accelerate payments, the page must include either the Checkout block, Classic Checkout, or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the Checkout block.', + 'woocommerce-paypal-payments' + ), + esc_url( $checkout_page_link ), + esc_url( $block_checkout_docs_link ) + ); + } elseif ( ! $this->has_classic_checkout() && ! $this->has_block_checkout() ) { + $notice_content = sprintf( + /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ + __( + 'Warning: The Checkout page of your store does not seem to be properly configured or uses an incompatible third-party Checkout solution. To enable Fastlane and accelerate payments, the page must include either the Checkout block, Classic Checkout, or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the Checkout block.', + 'woocommerce-paypal-payments' + ), + esc_url( $checkout_page_link ), + esc_url( $block_checkout_docs_link ) + ); + } + + return $this->render_notice( $notice_content, true, $raw_message ); + } + + /** + * Generates the incompatible plugins notice. + * + * @param bool $raw_message Whether to return raw message without HTML wrappers. + * @return string + */ + public function generate_incompatible_plugins_notice( bool $raw_message = false ): string { + if ( empty( $this->incompatible_plugin_names ) ) { + return ''; + } + + $plugins_settings_link = esc_url( admin_url( 'plugins.php' ) ); + $notice_content = sprintf( + /* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */ + __( + 'Note: The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following active plugins: ', + 'woocommerce-paypal-payments' + ), + $plugins_settings_link, + implode( '', $this->incompatible_plugin_names ) + ); + + return $this->render_notice( $notice_content, false, $raw_message ); + } + + /** + * Generates a warning notice with instructions on conflicting plugin-internal settings. + * + * @param Settings $settings The plugin settings container, which is checked for conflicting values. + * @param bool $raw_message Whether to return raw message without HTML wrappers. + * @return string + */ + public function generate_settings_conflict_notice( Settings $settings, bool $raw_message = false ) : string { + if ( $this->is_dcc_enabled( $settings ) ) { + return ''; + } + + $notice_content = __( + 'Warning: To enable Fastlane and accelerate payments, the Advanced Card Processing payment method must also be enabled.', + 'woocommerce-paypal-payments' + ); + + return $this->render_notice( $notice_content, true, $raw_message ); + } +} diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php deleted file mode 100644 index 6e3872b81..000000000 --- a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php +++ /dev/null @@ -1,147 +0,0 @@ -incompatible_plugin_names = $incompatible_plugin_names; - } - - /** - * Generates the full HTML of the notification. - * - * @param string $message HTML of the inner message contents. - * @param bool $is_error Whether the provided message is an error. Affects the notice color. - * - * @return string The full HTML code of the notification, or an empty string. - */ - private function render_notice( string $message, bool $is_error = false ) : string { - if ( ! $message ) { - return ''; - } - - return sprintf( - '

%2$s

', - $is_error ? 'ppcp-notice-error' : '', - $message - ); - } - - /** - * Generates the checkout notice. - * - * @return string - */ - public function generate_checkout_notice(): string { - $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' ); - $block_checkout_docs_link = __( - 'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/#using-the-cart-and-checkout-blocks', - 'woocommerce-paypal-payments' - ); - - $notice_content = ''; - - if ( CartCheckoutDetector::has_elementor_checkout() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ - __( - 'Warning: The Checkout page of your store currently uses the Elementor Checkout widget. To enable Fastlane and accelerate payments, the page must include either the Checkout block, Classic Checkout, or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the Checkout block.', - 'woocommerce-paypal-payments' - ), - esc_url( $checkout_page_link ), - esc_url( $block_checkout_docs_link ) - ); - } elseif ( ! CartCheckoutDetector::has_classic_checkout() && ! CartCheckoutDetector::has_block_checkout() ) { - $notice_content = sprintf( - /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */ - __( - 'Warning: The Checkout page of your store does not seem to be properly configured or uses an incompatible third-party Checkout solution. To enable Fastlane and accelerate payments, the page must include either the Checkout block, Classic Checkout, or the [woocommerce_checkout] shortcode. See this page for instructions on how to switch to the Checkout block.', - 'woocommerce-paypal-payments' - ), - esc_url( $checkout_page_link ), - esc_url( $block_checkout_docs_link ) - ); - } - - return $notice_content ? '

' . $notice_content . '

' : ''; - } - - /** - * Generates the incompatible plugins notice. - * - * @return string - */ - public function generate_incompatible_plugins_notice(): string { - if ( empty( $this->incompatible_plugin_names ) ) { - return ''; - } - - $plugins_settings_link = esc_url( admin_url( 'plugins.php' ) ); - $notice_content = sprintf( - /* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */ - __( - 'Note: The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following active plugins: ', - 'woocommerce-paypal-payments' - ), - $plugins_settings_link, - implode( '', $this->incompatible_plugin_names ) - ); - - return '

' . $notice_content . '

'; - } - - /** - * Generates a warning notice with instructions on conflicting plugin-internal settings. - * - * @param Settings $settings The plugin settings container, which is checked for conflicting - * values. - * @return string - */ - public function generate_settings_conflict_notice( Settings $settings ) : string { - $notice_content = ''; - $is_dcc_enabled = false; - - try { - $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ); - // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch - } catch ( NotFoundException $ignored ) { - // Never happens. - } - - if ( ! $is_dcc_enabled ) { - $notice_content = __( - 'Warning: To enable Fastlane and accelerate payments, the Advanced Card Processing payment method must also be enabled.', - 'woocommerce-paypal-payments' - ); - } - - return $this->render_notice( $notice_content, true ); - } -} diff --git a/modules/ppcp-blocks/resources/js/Components/paypal.js b/modules/ppcp-blocks/resources/js/Components/paypal.js index c9bb1ad88..e0c8a7529 100644 --- a/modules/ppcp-blocks/resources/js/Components/paypal.js +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -67,27 +67,6 @@ export const PayPalComponent = ( { ? `${ config.id }-${ fundingSource }` : config.id; - /** - * The block cart displays express checkout buttons. Those buttons are handled by the - * PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons"). - * - * A possible bug in WooCommerce does not use the correct payment method ID for the express - * payment buttons inside the cart, but sends the ID of the _first_ active payment method. - * - * This function uses an internal WooCommerce dispatcher method to set the correct method ID. - */ - const enforcePaymentMethodForCart = () => { - // Do nothing, unless we're handling block cart express payment buttons. - if ( 'cart-block' !== config.scriptData.context ) { - return; - } - - // Set the active payment method to PAYPAL_GATEWAY_ID. - wp.data - .dispatch( 'wc/store/payment' ) - .__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} ); - }; - useEffect( () => { // fill the form if in continuation (for product or mini-cart buttons) if ( continuationFilled || ! config.scriptData.continuation?.order ) { @@ -339,7 +318,6 @@ export const PayPalComponent = ( { shouldskipFinalConfirmation, getCheckoutRedirectUrl, setGotoContinuationOnError, - enforcePaymentMethodForCart, onSubmit, onError, onClose @@ -439,7 +417,6 @@ export const PayPalComponent = ( { shouldskipFinalConfirmation, getCheckoutRedirectUrl, setGotoContinuationOnError, - enforcePaymentMethodForCart, onSubmit, onError, onClose @@ -476,7 +453,6 @@ export const PayPalComponent = ( { shouldskipFinalConfirmation, getCheckoutRedirectUrl, setGotoContinuationOnError, - enforcePaymentMethodForCart, onSubmit, onError, onClose diff --git a/modules/ppcp-blocks/resources/js/Helper/Address.js b/modules/ppcp-blocks/resources/js/Helper/Address.js index 6d514d51a..28db1d631 100644 --- a/modules/ppcp-blocks/resources/js/Helper/Address.js +++ b/modules/ppcp-blocks/resources/js/Helper/Address.js @@ -81,12 +81,14 @@ export const paypalShippingToWc = ( shipping ) => { export const paypalPayerToWc = ( payer ) => { const firstName = payer?.name?.given_name ?? ''; const lastName = payer?.name?.surname ?? ''; + const phone = payer?.phone?.phone_number?.national_number ?? ''; const address = payer.address ? paypalAddressToWc( payer.address ) : {}; return { ...address, first_name: firstName, last_name: lastName, email: payer.email_address, + phone: phone }; }; diff --git a/modules/ppcp-blocks/resources/js/paypal-config.js b/modules/ppcp-blocks/resources/js/paypal-config.js index d78ee14db..363ee5078 100644 --- a/modules/ppcp-blocks/resources/js/paypal-config.js +++ b/modules/ppcp-blocks/resources/js/paypal-config.js @@ -61,7 +61,6 @@ export const handleApprove = async ( shouldskipFinalConfirmation, getCheckoutRedirectUrl, setGotoContinuationOnError, - enforcePaymentMethodForCart, onSubmit, onError, onClose @@ -132,7 +131,6 @@ export const handleApprove = async ( location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError( true ); - enforcePaymentMethodForCart(); onSubmit(); } } catch ( err ) { @@ -171,7 +169,6 @@ export const handleApproveSubscription = async ( shouldskipFinalConfirmation, getCheckoutRedirectUrl, setGotoContinuationOnError, - enforcePaymentMethodForCart, onSubmit, onError, onClose @@ -242,7 +239,6 @@ export const handleApproveSubscription = async ( location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError( true ); - enforcePaymentMethodForCart(); onSubmit(); } } catch ( err ) { diff --git a/modules/ppcp-blocks/services.php b/modules/ppcp-blocks/services.php index e4226ca2a..996240a79 100644 --- a/modules/ppcp-blocks/services.php +++ b/modules/ppcp-blocks/services.php @@ -28,13 +28,6 @@ return array( ); }, 'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod { - /** - * Cart instance; might be null, esp. in customizer or in Block Editor. - * - * @var null|WC_Cart $cart - */ - $cart = WC()->cart; - return new PayPalPaymentMethod( $container->get( 'blocks.url' ), $container->get( 'ppcp.asset-version' ), @@ -53,7 +46,6 @@ return array( $container->get( 'wcgateway.place-order-button-text' ), $container->get( 'wcgateway.place-order-button-description' ), $container->get( 'wcgateway.all-funding-sources' ), - $cart && $cart->needs_shipping() ); }, 'blocks.advanced-card-method' => static function( ContainerInterface $container ): AdvancedCardPaymentMethod { diff --git a/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php b/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php index c8e0417ab..43032c588 100644 --- a/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php +++ b/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php @@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; */ class UpdateShippingEndpoint implements EndpointInterface { const ENDPOINT = 'ppc-update-shipping'; - const WC_STORE_API_ENDPOINT = '/wp-json/wc/store/cart/'; + const WC_STORE_API_ENDPOINT = '/wp-json/wc/store/v1/cart/'; /** * The Request Data Helper. diff --git a/modules/ppcp-blocks/src/PayPalPaymentMethod.php b/modules/ppcp-blocks/src/PayPalPaymentMethod.php index e89123924..e03bd57d3 100644 --- a/modules/ppcp-blocks/src/PayPalPaymentMethod.php +++ b/modules/ppcp-blocks/src/PayPalPaymentMethod.php @@ -130,13 +130,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { */ private $all_funding_sources; - /** - * Whether shipping details must be collected during checkout; i.e. paying for physical goods? - * - * @var bool - */ - private $need_shipping; - /** * Assets constructor. * @@ -155,7 +148,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { * @param string $place_order_button_text The text for the standard "Place order" button. * @param string $place_order_button_description The text for additional "Place order" description. * @param array $all_funding_sources All existing funding sources for PayPal buttons. - * @param bool $need_shipping Whether shipping details are required for the purchase. */ public function __construct( string $module_url, @@ -172,8 +164,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { bool $use_place_order, string $place_order_button_text, string $place_order_button_description, - array $all_funding_sources, - bool $need_shipping + array $all_funding_sources ) { $this->name = PayPalGateway::ID; $this->module_url = $module_url; @@ -191,7 +182,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { $this->place_order_button_text = $place_order_button_text; $this->place_order_button_description = $place_order_button_description; $this->all_funding_sources = $all_funding_sources; - $this->need_shipping = $need_shipping; } /** @@ -258,6 +248,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { && $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ?? 'block-checkout' ); $place_order_enabled = ( $this->use_place_order || $this->add_place_order_method ) && ! $this->subscription_helper->cart_contains_subscription(); + $cart = WC()->cart; return array( 'id' => $this->gateway->id, @@ -284,7 +275,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { ), ), 'scriptData' => $script_data, - 'needShipping' => $this->need_shipping, + 'needShipping' => $cart && $cart->needs_shipping(), ); } diff --git a/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js b/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js index ae86ddea0..03952b14a 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ShippingHandler.js @@ -21,8 +21,8 @@ export const handleShippingOptionsChange = async ( data, actions, config ) => { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', - 'X-WC-Store-API-Nonce': - config.ajax.update_customer_shipping.wp_rest_nonce, + 'Nonce': + config.ajax.update_customer_shipping.wp_rest_nonce, }, body: JSON.stringify( { rate_id: shippingOptionId, @@ -106,9 +106,9 @@ export const handleShippingAddressChange = async ( data, actions, config ) => { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', - 'X-WC-Store-API-Nonce': - config.ajax.update_customer_shipping - .wp_rest_nonce, + 'Nonce': + config.ajax.update_customer_shipping + .wp_rest_nonce, }, body: JSON.stringify( { shipping_address: cartData.shipping_address, diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 5a489d330..44e9a52ac 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -36,7 +36,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration; @@ -49,7 +48,7 @@ return array( return $client_id; } - $env = $container->get( 'onboarding.environment' ); + $env = $container->get( 'settings.environment' ); /** * The environment. * @@ -125,8 +124,8 @@ return array( } } - $state = $container->get( 'onboarding.state' ); - if ( $state->current_state() !== State::STATE_ONBOARDED ) { + $is_connected = $container->get( 'settings.flag.is-connected' ); + if ( ! $is_connected ) { return new DisabledSmartButton(); } @@ -142,7 +141,7 @@ return array( $dcc_applies = $container->get( 'api.helpers.dccapplies' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' ); $messages_apply = $container->get( 'button.helper.messages-apply' ); - $environment = $container->get( 'onboarding.environment' ); + $environment = $container->get( 'settings.environment' ); $payment_token_repository = $container->get( 'vaulting.repository.payment-token' ); return new SmartButton( $container->get( 'button.url' ), @@ -241,11 +240,11 @@ return array( ); }, 'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler { - - $state = $container->get( 'onboarding.state' ); - $order_processor = $container->get( 'wcgateway.order-processor' ); - $session_handler = $container->get( 'session.handler' ); - return new EarlyOrderHandler( $state, $order_processor, $session_handler ); + return new EarlyOrderHandler( + $container->get( 'settings.flag.is-connected' ), + $container->get( 'wcgateway.order-processor' ), + $container->get( 'session.handler' ) + ); }, 'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint { $request_data = $container->get( 'button.request-data' ); diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index 775be302e..2e8cc4c0b 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -272,8 +272,9 @@ class ApproveOrderEndpoint implements EndpointInterface { * @return void */ protected function toggle_final_review_enabled_setting(): void { + // TODO new-ux: This flag must also be updated in the new settings. $final_review_enabled_setting = $this->settings->has( 'blocks_final_review_enabled' ) && $this->settings->get( 'blocks_final_review_enabled' ); - $final_review_enabled_setting ? $this->settings->set( 'blocks_final_review_enabled', false ) : $this->settings->set( 'blocks_final_review_enabled', true ); + $this->settings->set( 'blocks_final_review_enabled', ! $final_review_enabled_setting ); $this->settings->persist(); } } diff --git a/modules/ppcp-button/src/Helper/DisabledFundingSources.php b/modules/ppcp-button/src/Helper/DisabledFundingSources.php index 203208b47..1f5c87afc 100644 --- a/modules/ppcp-button/src/Helper/DisabledFundingSources.php +++ b/modules/ppcp-button/src/Helper/DisabledFundingSources.php @@ -104,6 +104,6 @@ class DisabledFundingSources { $disable_funding = $all_sources; } - return $disable_funding; + return apply_filters( 'woocommerce_paypal_payments_disabled_funding_sources', $disable_funding ); } } diff --git a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php index 46b4ddf41..393ecab45 100644 --- a/modules/ppcp-button/src/Helper/EarlyOrderHandler.php +++ b/modules/ppcp-button/src/Helper/EarlyOrderHandler.php @@ -11,8 +11,6 @@ namespace WooCommerce\PayPalCommerce\Button\Helper; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; -use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; @@ -23,11 +21,11 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; class EarlyOrderHandler { /** - * The State. + * Whether the merchant is connected to PayPal (onboarding completed). * - * @var State + * @var bool */ - private $state; + private bool $is_connected; /** * The Order Processor. @@ -46,17 +44,17 @@ class EarlyOrderHandler { /** * EarlyOrderHandler constructor. * - * @param State $state The State. + * @param bool $is_connected Whether onboarding was completed. * @param OrderProcessor $order_processor The Order Processor. * @param SessionHandler $session_handler The Session Handler. */ public function __construct( - State $state, + bool $is_connected, OrderProcessor $order_processor, SessionHandler $session_handler ) { - $this->state = $state; + $this->is_connected = $is_connected; $this->order_processor = $order_processor; $this->session_handler = $session_handler; } @@ -67,7 +65,7 @@ class EarlyOrderHandler { * @return bool */ public function should_create_early_order(): bool { - return $this->state->current_state() === State::STATE_ONBOARDED; + return $this->is_connected; } //phpcs:disable WordPress.Security.NonceVerification.Recommended diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index 8341aa29a..f3e5b7def 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -199,8 +199,9 @@ class WooCommerceOrderCreator { $shipping_options = null; if ( $payer ) { - $address = $payer->address(); - $payer_name = $payer->name(); + $address = $payer->address(); + $payer_name = $payer->name(); + $payer_phone = $payer->phone(); $wc_email = null; $wc_customer = WC()->customer; @@ -220,6 +221,7 @@ class WooCommerceOrderCreator { 'state' => $address ? $address->admin_area_1() : '', 'postcode' => $address ? $address->postal_code() : '', 'country' => $address ? $address->country_code() : '', + 'phone' => $payer_phone ? $payer_phone->phone()->national_number() : '', ); } diff --git a/modules/ppcp-card-fields/services.php b/modules/ppcp-card-fields/services.php index 1e32cc4f6..d1bf340b3 100644 --- a/modules/ppcp-card-fields/services.php +++ b/modules/ppcp-card-fields/services.php @@ -43,6 +43,7 @@ return array( 'FR', 'DE', 'GR', + 'HK', 'HU', 'IE', 'IT', @@ -56,6 +57,7 @@ return array( 'PT', 'RO', 'SK', + 'SG', 'SI', 'ES', 'SE', diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index 3ceedea9f..c862d2bc8 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -10,9 +10,13 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat; use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets; +use WooCommerce\PayPalCommerce\Compat\Settings\GeneralSettingsMapHelper; +use WooCommerce\PayPalCommerce\Compat\Settings\PaymentMethodSettingsMapHelper; use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMap; use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper; +use WooCommerce\PayPalCommerce\Compat\Settings\SettingsTabMapHelper; use WooCommerce\PayPalCommerce\Compat\Settings\StylingSettingsMapHelper; +use WooCommerce\PayPalCommerce\Compat\Settings\SubscriptionSettingsMapHelper; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; return array( @@ -133,30 +137,26 @@ return array( $styling_settings_map_helper = $container->get( 'compat.settings.styling_map_helper' ); assert( $styling_settings_map_helper instanceof StylingSettingsMapHelper ); + $settings_tab_map_helper = $container->get( 'compat.settings.settings_tab_map_helper' ); + assert( $settings_tab_map_helper instanceof SettingsTabMapHelper ); + + $subscription_map_helper = $container->get( 'compat.settings.subscription_map_helper' ); + assert( $subscription_map_helper instanceof SubscriptionSettingsMapHelper ); + + $general_map_helper = $container->get( 'compat.settings.general_map_helper' ); + assert( $general_map_helper instanceof GeneralSettingsMapHelper ); + + $payment_methods_map_helper = $container->get( 'compat.settings.payment_methods_map_helper' ); + assert( $payment_methods_map_helper instanceof PaymentMethodSettingsMapHelper ); + return array( new SettingsMap( $container->get( 'settings.data.general' ), - /** - * The new GeneralSettings class stores the current connection - * details, without adding an environment-suffix (no `_sandbox` - * or `_production` in the field name) - * Only the `sandbox_merchant` flag indicates, which environment - * the credentials are used for. - */ - array( - 'merchant_id' => 'merchant_id', - 'client_id' => 'client_id', - 'client_secret' => 'client_secret', - 'sandbox_on' => 'sandbox_merchant', - 'live_client_id' => 'client_id', - 'live_client_secret' => 'client_secret', - 'live_merchant_id' => 'merchant_id', - 'live_merchant_email' => 'merchant_email', - 'sandbox_client_id' => 'client_id', - 'sandbox_client_secret' => 'client_secret', - 'sandbox_merchant_id' => 'merchant_id', - 'sandbox_merchant_email' => 'merchant_email', - ) + $general_map_helper->map() + ), + new SettingsMap( + $container->get( 'settings.data.settings' ), + $settings_tab_map_helper->map() ), new SettingsMap( $container->get( 'settings.data.styling' ), @@ -172,15 +172,50 @@ return array( */ $styling_settings_map_helper->map() ), + new SettingsMap( + $container->get( 'settings.data.settings' ), + $subscription_map_helper->map() + ), + /** + * We need to pass the PaymentSettings model instance to use it in some helpers. + * Once the new settings module is permanently enabled, + * this model can be passed as a dependency to the appropriate helper classes. + * For now, we must pass it this way to avoid errors when the new settings module is disabled. + */ + new SettingsMap( + $container->get( 'settings.data.payment' ), + array() + ), + new SettingsMap( + $container->get( 'settings.data.payment' ), + $payment_methods_map_helper->map() + ), ); }, 'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper { return new SettingsMapHelper( $container->get( 'compat.setting.new-to-old-map' ), - $container->get( 'compat.settings.styling_map_helper' ) + $container->get( 'compat.settings.styling_map_helper' ), + $container->get( 'compat.settings.settings_tab_map_helper' ), + $container->get( 'compat.settings.subscription_map_helper' ), + $container->get( 'compat.settings.general_map_helper' ), + $container->get( 'compat.settings.payment_methods_map_helper' ), + $container->get( 'wcgateway.settings.admin-settings-enabled' ) ); }, 'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper { return new StylingSettingsMapHelper(); }, + 'compat.settings.settings_tab_map_helper' => static function() : SettingsTabMapHelper { + return new SettingsTabMapHelper(); + }, + 'compat.settings.subscription_map_helper' => static function( ContainerInterface $container ) : SubscriptionSettingsMapHelper { + return new SubscriptionSettingsMapHelper( $container->get( 'wc-subscriptions.helper' ) ); + }, + 'compat.settings.general_map_helper' => static function() : GeneralSettingsMapHelper { + return new GeneralSettingsMapHelper(); + }, + 'compat.settings.payment_methods_map_helper' => static function() : PaymentMethodSettingsMapHelper { + return new PaymentMethodSettingsMapHelper(); + }, ); diff --git a/modules/ppcp-compat/src/Settings/GeneralSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/GeneralSettingsMapHelper.php new file mode 100644 index 000000000..531b9e8a9 --- /dev/null +++ b/modules/ppcp-compat/src/Settings/GeneralSettingsMapHelper.php @@ -0,0 +1,70 @@ + + */ + public function map(): array { + return array( + 'merchant_id' => 'merchant_id', + 'client_id' => 'client_id', + 'client_secret' => 'client_secret', + 'sandbox_on' => 'sandbox_merchant', + 'live_client_id' => 'client_id', + 'live_client_secret' => 'client_secret', + 'live_merchant_id' => 'merchant_id', + 'live_merchant_email' => 'merchant_email', + 'merchant_email' => 'merchant_email', + 'sandbox_client_id' => 'client_id', + 'sandbox_client_secret' => 'client_secret', + 'sandbox_merchant_id' => 'merchant_id', + 'sandbox_merchant_email' => 'merchant_email', + 'enabled' => '', + 'allow_local_apm_gateways' => '', + ); + } + + /** + * Retrieves the mapped value for the given key from the new settings. + * + * @param string $old_key The key from the legacy settings. + * @param array $settings_model The new settings model data as an array. + * @return mixed The value of the mapped setting, or null if not applicable. + */ + public function mapped_value( string $old_key, array $settings_model ) { + $settings_map = $this->map(); + $new_key = $settings_map[ $old_key ] ?? false; + switch ( $old_key ) { + case 'enabled': + case 'allow_local_apm_gateways': + return true; + + default: + return $settings_model[ $new_key ] ?? null; + } + } +} diff --git a/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php new file mode 100644 index 000000000..1faf46955 --- /dev/null +++ b/modules/ppcp-compat/src/Settings/PaymentMethodSettingsMapHelper.php @@ -0,0 +1,64 @@ + + */ + public function map(): array { + return array( + 'dcc_enabled' => CreditCardGateway::ID, + 'axo_enabled' => AxoGateway::ID, + ); + } + + /** + * Retrieves the value of a mapped key from the new settings. + * + * @param string $old_key The key from the legacy settings. + * @return mixed The value of the mapped setting, (null if not found). + */ + public function mapped_value( string $old_key ): ?bool { + + $payment_method = $this->map()[ $old_key ] ?? false; + + if ( ! $payment_method ) { + return null; + } + + return $this->is_gateway_enabled( $payment_method ); + } + + /** + * Checks if the payment gateway with the given name is enabled. + * + * @param string $gateway_name The gateway name. + * @return bool True if the payment gateway with the given name is enabled, otherwise false. + */ + protected function is_gateway_enabled( string $gateway_name ): bool { + $gateway_settings = get_option( "woocommerce_{$gateway_name}_settings", array() ); + $gateway_enabled = $gateway_settings['enabled'] ?? false; + + return $gateway_enabled === 'yes'; + } +} diff --git a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php index a18f2ceda..39a285041 100644 --- a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php @@ -10,6 +10,10 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; +use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; +use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; /** @@ -49,17 +53,70 @@ class SettingsMapHelper { */ protected StylingSettingsMapHelper $styling_settings_map_helper; + /** + * A helper for mapping the old/new settings tab settings. + * + * @var SettingsTabMapHelper + */ + protected SettingsTabMapHelper $settings_tab_map_helper; + + /** + * A helper for mapping old and new subscription settings. + * + * @var SubscriptionSettingsMapHelper + */ + protected SubscriptionSettingsMapHelper $subscription_map_helper; + + /** + * A helper for mapping old and new general settings. + * + * @var GeneralSettingsMapHelper + */ + protected GeneralSettingsMapHelper $general_settings_map_helper; + + /** + * A helper for mapping old and new payment method settings. + * + * @var PaymentMethodSettingsMapHelper + */ + protected PaymentMethodSettingsMapHelper $payment_method_settings_map_helper; + + /** + * Whether the new settings module is enabled. + * + * @var bool + */ + protected bool $new_settings_module_enabled; + /** * Constructor. * - * @param SettingsMap[] $settings_map A list of settings maps containing key definitions. - * @param StylingSettingsMapHelper $styling_settings_map_helper A helper for mapping the old/new styling settings. + * @param SettingsMap[] $settings_map A list of settings maps containing key definitions. + * @param StylingSettingsMapHelper $styling_settings_map_helper A helper for mapping the old/new styling settings. + * @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings. + * @param SubscriptionSettingsMapHelper $subscription_map_helper A helper for mapping old and new subscription settings. + * @param GeneralSettingsMapHelper $general_settings_map_helper A helper for mapping old and new general settings. + * @param PaymentMethodSettingsMapHelper $payment_method_settings_map_helper A helper for mapping old and new payment method settings. + * @param bool $new_settings_module_enabled Whether the new settings module is enabled. * @throws RuntimeException When an old key has multiple mappings. */ - public function __construct( array $settings_map, StylingSettingsMapHelper $styling_settings_map_helper ) { + public function __construct( + array $settings_map, + StylingSettingsMapHelper $styling_settings_map_helper, + SettingsTabMapHelper $settings_tab_map_helper, + SubscriptionSettingsMapHelper $subscription_map_helper, + GeneralSettingsMapHelper $general_settings_map_helper, + PaymentMethodSettingsMapHelper $payment_method_settings_map_helper, + bool $new_settings_module_enabled + ) { $this->validate_settings_map( $settings_map ); - $this->settings_map = $settings_map; - $this->styling_settings_map_helper = $styling_settings_map_helper; + $this->settings_map = $settings_map; + $this->styling_settings_map_helper = $styling_settings_map_helper; + $this->settings_tab_map_helper = $settings_tab_map_helper; + $this->subscription_map_helper = $subscription_map_helper; + $this->general_settings_map_helper = $general_settings_map_helper; + $this->payment_method_settings_map_helper = $payment_method_settings_map_helper; + $this->new_settings_module_enabled = $new_settings_module_enabled; } /** @@ -89,6 +146,10 @@ class SettingsMapHelper { * @return mixed|null The value of the mapped setting, or null if not found. */ public function mapped_value( string $old_key ) { + if ( ! $this->new_settings_module_enabled ) { + return null; + } + $this->ensure_map_initialized(); if ( ! isset( $this->key_to_model[ $old_key ] ) ) { return null; @@ -112,6 +173,10 @@ class SettingsMapHelper { * @return bool True if the key exists in the new settings, false otherwise. */ public function has_mapped_key( string $old_key ) : bool { + if ( ! $this->new_settings_module_enabled ) { + return false; + } + $this->ensure_map_initialized(); return isset( $this->key_to_model[ $old_key ] ); @@ -134,7 +199,23 @@ class SettingsMapHelper { switch ( true ) { case $model instanceof StylingSettings: - return $this->styling_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); + return $this->styling_settings_map_helper->mapped_value( + $old_key, + $this->model_cache[ $model_id ], + $this->get_payment_settings_model() + ); + + case $model instanceof GeneralSettings: + return $this->general_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); + + case $model instanceof SettingsModel: + return $old_key === 'subscriptions_mode' + ? $this->subscription_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ) + : $this->settings_tab_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); + + case $model instanceof PaymentSettings: + return $this->payment_method_settings_map_helper->mapped_value( $old_key ); + default: return $this->model_cache[ $model_id ][ $new_key ] ?? null; } @@ -173,4 +254,23 @@ class SettingsMapHelper { } } } + + /** + * Retrieves the PaymentSettings model instance. + * + * Once the new settings module is permanently enabled, + * this model can be passed as a dependency to the appropriate helper classes. + * For now, we must pass it this way to avoid errors when the new settings module is disabled. + * + * @return AbstractDataModel|null + */ + protected function get_payment_settings_model() : ?AbstractDataModel { + foreach ( $this->settings_map as $settings_map_instance ) { + if ( $settings_map_instance->get_model() instanceof PaymentSettings ) { + return $settings_map_instance->get_model(); + } + } + + return null; + } } diff --git a/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php new file mode 100644 index 000000000..6e61328d2 --- /dev/null +++ b/modules/ppcp-compat/src/Settings/SettingsTabMapHelper.php @@ -0,0 +1,146 @@ + + */ + public function map(): array { + return array( + 'disable_cards' => 'disabled_cards', + 'brand_name' => 'brand_name', + 'soft_descriptor' => 'soft_descriptor', + 'payee_preferred' => 'instant_payments_only', + 'subtotal_mismatch_behavior' => 'subtotal_adjustment', + 'landing_page' => 'landing_page', + 'smart_button_language' => 'button_language', + 'prefix' => 'invoice_prefix', + 'intent' => '', + 'vault_enabled_dcc' => 'save_card_details', + 'blocks_final_review_enabled' => 'enable_pay_now', + 'logging_enabled' => 'enable_logging', + 'vault_enabled' => 'save_paypal_and_venmo', + ); + } + + /** + * Retrieves the value of a mapped key from the new settings. + * + * @param string $old_key The key from the legacy settings. + * @param array $settings_model The new settings model data as an array. + * @return mixed The value of the mapped setting, (null if not found). + */ + public function mapped_value( string $old_key, array $settings_model ) { + $settings_map = $this->map(); + $new_key = $settings_map[ $old_key ] ?? false; + switch ( $old_key ) { + case 'subtotal_mismatch_behavior': + return $this->mapped_mismatch_behavior_value( $settings_model ); + + case 'landing_page': + return $this->mapped_landing_page_value( $settings_model ); + + case 'intent': + return $this->mapped_intent_value( $settings_model ); + + case 'blocks_final_review_enabled': + return $this->mapped_pay_now_value( $settings_model ); + + default: + return $settings_model[ $new_key ] ?? null; + } + } + + /** + * Retrieves the mapped value for the 'mismatch_behavior' from the new settings. + * + * @param array $settings_model The new settings model data as an array. + * @return 'extra_line'|'ditch'|null The mapped 'mismatch_behavior' setting value. + */ + protected function mapped_mismatch_behavior_value( array $settings_model ): ?string { + $subtotal_adjustment = $settings_model['subtotal_adjustment'] ?? false; + + if ( ! $subtotal_adjustment ) { + return null; + } + + return $subtotal_adjustment === 'correction' ? PurchaseUnitSanitizer::MODE_EXTRA_LINE : PurchaseUnitSanitizer::MODE_DITCH; + } + + /** + * Retrieves the mapped value for the 'landing_page' from the new settings. + * + * @param array $settings_model The new settings model data as an array. + * @return 'LOGIN'|'BILLING'|'NO_PREFERENCE'|null The mapped 'landing_page' setting value. + */ + protected function mapped_landing_page_value( array $settings_model ): ?string { + $landing_page = $settings_model['landing_page'] ?? false; + + if ( ! $landing_page ) { + return null; + } + + return $landing_page === 'login' + ? ApplicationContext::LANDING_PAGE_LOGIN + : ( $landing_page === 'guest_checkout' + ? ApplicationContext::LANDING_PAGE_BILLING + : ApplicationContext::LANDING_PAGE_NO_PREFERENCE + ); + } + + /** + * Retrieves the mapped value for the order intent from the new settings. + * + * @param array $settings_model The new settings model data as an array. + * @return 'AUTHORIZE'|'CAPTURE'|null The mapped 'intent' setting value. + */ + protected function mapped_intent_value( array $settings_model ): ?string { + $authorize_only = $settings_model['authorize_only'] ?? null; + $capture_virtual_orders = $settings_model['capture_virtual_orders'] ?? null; + + if ( is_null( $authorize_only ) && is_null( $capture_virtual_orders ) ) { + return null; + } + + return $authorize_only ? 'AUTHORIZE' : 'CAPTURE'; + } + + /** + * Retrieves the mapped value for the "Pay Now Experience" from the new settings. + * + * @param array $settings_model The new settings model data as an array. + * @return bool|null The mapped 'Pay Now Experience' setting value. + */ + protected function mapped_pay_now_value( array $settings_model ): ?bool { + $enable_pay_now = $settings_model['enable_pay_now'] ?? null; + + if ( is_null( $enable_pay_now ) ) { + return null; + } + + return ! $enable_pay_now; + } +} diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index 5755f9d01..1015573b2 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -10,7 +10,11 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; /** @@ -23,6 +27,8 @@ class StylingSettingsMapHelper { use ContextTrait; + protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID ); + /** * Maps old setting keys to new setting style names. * @@ -40,11 +46,13 @@ class StylingSettingsMapHelper { public function map(): array { $mapped_settings = array( - 'smart_button_locations' => '', - 'pay_later_button_locations' => '', - 'disable_funding' => '', - 'googlepay_button_enabled' => '', - 'applepay_button_enabled' => '', + 'smart_button_locations' => '', + 'pay_later_button_locations' => '', + 'disable_funding' => '', + 'googlepay_button_enabled' => '', + 'applepay_button_enabled' => '', + 'smart_button_enable_styling_per_location' => '', + 'pay_later_button_enabled' => '', ); foreach ( $this->locations_map() as $old_location_name => $new_location_name ) { @@ -60,27 +68,34 @@ class StylingSettingsMapHelper { /** * Retrieves the value of a mapped key from the new settings. * - * @param string $old_key The key from the legacy settings. - * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param string $old_key The key from the legacy settings. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param AbstractDataModel|null $payment_settings The payment settings model. * * @return mixed The value of the mapped setting, (null if not found). */ - public function mapped_value( string $old_key, array $styling_models ) { + public function mapped_value( string $old_key, array $styling_models, ?AbstractDataModel $payment_settings ) { switch ( $old_key ) { case 'smart_button_locations': return $this->mapped_smart_button_locations_value( $styling_models ); + case 'smart_button_enable_styling_per_location': + return true; + case 'pay_later_button_locations': return $this->mapped_pay_later_button_locations_value( $styling_models ); case 'disable_funding': - return $this->mapped_disabled_funding_value( $styling_models ); + return $this->mapped_disabled_funding_value( $styling_models, $payment_settings ); case 'googlepay_button_enabled': - return $this->mapped_google_pay_or_apple_pay_enabled_value( $styling_models, 'googlepay' ); + return $this->mapped_button_enabled_value( $styling_models, GooglePayGateway::ID ); case 'applepay_button_enabled': - return $this->mapped_google_pay_or_apple_pay_enabled_value( $styling_models, 'applepay' ); + return $this->mapped_button_enabled_value( $styling_models, ApplePayGateway::ID ); + + case 'pay_later_button_enabled': + return $this->mapped_pay_later_button_enabled_value( $styling_models, $payment_settings ); default: foreach ( $this->locations_map() as $old_location_name => $new_location_name ) { @@ -198,7 +213,7 @@ class StylingSettingsMapHelper { $enabled_locations = array(); $locations = array_flip( $this->locations_map() ); foreach ( $styling_models as $model ) { - if ( ! $model->enabled || ! in_array( 'paylater', $model->methods, true ) ) { + if ( ! $model->enabled || ! in_array( 'pay-later', $model->methods, true ) ) { continue; } @@ -214,52 +229,116 @@ class StylingSettingsMapHelper { /** * Retrieves the mapped disabled funding value from the new settings. * - * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param AbstractDataModel|null $payment_settings The payment settings model. * @return array|null The list of disabled funding, or null if none are disabled. */ - protected function mapped_disabled_funding_value( array $styling_models ): ?array { - $disabled_funding = array(); - $locations_to_context_map = $this->current_context_to_new_button_location_map(); - - foreach ( $styling_models as $model ) { - if ( $model->location !== $locations_to_context_map[ $this->context() ] || in_array( 'venmo', $model->methods, true ) ) { - continue; - } - - $disabled_funding[] = 'venmo'; - - return $disabled_funding; + protected function mapped_disabled_funding_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?array { + if ( is_null( $payment_settings ) ) { + return null; } - return null; + $disabled_funding = array(); + $locations_to_context_map = $this->current_context_to_new_button_location_map(); + $current_context = $locations_to_context_map[ $this->context() ] ?? ''; + assert( $payment_settings instanceof PaymentSettings ); + + foreach ( $styling_models as $model ) { + if ( $model->location === $current_context ) { + if ( ! in_array( 'venmo', $model->methods, true ) || ! $payment_settings->get_venmo_enabled() ) { + $disabled_funding[] = 'venmo'; + } + } + } + + return $disabled_funding; } /** - * Retrieves the mapped enabled/disabled Google Pay or Apple Pay value from the new settings. + * Retrieves the mapped enabled or disabled PayLater button value from the new settings. * * @param LocationStylingDTO[] $styling_models The list of location styling models. - * @param 'googlepay'|'applepay' $button_name The button name ('googlepay' or 'applepay'). - * @return int The enabled (1) or disabled (0) state. - * @throws RuntimeException If an invalid button name is provided. + * @param AbstractDataModel|null $payment_settings The payment settings model. + * @return int|null The enabled (1) or disabled (0) state or null if it should fall back to old settings value. */ - protected function mapped_google_pay_or_apple_pay_enabled_value( array $styling_models, string $button_name ): ?int { - if ( $button_name !== 'googlepay' && $button_name !== 'applepay' ) { - throw new RuntimeException( 'Wrong button name is provided. Either "googlepay" or "applepay" can be used' ); + protected function mapped_pay_later_button_enabled_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?int { + + if ( ! $payment_settings instanceof PaymentSettings ) { + return null; } $locations_to_context_map = $this->current_context_to_new_button_location_map(); + $current_context = $locations_to_context_map[ $this->context() ] ?? ''; foreach ( $styling_models as $model ) { - if ( ! $model->enabled - || $model->location !== $locations_to_context_map[ $this->context() ] - || ! in_array( $button_name, $model->methods, true ) - ) { - continue; + if ( $model->enabled && $model->location === $current_context ) { + if ( in_array( 'pay-later', $model->methods, true ) && $payment_settings->get_paylater_enabled() ) { + return 1; + } } - - return 1; } return 0; } + + /** + * Retrieves the mapped enabled or disabled button value from the new settings. + * + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param string $button_name The button name (see {@link self::BUTTON_NAMES}). + * @return int The enabled (1) or disabled (0) state. + * @throws RuntimeException If an invalid button name is provided. + */ + protected function mapped_button_enabled_value( array $styling_models, string $button_name ): ?int { + if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) { + throw new RuntimeException( 'Wrong button name is provided.' ); + } + + $locations_to_context_map = $this->current_context_to_new_button_location_map(); + $current_context = $locations_to_context_map[ $this->context() ] ?? ''; + + foreach ( $styling_models as $model ) { + if ( $model->enabled && $model->location === $current_context ) { + if ( in_array( $button_name, $model->methods, true ) && $this->is_gateway_enabled( $button_name ) ) { + return 1; + } + } + } + + if ( $current_context === 'classic_checkout' ) { + /** + * Outputs an inline CSS style that hides the Google Pay gateway (on Classic Checkout) + * In case if the button is disabled from the styling settings but the gateway itself is enabled. + * + * @return void + */ + add_action( + 'woocommerce_paypal_payments_checkout_button_render', + static function (): void { + ?> + + subscription_helper = $subscription_helper; + } + + /** + * Maps the old subscription setting key. + * + * This method creates a placeholder mapping as this setting doesn't exist in the new settings. + * The Subscriptions mode value is set automatically based on the merchant type. + * + * @psalm-return array + */ + public function map(): array { + return array( 'subscriptions_mode' => '' ); + } + + /** + * Retrieves the mapped value for the subscriptions_mode key from the new settings. + * + * @param string $old_key The key from the legacy settings. + * @param array $settings_model The new settings model data as an array. + * + * @return 'vaulting_api'|'subscriptions_api'|'disable_paypal_subscriptions'|null The mapped subscriptions_mode value, or null if not applicable. + */ + public function mapped_value( string $old_key, array $settings_model ): ?string { + if ( $old_key !== 'subscriptions_mode' || ! $this->subscription_helper->plugin_is_active() ) { + return null; + } + + $vaulting = $settings_model['save_paypal_and_venmo'] ?? false; + $subscription_mode_value = $vaulting ? self::OLD_SETTINGS_SUBSCRIPTION_MODE_VALUE_VAULTING : self::OLD_SETTINGS_SUBSCRIPTION_MODE_VALUE_SUBSCRIPTIONS; + + /** + * Allows disabling the subscription mode when using the new settings UI. + * + * @returns bool true if the subscription mode should be disabled, false otherwise (default is false). + */ + $subscription_mode_disabled = (bool) apply_filters( 'woocommerce_paypal_payments_subscription_mode_disabled', false ); + + return $subscription_mode_disabled ? self::OLD_SETTINGS_SUBSCRIPTION_MODE_VALUE_DISABLED : $subscription_mode_value; + } +} diff --git a/modules/ppcp-googlepay/resources/js/boot-block.js b/modules/ppcp-googlepay/resources/js/boot-block.js index 8c63bba14..22f681db7 100644 --- a/modules/ppcp-googlepay/resources/js/boot-block.js +++ b/modules/ppcp-googlepay/resources/js/boot-block.js @@ -81,22 +81,23 @@ const GooglePayComponent = ( { isEditing, buttonAttributes } ) => { }; const features = [ 'products' ]; - -registerExpressPaymentMethod( { - name: buttonData.id, - title: `PayPal - ${ buttonData.title }`, - description: __( - 'Eligible users will see the PayPal button.', - 'woocommerce-paypal-payments' - ), - gatewayId: 'ppcp-gateway', - label:
, - content: , - edit: , - ariaLabel: buttonData.title, - canMakePayment: () => buttonData.enabled, - supports: { - features, - style: [ 'height', 'borderRadius' ], - }, -} ); +if ( buttonConfig?.is_enabled ) { + registerExpressPaymentMethod( { + name: buttonData.id, + title: `PayPal - ${ buttonData.title }`, + description: __( + 'Eligible users will see the PayPal button.', + 'woocommerce-paypal-payments' + ), + gatewayId: 'ppcp-gateway', + label:
, + content: , + edit: , + ariaLabel: buttonData.title, + canMakePayment: () => buttonData.enabled, + supports: { + features, + style: [ 'height', 'borderRadius' ], + }, + } ); +} diff --git a/modules/ppcp-googlepay/services.php b/modules/ppcp-googlepay/services.php index f92aa0bd8..356587bf6 100644 --- a/modules/ppcp-googlepay/services.php +++ b/modules/ppcp-googlepay/services.php @@ -19,7 +19,6 @@ use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmApplies; use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus; use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; return array( @@ -106,6 +105,7 @@ return array( 'FR', // France 'DE', // Germany 'GR', // Greece + 'HK', // Hong Kong 'HU', // Hungary 'IE', // Ireland 'IT', // Italy @@ -119,6 +119,7 @@ return array( 'PL', // Poland 'PT', // Portugal 'RO', // Romania + 'SG', // Singapore 'SK', // Slovakia 'SI', // Slovenia 'ES', // Spain @@ -148,6 +149,7 @@ return array( 'CZK', // Czech Koruna 'DKK', // Danish Krone 'EUR', // Euro + 'HKD', // Hong Kong Dollar 'GBP', // British Pound Sterling 'HUF', // Hungarian Forint 'ILS', // Israeli New Shekel @@ -157,6 +159,7 @@ return array( 'NZD', // New Zealand Dollar 'PHP', // Philippine Peso 'PLN', // Polish Zloty + 'SGD', // Singapur-Dollar 'SEK', // Swedish Krona 'THB', // Thai Baht 'TWD', // New Taiwan Dollar @@ -174,7 +177,7 @@ return array( $container->get( 'session.handler' ), $container->get( 'wc-subscriptions.helper' ), $container->get( 'wcgateway.settings' ), - $container->get( 'onboarding.environment' ), + $container->get( 'settings.environment' ), $container->get( 'wcgateway.settings.status' ), $container->get( 'woocommerce.logger.woocommerce' ) ); @@ -221,15 +224,15 @@ return array( }, 'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string { - $state = $container->get( 'onboarding.state' ); - if ( $state->current_state() < State::STATE_ONBOARDED ) { + $is_connected = $container->get( 'settings.flag.is-connected' ); + if ( ! $is_connected ) { return ''; } $product_status = $container->get( 'googlepay.helpers.apm-product-status' ); assert( $product_status instanceof ApmProductStatus ); - $environment = $container->get( 'onboarding.environment' ); + $environment = $container->get( 'settings.environment' ); assert( $environment instanceof Environment ); $enabled = $product_status->is_active(); diff --git a/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php b/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php index dbe812837..20917eac7 100644 --- a/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php +++ b/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php @@ -89,6 +89,7 @@ class ApmProductStatus extends ProductStatus { } } + // Settings used as a cache; `settings->set` is compatible with new UI. if ( $has_capability ) { $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); } else { diff --git a/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php b/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php index 1fac77a6e..cc58eb791 100644 --- a/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php +++ b/modules/ppcp-local-alternative-payment-methods/src/LocalAlternativePaymentMethodsModule.php @@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods; use WC_Order; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; -use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; @@ -43,8 +42,31 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo /** * {@inheritDoc} */ - public function run( ContainerInterface $c ): bool { + public function run( ContainerInterface $c ) : bool { + add_action( 'after_setup_theme', fn() => $this->run_with_translations( $c ) ); + return true; + } + + /** + * Set up WP hooks that depend on translation features. + * Runs after the theme setup, when translations are available, which is fired + * before the `init` hook, which usually contains most of the logic. + * + * @param ContainerInterface $c The DI container. + * @return void + */ + private function run_with_translations( ContainerInterface $c ) : void { + // When Local APMs are disabled, none of the following hooks are needed. + if ( ! $this->should_add_local_apm_gateways( $c ) ) { + return; + } + + /** + * The "woocommerce_payment_gateways" filter is responsible for ADDING + * custom payment gateways to WooCommerce. Here, we add all the local + * APM gateways to the filtered list, so they become available later on. + */ add_filter( 'woocommerce_payment_gateways', /** @@ -53,14 +75,6 @@ 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; - } - if ( ! is_array( $methods ) ) { return $methods; } @@ -74,6 +88,10 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo } ); + /** + * Filters the "available gateways" list by REMOVING gateways that + * are not available for the current customer. + */ add_filter( 'woocommerce_available_payment_gateways', /** @@ -82,29 +100,22 @@ 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 ) ) { + if ( ! is_array( $methods ) || is_admin() || empty( WC()->customer ) ) { + // Don't restrict the gateway list on wp-admin or when no customer is known. return $methods; } - if ( ! is_admin() ) { - if ( ! isset( WC()->customer ) ) { - return $methods; - } + $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' ); + $customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country(); + $site_currency = get_woocommerce_currency(); - $customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country(); - $site_currency = get_woocommerce_currency(); + // Remove unsupported gateways from the customer's payment options. + foreach ( $payment_methods as $payment_method ) { + $is_currency_supported = in_array( $site_currency, $payment_method['currencies'], true ); + $is_country_supported = in_array( $customer_country, $payment_method['countries'], true ); - $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' ); - foreach ( $payment_methods as $payment_method ) { - if ( - ! in_array( $customer_country, $payment_method['countries'], true ) - || ! in_array( $site_currency, $payment_method['currencies'], true ) - ) { - unset( $methods[ $payment_method['id'] ] ); - } + if ( ! $is_currency_supported || ! $is_country_supported ) { + unset( $methods[ $payment_method['id'] ] ); } } @@ -112,12 +123,15 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo } ); + /** + * Adds all local APM gateways in the "payment_method_type" block registry + * to make the payment methods available in the Block Checkout. + * + * @see IntegrationRegistry::initialize + */ 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' ) ); @@ -128,9 +142,6 @@ 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'] ?? ''; @@ -149,9 +160,6 @@ 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; @@ -184,9 +192,6 @@ 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 ) @@ -202,8 +207,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo 10, 2 ); - - return true; } /** @@ -229,12 +232,42 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo * @param ContainerInterface $container Container. * @return bool */ - private function should_add_local_apm_gateways( ContainerInterface $container ): bool { + private function should_add_local_apm_gateways( ContainerInterface $container ) : bool { + // APMs are only available after merchant onboarding is completed. + $is_connected = $container->get( 'settings.flag.is-connected' ); + if ( ! $is_connected ) { + /** + * When the merchant is _not_ connected yet, we still need to + * register the APM gateways in one case: + * + * During the authentication process (which happens via a REST call) + * the gateways need to be present, so they can be correctly + * pre-configured for new merchants. + */ + return $this->is_rest_request(); + } + + // The general plugin functionality must be enabled. $settings = $container->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - return $settings->has( 'enabled' ) - && $settings->get( 'enabled' ) === true - && $settings->has( 'allow_local_apm_gateways' ) + if ( ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' ) ) { + return false; + } + + // Register APM gateways, when the relevant setting is active. + return $settings->has( 'allow_local_apm_gateways' ) && $settings->get( 'allow_local_apm_gateways' ) === true; } + + /** + * Checks, whether the current request is trying to access a WooCommerce REST endpoint. + * + * @return bool True, if the request path matches the WC-Rest namespace. + */ + private function is_rest_request(): bool { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ); + + return str_contains( $request_uri, '/wp-json/wc/' ); + } } diff --git a/modules/ppcp-local-alternative-payment-methods/src/LocalApmProductStatus.php b/modules/ppcp-local-alternative-payment-methods/src/LocalApmProductStatus.php index 2a89146be..546a94d15 100644 --- a/modules/ppcp-local-alternative-payment-methods/src/LocalApmProductStatus.php +++ b/modules/ppcp-local-alternative-payment-methods/src/LocalApmProductStatus.php @@ -71,6 +71,7 @@ class LocalApmProductStatus extends ProductStatus { } } + // Settings used as a cache; `settings->set` is compatible with new UI. if ( $has_capability ) { $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); } else { diff --git a/modules/ppcp-onboarding/resources/js/onboarding.js b/modules/ppcp-onboarding/resources/js/onboarding.js index 06197afd6..658a63f96 100644 --- a/modules/ppcp-onboarding/resources/js/onboarding.js +++ b/modules/ppcp-onboarding/resources/js/onboarding.js @@ -326,9 +326,9 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) { isDisconnecting = true; - const saveButton = document.querySelector( '.woocommerce-save-button' ); - saveButton.removeAttribute( 'disabled' ); - saveButton.click(); + const saveButton = document.querySelector( '.woocommerce-save-button' ); + saveButton.removeAttribute( 'disabled' ); + saveButton.click(); }; // Prevent the message about unsaved checkbox/radiobutton when reloading the page. @@ -345,9 +345,11 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) { const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' ); - sandboxSwitchElement?.addEventListener( 'click', () => { - document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' ); - }); + sandboxSwitchElement?.addEventListener( 'click', () => { + document + .querySelector( '.woocommerce-save-button' ) + ?.removeAttribute( 'disabled' ); + } ); const validate = () => { const selectors = sandboxSwitchElement.checked @@ -389,7 +391,8 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) { const isSandboxInBackend = PayPalCommerceGatewayOnboarding.current_env === 'sandbox'; - if ( sandboxSwitchElement.checked !== isSandboxInBackend ) { + + if ( sandboxSwitchElement?.checked !== isSandboxInBackend ) { sandboxSwitchElement.checked = isSandboxInBackend; } diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index cbbd02da5..64d136092 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -18,10 +18,11 @@ use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( 'api.paypal-host' => function( ContainerInterface $container ) : string { - $environment = $container->get( 'onboarding.environment' ); + $environment = $container->get( 'settings.environment' ); /** * The current environment. * @@ -34,7 +35,7 @@ return array( }, 'api.paypal-website-url' => function( ContainerInterface $container ) : string { - $environment = $container->get( 'onboarding.environment' ); + $environment = $container->get( 'settings.environment' ); assert( $environment instanceof Environment ); if ( $environment->current_environment_is( Environment::SANDBOX ) ) { return $container->get( 'api.paypal-website-url-sandbox' ); @@ -56,9 +57,16 @@ return array( return $state->current_state() >= State::STATE_ONBOARDED; }, - 'onboarding.environment' => function( ContainerInterface $container ) : Environment { + 'settings.flag.is-sandbox' => static function ( ContainerInterface $container ) : bool { $settings = $container->get( 'wcgateway.settings' ); - return new Environment( $settings ); + assert( $settings instanceof Settings ); + + return $settings->has( 'sandbox_on' ) && $settings->get( 'sandbox_on' ); + }, + 'settings.environment' => function ( ContainerInterface $container ) : Environment { + return new Environment( + $container->get( 'settings.flag.is-sandbox' ) + ); }, 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets { @@ -68,7 +76,7 @@ return array( $container->get( 'onboarding.url' ), $container->get( 'ppcp.asset-version' ), $state, - $container->get( 'onboarding.environment' ), + $container->get( 'settings.environment' ), $login_seller_endpoint, $container->get( 'wcgateway.current-ppcp-settings-page-id' ) ); diff --git a/modules/ppcp-onboarding/src/OnboardingRESTController.php b/modules/ppcp-onboarding/src/OnboardingRESTController.php index d95b0a9d3..95d47b8f8 100644 --- a/modules/ppcp-onboarding/src/OnboardingRESTController.php +++ b/modules/ppcp-onboarding/src/OnboardingRESTController.php @@ -133,9 +133,11 @@ class OnboardingRESTController { * @return array */ public function get_status( $request ) { - $environment = $this->container->get( 'onboarding.environment' ); + $environment = $this->container->get( 'settings.environment' ); $state = $this->container->get( 'onboarding.state' ); + // Legacy onboarding module; using `State::STATE_ONBOARDED` checks is valid here. + return array( 'environment' => $environment->current_environment(), 'onboarded' => ( $state->current_state() >= State::STATE_ONBOARDED ), diff --git a/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php b/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php index 081f1599b..13189fd77 100644 --- a/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php +++ b/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php @@ -103,12 +103,8 @@ class OnboardingRenderer { 'displayMode' => 'minibrowser', ); - $data = $this->partner_referrals_data - ->with_products( $products ) - ->data(); - $environment = $is_production ? 'production' : 'sandbox'; - $product = 'PPCP' === $data['products'][0] ? 'ppcp' : 'express_checkout'; + $product = strtolower( $products[0] ?? 'express_checkout' ); $cache_key = $environment . '-' . $product; $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, get_current_user_id() ); @@ -122,8 +118,7 @@ class OnboardingRenderer { $onboarding_url->init(); - $data = $this->partner_referrals_data - ->append_onboarding_token( $data, $onboarding_url->token() ?: '' ); + $data = $this->partner_referrals_data->data( $products, $onboarding_url->token() ?: '' ); $url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data ); $url = add_query_arg( $args, $url ); diff --git a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php index b1f77de85..6c1dc4b41 100644 --- a/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php +++ b/modules/ppcp-paylater-configurator/src/Endpoint/SaveConfig.php @@ -95,6 +95,7 @@ class SaveConfig { * @param array $config The configurator config. */ public function save_config( array $config ): void { + // TODO new-ux: We should convert this to a new AbstractDataModel class in the settings folder! $this->settings->set( 'pay_later_enable_styling_per_messaging_location', true ); $this->settings->set( 'pay_later_messaging_enabled', true ); diff --git a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php index c78cdc31d..bc588df31 100644 --- a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php +++ b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php @@ -374,7 +374,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu function( $subscription ) use ( $c ) { $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; if ( $subscription_id ) { - $environment = $c->get( 'onboarding.environment' ); + $environment = $c->get( 'settings.environment' ); $host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; ?> @@ -488,7 +488,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu return; } - $environment = $c->get( 'onboarding.environment' ); + $environment = $c->get( 'settings.environment' ); echo ''; @@ -522,7 +522,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu return; } - $environment = $c->get( 'onboarding.environment' ); + $environment = $c->get( 'settings.environment' ); $this->render_paypal_subscription_fields( $product, $environment ); } diff --git a/modules/ppcp-settings/docs/applying-default-values-after-onboarding.md b/modules/ppcp-settings/docs/applying-default-values-after-onboarding.md new file mode 100644 index 000000000..851fcc3c4 --- /dev/null +++ b/modules/ppcp-settings/docs/applying-default-values-after-onboarding.md @@ -0,0 +1,94 @@ +# Applying Default Configuration After Onboarding + +The `OnboardingProfile` has a property named `setup_done`, which indicated whether the default +configuration was set up. + +### `OnboardingProfile::is_setup_done()` + +This flag indicated, whether the default plugin configuration was applied or not. +It's set to true after the merchant's authentication attempt was successful, and settings were +adjusted. + +The only way to reset this flag, is to enable the "**Start Over**" toggle and disconnecting the +merchant: +https://example.com/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=settings#disconnect-merchant + +### `class SettingsDataManager` + +The `SettingsDataManager` service is responsible for applying all defaults options at the end of the +onboarding process. + +### `SettingsDataManager::set_defaults_for_new_merchant()` + +This method expects a DTO argument (`ConfigurationFlagsDTO`) that provides relevant details about +the merchant and onboarding choices. + +It verifies, if default settings were already applied (by checking the +`OnboardingProfile::is_setup_done()` state). If not done yet, the DTO object is inspected to +initialize the plugin's configuration, before marking the `setup_done` flag as completed. + +## Default Settings Matrix + +### Decision Flags + +- **Country**: The merchant country. + - According to PayPal settings, not the WooCommerce country + - Test case: Set Woo country to Germany and sign in with a US merchant account; this should + trigger the "Country: US" branches below. +- **Seller Type**: Business or Casual. + - According to PayPal, not the onboarding choice + - Test case: Choose "Personal" during onboarding but log in with a business account; this should + trigger the "Account: Business" branches below. +- **Subscriptions**: An onboarding choice on the "Products" screen. +- **Cards**: An onboarding choice, on the "Checkout Options" screen. + - Refers to the first option on the checkout options screen ("Custom Card Fields", etc.) + +### Payment Methods + +By default, all payment methods are turned off after onboarding, unless the conditions specified in +the following table are met. + +| Payment Method | Country | Seller Type | Subscriptions | Cards | Notes | +|----------------|---------|-------------|---------------|-------|-------------------------------| +| Venmo | US | *any* | *any* | *any* | Always | +| Pay Later | US | *any* | *any* | *any* | Always | +| ACDC | US | Business | *any* | ✅ | Greyed out for Casual Sellers | +| BCDC | US | *any* | *any* | ✅ | | +| Apple Pay | US | Business | *any* | ✅ | Based on feature eligibility | +| Google Pay | US | Business | *any* | ✅ | Based on feature eligibility | +| All APMs | US | Business | *any* | ✅ | Based on feature eligibility | + +### Settings + +| Feature | Country | Seller-Type | Subscriptions | Cards | Notes | +|-----------------------------|---------|-------------|---------------|-------|----------------------------| +| Pay Now Experience | US | _any_ | _any_ | _any_ | | +| Save PayPal and Venmo | US | Business | ✅ | _any_ | | +| Save Credit and Debit Cards | US | Business | ✅ | ✅ | Requires ACDC eligibility* | + +- `*` If merchant has no ACDC eligibility, the setting should be disabled (not toggleable). + +### Styling + +All US merchants use the same settings, regardless of onboarding choices. + +| Button Location | Enabled | Displayed Payment Methods | +|------------------|---------|-------------------------------------------------| +| Cart | ✅ | PayPal, Venmo, Pay Later, Google Pay, Apple Pay | +| Classic Checkout | ✅ | PayPal, Venmo, Pay Later, Google Pay, Apple Pay | +| Express Checkout | ✅ | PayPal, Venmo, Pay Later, Google Pay, Apple Pay | +| Mini Cart | ✅ | PayPal, Venmo, Pay Later, Google Pay, Apple Pay | +| Product Page | ✅ | PayPal, Venmo, Pay Later | + +### Pay Later Messaging + +All US merchants use the same settings, regardless of onboarding choices. + +| Location | Enabled | +|-------------------|---------| +| Product | ✅ | +| Cart | ✅ | +| Checkout | ✅ | +| Home | ❌ | +| Shop | ❌ | +| WooCommerce Block | ❌ | diff --git a/modules/ppcp-settings/docs/authentication-flows.md b/modules/ppcp-settings/docs/authentication-flows.md new file mode 100644 index 000000000..bf666726f --- /dev/null +++ b/modules/ppcp-settings/docs/authentication-flows.md @@ -0,0 +1,120 @@ +# Authentication Flows + +The settings UI offers two distinct authentication methods: + +- OAuth +- Direct API + +## OAuth + +This is the usual authentication UI for most users. It opens a "PayPal popup" with a login mask. +The authentication flow consists of **three steps**: + +- Generate a referral URL with a special token +- Translate a one-time OAuth secret into permanent API credentials +- Complete authentication by confirming the token from step 1 + +**Usage:** + +1. Available on the first onboarding page (for sandbox login), or on the last page of the onboarding wizard. +2. Authentication is initiated by clicking a "Connect" button which opens a popup with a PayPal login mask + - Sometimes the login opens in a new tab, mainly on Mac when using the browser in full-screen mode +3. After completing the login, the final page shows a "Return to your store" button; clicking that button closes the popup/tab and completes the authentication process + +**More details on what happens:** + +```mermaid +sequenceDiagram + autonumber + participant R as React API + participant S as PHP Server + + R->>S: Request partner referral URL + Note over S: Generate and store a one-time token + create participant W as WooCommerce API + S->>W: Request referral URL + destroy W + W->>S: Generate the full partner referral URL + S->>R: Return referral URL + create participant P as PayPal Popup + R->>P: Open PayPal popup, which was generated by WooCommerce APi + Note over P: Complete login inside Popup + P->>R: Call JS function with OAuth ID and shared secret + R->>S: Send OAuth data to REST endpoint + create participant PP as PayPal API + S->>PP: Request permanent credentials + PP->>S: Translate one-time secret to permanent credentials + destroy P + P->>R: Redirect browser tab with unique token + Note over R: App unmounts during redirect + + Note over S: During page load + Note over S: Verify token and finalize authentication + S->>PP: Request merchant details + destroy PP + PP->>S: Return merchant details (e.g. country) + Note over S: Render the settings page with React app + S->>R: Boot react app in "settings mode" +``` + +1. Authentication starts _before_ the "Connect" button is rendered, as we generate a one-time partner referral URL + - See `ConnectionUrlGenerator::generate()` + - This referral URL configures PayPal: Which items render inside the Popup? What is the "return + URL" for the final step? Is it a sandbox or live login? +2. _...The merchant completes the login or account creation flow inside the popup..._ +3. During page-load of the final confirmation page inside the popup: PayPal directly calls a JS function on the WooCommerce settings page, i.e. the popup communicates with the open WooCommerce tab. This JS function sends an oauth ID and shared secret (OTP) to a REST endpoint + - See `AuthenticatoinRestEndpoint::connect_oauth()` + - See `AuthenticationManager::authenticate_via_oauth()` → translates the one-time shared secret + into a permanent client secret + - At this stage, the authentication is _incomplete_, as some details are only provided by the + final step +4. When clicking the "Return to store" button, the popup closes and the WooCommerce settings page "reloads"; it's actually a _redirect_ which is initiated by PayPal and receives a unique token (which was generated by the `ConnectionUrlGenerator`) that is required to complete authentication. + - See `ConnectionListener::process()` + - See `AuthenticationManager::finish_oauth_authentication()` + - This listener runs on every wp-admin page load and bails if the required token is not present +5. After the final page reload, the React app directly enters "Settings mode" + +## Direct API + +This method is only available for business accounts, as it requires the merchant to create a PayPal REST app that's linked to their account. + +
+Setup the PayPal REST app + +1. Visit https://developer.paypal.com/ +2. In section "Apps & Credentials" click "Create App" +3. After the app is ready, it displays the `Client ID` and `Secret Key` values + +
+ +**Usage:** + +1. Available on the first onboarding screen, via the "See advanced options" form at the bottom of the page +2. Activate the "Manual Connection" toggle; then enter the `Client ID` and `Secret Key` and hit Enter + +**What happens:** + +```mermaid +sequenceDiagram + participant R as React + participant S as Server + participant P as PayPal API + + R->>S: Send credentials to REST endpoint + S->>P: Authenticate via Direct API + P->>S: Return authentication result + S->>P: Request merchant details + P->>S: Return merchant details (e.g. country) + + Note over S: Process authentication result + S->>R: Return authentication status + Note over R: Update UI to authenticated state
(no page reload) + +``` + +1. Client ID and Secret are sent to a REST endpoint of the plugin. The authentication happens on server-side. + - See `AuthenticatoinRestEndpoint::connect_direct()` + - See `AuthenticationManager::authenticate_via_direct_api()` +2. After authentication is completed, the merchant account is prepared on server side and a confirmation is returned to the React app. + - See `AuthenticationManager::update_connection_details()` → condition `is_merchant_connected()` +3. The React app directly switches to the "Settings mode" without a page reload. diff --git a/modules/ppcp-settings/docs/glossary.md b/modules/ppcp-settings/docs/glossary.md new file mode 100644 index 000000000..022f99551 --- /dev/null +++ b/modules/ppcp-settings/docs/glossary.md @@ -0,0 +1,35 @@ +# Glossary + +This document provides definitions and explanations of key terms used in the plugin. + +--- + +## Eligibility + +**Eligibility** determines whether a merchant can access a specific feature within the plugin. It is a boolean value (`true` or `false`) that depends on certain criteria, such as: + +- **Country**: The merchant's location or the country where their business operates. +- **Other Factors**: Additional conditions, such as subscription plans, business type, or compliance requirements. + +If a merchant is **eligible** (`true`) for a feature, the feature will be visible and accessible in the plugin. If they are **not eligible** (`false`), the feature will be hidden or unavailable. + +--- + +## Capability + +**Capability** refers to the activation status of a feature for an eligible merchant. Even if a merchant is eligible for a feature, they may need to activate it in their PayPal dashboard to use it. Capability has two states: + +- **Active**: The feature is enabled, and the merchant can configure and use it. +- **Inactive**: The feature is not enabled, and the merchant will be guided on how to activate it (e.g., through instructions or prompts). + +Capability ensures that eligible merchants have control over which features they want to use and configure within the plugin. + +--- + +### Example Workflow + +1. A merchant is **eligible** for a feature based on their country and other factors. +2. If the feature is **active** (capability is enabled), the merchant can configure and use it. +3. If the feature is **inactive**, the plugin will provide instructions on how to activate it. + +--- diff --git a/modules/ppcp-settings/resources/css/_variables.scss b/modules/ppcp-settings/resources/css/_variables.scss index 69d327105..b42dbd7c4 100644 --- a/modules/ppcp-settings/resources/css/_variables.scss +++ b/modules/ppcp-settings/resources/css/_variables.scss @@ -17,6 +17,7 @@ $color-text-text: #070707; $color-border: #AEAEAE; $color-divider: #F0F0F0; $color-error-red: #cc1818; +$color-warning: #e2a030; $shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15); $color-gradient-dark: #001435; @@ -66,6 +67,7 @@ $card-vertical-gap: 48px; --color-text-teriary: #{$color-text-tertiary}; --color-text-description: #{$color-gray-700}; --color-error: #{$color-error-red}; + --color-warning: #{$color-warning}; // Default settings-block theme. --block-item-gap: 16px; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss index c1afcb4e8..72633ce3d 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_payment-method-item.scss @@ -63,6 +63,10 @@ &:hover { transform: rotate(45deg); } + + &.components-button { + height: auto; + } } .ppcp--method-icon { @@ -79,6 +83,234 @@ justify-content: space-between; align-items: center; margin-top: auto; + min-height: 24px; + } + + .ppcp--method-toggle-wrapper { + display: flex; + align-items: center; + } + } +} + +.ppcp-r-payment-methods { + .ppcp-highlight { + animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid $color-blueberry; + border-radius: var(--container-border-radius); + position: relative; + z-index: 1; + } + + @keyframes ppcp-highlight-fade { + 0%, 20% { + background-color: rgba($color-blueberry, 0.08); + border-color: $color-blueberry; + border-width: 1px; + } + 100% { + background-color: transparent; + border-color: $color-gray-300; + border-width: 1px; + } + } +} + +// Disabled state styling. +.ppcp--method-item--disabled { + position: relative; + + // Apply grayscale and disable interactions. + .ppcp--method-inner { + opacity: 0.7; + filter: grayscale(1); + pointer-events: none; + transition: filter 0.2s ease; + } + + // Override text colors. + .ppcp--method-title { + color: $color-gray-700 !important; + } + + .ppcp--method-description p { + color: $color-gray-500 !important; + } + + .ppcp--method-disabled-message { + opacity: 0; + transform: translateY(-5px); + transition: opacity 0.2s ease, transform 0.2s ease; + } + + // Style all buttons and toggle controls. + .components-button, + .components-form-toggle { + opacity: 0.5; + } + + // Hover state - only blur the inner content. + &:hover { + .ppcp--method-inner { + filter: blur(2px) grayscale(1); + } + + .ppcp--method-disabled-message { + opacity: 1; + transform: translateY(0); + } + } +} + +// Disabled overlay. +.ppcp--method-disabled-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba($color-white, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 8; + border-radius: var(--container-border-radius); + pointer-events: auto; + opacity: 0; + transition: opacity 0.2s ease; +} + +.ppcp--method-item--disabled:hover .ppcp--method-disabled-overlay { + opacity: 1; +} + +.ppcp--method-disabled-message { + padding: 14px 18px; + text-align: center; + @include font(13, 20, 500); + color: $color-text-tertiary; + position: relative; + z-index: 9; + border: none; + + a { + text-decoration: none; + } +} + +/* Warning message */ +.ppcp--method-warning { + position: relative; + display: inline-flex; + cursor: help; + + svg { + fill: currentColor; + color: $color-warning; + } + + /* Add invisible bridge to prevent gap between icon and popover */ + &:before { + content: ''; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + width: 30px; + height: 15px; + background-color: transparent; + } + + // Popover bubble + .ppcp--method-warning-message { + position: absolute; + bottom: calc(100% + 15px); + display: flex; + flex-direction: column; + gap: 10px; + left: 50%; + transform: translateX(-50%); + width: 250px; + padding: 16px; + background-color: $color-white; + border: 1px solid $color-gray-200; + border-radius: 4px; + z-index: 9; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s; + pointer-events: none; + + &:after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + margin-left: -6px; + width: 12px; + height: 12px; + background: $color-white; + border-right: 1px solid $color-gray-200; + border-bottom: 1px solid $color-gray-200; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.01); + transform: rotate(45deg); + margin-top: -6px; + } + + a { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + .ppcp--method-notice-list { + margin-bottom: 0; + } + + .highlight { + font-weight: 700; + background: inherit; + color: inherit; + } + + code { + font-size: 12px; + } + + ul { + list-style: inside; + } + } + + &:hover .ppcp--method-warning-message, + & .ppcp--method-warning-message:hover { + opacity: 1; + visibility: visible; + pointer-events: auto; + } +} + +// For RTL support +html[dir="rtl"] .ppcp--method-warning { + &:before { + left: auto; + right: 50%; + transform: translateX(50%); + } + + .ppcp--method-warning-message { + left: auto; + right: 50%; + transform: translateX(50%); + + &:after { + left: auto; + right: 50%; + margin-right: -6px; + margin-left: 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 1efe54236..0aa25d536 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 @@ -73,7 +73,6 @@ gap: 8px; &--save { - margin-top: -4px; align-items: flex-end; } @@ -87,7 +86,7 @@ &__field-rows { display: flex; flex-direction: column; - gap: 24px; + gap: 18px; &--acdc { gap: 18px; @@ -98,19 +97,11 @@ } .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; diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss index 5b1d04dd4..2ab6c9311 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-block.scss @@ -24,6 +24,10 @@ padding-top: var(--block-separator-gap, 32px); border-top: var(--block-separator-size, 1px) solid var(--block-separator-color); } + + &.ppcp--pull-right { + float: right; + } } .ppcp-r-settings-block { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss index 441cf4608..59d897132 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-card.scss @@ -69,4 +69,34 @@ $width_gap: 24px; color: var(--color-text-teriary); margin: 0; } + + + .ppcp-r-settings-card { + margin-top: $card-vertical-gap; + padding-top: $card-vertical-gap; + border-top: 1px solid $color-gray-200; + } + + .ppcp--card-actions { + transition: opacity 0.3s; + + &.ppcp--dimmed { + opacity: 0.5; + + &:hover { + opacity: 1; + } + } + + .components-button.is-tertiary { + transition: color 0.3s, background 0.3s; + + &:first-child { + padding-left: 0; + } + + svg { + margin-right: 4px; + } + } + } } diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss index 90c14d734..e2f091966 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_settings-wrapper.scss @@ -13,14 +13,3 @@ padding-bottom: 36px; } } - -.ppcp-r-settings { - > * { - margin-bottom: $card-vertical-gap; - } - - > *:not(:last-child) { - padding-bottom: $card-vertical-gap; - border-bottom: 1px solid $color-gray-200; - } -} diff --git a/modules/ppcp-settings/resources/css/components/screens/_modals.scss b/modules/ppcp-settings/resources/css/components/screens/_modals.scss new file mode 100644 index 000000000..5dbdf7652 --- /dev/null +++ b/modules/ppcp-settings/resources/css/components/screens/_modals.scss @@ -0,0 +1,19 @@ +/** + * Modal for disconnecting the merchant from the current PayPal account. + */ +.ppcp--modal-disconnect { + .ppcp--toggle-danger .components-form-toggle { + &.is-checked { + --wp-components-color-accent: #cc1818; + } + } + + .ppcp--action-buttons { + text-align: right; + margin-top: 32px; + + .components-button { + transition: background 0.3s; + } + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 0268bc1ca..63952ed1f 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -226,30 +226,91 @@ } } -// Payment Methods -.ppcp-r-payment-methods { - display: flex; - flex-direction: column; - gap: 48px; -} +.ppcp-r-settings { + .ppcp-highlight { + position: relative; + z-index: 1; -.ppcp-highlight { - animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1); - border: 1px solid $color-blueberry; - border-radius: var(--container-border-radius); - position: relative; - z-index: 1; -} + &::before { + content: ''; + position: absolute; + top: -8px; + left: -12px; + right: -12px; + bottom: -8px; + border: 1px solid $color-blueberry; + border-radius: 4px; + z-index: -1; + pointer-events: none; + animation: ppcp-setting-highlight-bg 2s cubic-bezier(0.4, 0, 0.2, 1); + animation-fill-mode: forwards; + } -@keyframes ppcp-highlight-fade { - 0%, 20% { - background-color: rgba($color-blueberry, 0.08); - border-color: $color-blueberry; - border-width: 1px; + &::after { + content: ''; + position: absolute; + top: -8px; + left: -12px; + width: 4px; + bottom: -8px; + background-color: $color-blueberry; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + z-index: -1; + pointer-events: none; + animation: ppcp-setting-highlight-accent 2s cubic-bezier(0.4, 0, 0.2, 1); + animation-fill-mode: forwards; + } } - 100% { - background-color: transparent; - border-color: $color-gray-300; - border-width: 1px; + + @keyframes ppcp-setting-highlight-bg { + 0%, 15% { + background-color: rgba($color-blueberry, 0.08); + border-color: $color-blueberry; + } + 70% { + background-color: transparent; + border-color: transparent; + } + 100% { + background-color: transparent; + border-color: transparent; + } + } + + @keyframes ppcp-setting-highlight-accent { + 0%, 15% { + opacity: 1; + } + 70% { + opacity: 0; + } + 100% { + opacity: 0; + } + } + + .ppcp-r-settings-section { + .ppcp--setting-row { + position: relative; + padding: 12px; + margin: 0 -12px; + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba($color-gray-100, 0.5); + } + } + } + + // RTL support + html[dir="rtl"] { + .ppcp-highlight { + &::after { + left: auto; + right: -12px; + border-radius: 0 4px 4px 0; + } + } } } diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss index 8e76d0028..e37fce07a 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-paylater-configurator.scss @@ -13,7 +13,7 @@ #configurator-eligibleContainer.css-4nclxm.e1vy3g880 { width: 100%; max-width: 100%; - padding: 48px 0px 48px 48px; + padding: 16px 0px 16px 16px; #configurator-controlPanelContainer.css-5urmrq.e1vy3g880 { width: 374px; @@ -25,6 +25,7 @@ .css-7xkxom, .css-8tvj6u { height: auto; + width: 1.2rem; } .css-10nkerk.ej6n7t60 { @@ -37,14 +38,19 @@ } .css-1vc34jy-handler { - height: 1.6rem; - width: 1.6rem; + height: 1.7em; + width: 1.5rem; } .css-8vwtr6-state { - height: 1.6rem; + height: 1.4rem; + width: 3rem; } } + + .css-1s8clkf.etu8a6w2 { + width: 374px; + } } &__subheader, #configurator-controlPanelSubHeader { @@ -68,6 +74,7 @@ .css-rok10q, .css-dfgbdq-text_body_strong { margin-top: 0; + margin-bottom: 0; } &__publish-button { @@ -110,4 +117,30 @@ width: 100%; } } + + .css-n4cwz8 { + margin-top: 20px; + } + + .css-1ce6bcu-container { + width: 3rem; + height: 1.8rem; + } + + #configurator-previewSectionSubHeaderText { + margin-right: 10px; + } + + .css-zcyvrz.ej6n7t60 { + margin-bottom: 5px; + + .css-3xbhoy-svg-size_md-icon { + width: 1.5rem; + height: 1.5rem; + } + + .css-7i5kpm-icon-button_base-size_xl-size_sm-secondary { + padding: 0.5rem; + } + } } diff --git a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss index 488aef61d..5c171a94b 100644 --- a/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss +++ b/modules/ppcp-settings/resources/css/components/screens/settings/_tab-styling.scss @@ -3,8 +3,8 @@ --block-separator-gap: 24px; --block-header-gap: 18px; --panel-width: 422px; - --sticky-offset-top: 92px; // 32px admin-bar + 60px TopNavigation height - --preview-height-reduction: 236px; // 32px admin-bar + 60px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin + --sticky-offset-top: 132px; // 32px admin-bar + 100px TopNavigation height + --preview-height-reduction: 276px; // 32px admin-bar + 100px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin display: flex; border: 1px solid var(--color-separators); diff --git a/modules/ppcp-settings/resources/css/style.scss b/modules/ppcp-settings/resources/css/style.scss index 0875dcad5..d70b4533f 100644 --- a/modules/ppcp-settings/resources/css/style.scss +++ b/modules/ppcp-settings/resources/css/style.scss @@ -11,3 +11,4 @@ @import './components/reusable-components/payment-method-modal'; @import './components/screens/fullscreen'; +@import './components/screens/modals'; diff --git a/modules/ppcp-settings/resources/js/Components/App.js b/modules/ppcp-settings/resources/js/Components/App.js index 1febe8674..74e20bd87 100644 --- a/modules/ppcp-settings/resources/js/Components/App.js +++ b/modules/ppcp-settings/resources/js/Components/App.js @@ -6,13 +6,13 @@ import SpinnerOverlay from './ReusableComponents/SpinnerOverlay'; import SendOnlyMessage from './Screens/SendOnlyMessage'; import OnboardingScreen from './Screens/Onboarding'; import SettingsScreen from './Screens/Settings'; -import { getQuery } from '../utils/navigation'; +import { getQuery, cleanUrlQueryParams } from '../utils/navigation'; const SettingsApp = () => { const { isReady: onboardingIsReady, completed: onboardingCompleted } = OnboardingHooks.useSteps(); + const { isReady: merchantIsReady } = CommonHooks.useStore(); const { - isReady: merchantIsReady, merchant: { isSendOnlyCountry }, } = CommonHooks.useMerchantInfo(); @@ -32,9 +32,19 @@ const SettingsApp = () => { loading: ! onboardingIsReady, } ); - const [ activePanel, setActivePanel ] = useState( - getQuery().panel || 'overview' - ); + const [ activePanel, setActivePanel ] = useState( getQuery().panel ); + + const removeUnsupportedArgs = () => { + const urlWasCleaned = cleanUrlQueryParams( [ + 'page', + 'tab', + 'section', + ] ); + + if ( urlWasCleaned ) { + setActivePanel( '' ); + } + }; const Content = useMemo( () => { if ( ! onboardingIsReady || ! merchantIsReady ) { @@ -42,16 +52,18 @@ const SettingsApp = () => { } if ( isSendOnlyCountry ) { + removeUnsupportedArgs(); return ; } if ( ! onboardingCompleted ) { + removeUnsupportedArgs(); return ; } return ( ); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index 44c5beb27..ca73ab531 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -2,7 +2,7 @@ import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; -import { useAccordionState } from '../../hooks/useAccordionState'; +import { useToggleState } from '../../hooks/useToggleState'; import { Content, Description, @@ -21,7 +21,7 @@ const Accordion = ( { children = null, className = '', } ) => { - const { isOpen, toggleOpen } = useAccordionState( { id, initiallyOpen } ); + const { isOpen, toggleOpen } = useToggleState( id, initiallyOpen ); const wrapperClasses = classNames( 'ppcp-r-accordion', className, { 'ppcp--is-open': isOpen, } ); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js index 2d648e20b..8c07544d9 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Controls/ControlToggleButton.js @@ -1,8 +1,15 @@ import { ToggleControl } from '@wordpress/components'; import { Action, Description } from '../Elements'; -const ControlToggleButton = ( { label, description, value, onChange } ) => ( - +const ControlToggleButton = ( { + id = '', + label, + description, + value, + onChange, + disabled = false, +} ) => ( + ( help={ description ? { description } : null } + disabled={ disabled } /> ); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/DataStoreControl.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/DataStoreControl.js index 90bf6dfbd..d336d458c 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/DataStoreControl.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/DataStoreControl.js @@ -18,6 +18,7 @@ const DataStoreControl = React.forwardRef( control: ControlComponent, value: externalValue, onChange, + onConfirm = null, delay = 300, ...props }, @@ -25,7 +26,9 @@ const DataStoreControl = React.forwardRef( ) => { const [ internalValue, setInternalValue ] = useState( externalValue ); const onChangeRef = useRef( onChange ); + const onConfirmRef = useRef( onConfirm ); onChangeRef.current = onChange; + onConfirmRef.current = onConfirm; const debouncedUpdate = useRef( debounce( ( value ) => { @@ -36,7 +39,7 @@ const DataStoreControl = React.forwardRef( useEffect( () => { setInternalValue( externalValue ); debouncedUpdate?.cancel(); - }, [ externalValue ] ); + }, [ debouncedUpdate, externalValue ] ); useEffect( () => { return () => debouncedUpdate?.cancel(); @@ -50,12 +53,25 @@ const DataStoreControl = React.forwardRef( [ debouncedUpdate ] ); + const handleKeyDown = useCallback( + ( event ) => { + if ( onConfirmRef.current && event.key === 'Enter' ) { + event.preventDefault(); + debouncedUpdate.flush(); + onConfirmRef.current(); + return false; + } + }, + [ debouncedUpdate ] + ); + return ( ); } diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/Action.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/Action.js index d1b935a71..333086e10 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/Action.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/Action.js @@ -1,5 +1,7 @@ -const Action = ( { children } ) => ( -
{ children }
+const Action = ( { id, children } ) => ( +
+ { children } +
); export default Action; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/CardActions.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/CardActions.js new file mode 100644 index 000000000..6f5ff523f --- /dev/null +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/CardActions.js @@ -0,0 +1,11 @@ +import classNames from 'classnames'; + +const CardActions = ( { isDimmed = false, children } ) => { + const className = classNames( 'ppcp--card-actions', { + 'ppcp--dimmed': isDimmed, + } ); + + return
{ children }
; +}; + +export default CardActions; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/index.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/index.js index 94c100e41..c56a70b8a 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/index.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Elements/index.js @@ -3,6 +3,7 @@ */ export { default as Action } from './Action'; +export { default as CardActions } from './CardActions'; export { default as Content } from './Content'; export { default as ContentWrapper } from './ContentWrapper'; export { default as Description } from './Description'; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields/OptionSelector.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields/OptionSelector.js index ba369d0f3..6ecdbdc5d 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields/OptionSelector.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/Fields/OptionSelector.js @@ -9,7 +9,13 @@ const OptionSelector = ( { } ) => (
{ options.map( - ( { value: itemValue, title, description, contents } ) => { + ( { + value: itemValue, + title, + description, + contents, + isDisabled = false, + } ) => { let isSelected; if ( Array.isArray( value ) ) { @@ -27,6 +33,7 @@ const OptionSelector = ( { onChange={ onChange } isMulti={ multiSelect } isSelected={ isSelected } + isDisabled={ isDisabled } > { contents } @@ -46,13 +53,13 @@ const OptionItem = ( { isMulti, isSelected, children, + isDisabled = false, } ) => { const boxClassName = classNames( 'ppcp-r-select-box', { 'ppcp--selected': isSelected, 'ppcp--multiselect': isMulti, 'ppcp--no-title': ! itemTitle, } ); - return ( // eslint-disable-next-line jsx-a11y/label-has-associated-control -- label has a nested input control.