Merge branch 'trunk' into PCP-4110-incorrect-subscription-cancellation-handling-with-pay-pal-subscriptions

This commit is contained in:
Emili Castells Guasch 2025-03-18 12:19:36 +01:00
commit 65e1ca4eb0
259 changed files with 9482 additions and 3476 deletions

40
.env.integration Normal file
View file

@ -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"

View file

@ -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" BASEURL="https://woocommerce-paypal-payments.ddev.site"
AUTHORIZATION="Bearer ABC123" AUTHORIZATION="Bearer ABC123"

View file

@ -1,4 +1,4 @@
name: e2e tests name: Integration tests
on: workflow_dispatch on: workflow_dispatch
@ -7,8 +7,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
php-versions: ['7.4', '8.2'] php-versions: ['7.4']
wc-versions: ['6.9.4', '7.7.2'] wc-versions: ['9.7.1']
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
steps: steps:
@ -30,10 +30,10 @@ jobs:
run: ddev orchestrate -f run: ddev orchestrate -f
- name: Create config - name: Create config
run: cp -n .env.e2e.example .env.e2e run: cp -n .env.integration.example .env.integration
- name: Setup tests - name: Setup tests
run: ddev php tests/e2e/PHPUnit/setup.php run: ddev php tests/integration/PHPUnit/setup.php
- name: Run PHPUnit - name: Run PHPUnit
run: ddev exec phpunit -c tests/e2e/phpunit.xml.dist run: ddev exec phpunit -c tests/integration/phpunit.xml.dist

View file

@ -94,6 +94,45 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group =
return 0; 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 * HTML API: WP_HTML_Tag_Processor class
*/ */

View file

@ -1,5 +1,21 @@
*** Changelog *** *** 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 = = 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 - 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 * Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872

View file

@ -41,7 +41,7 @@
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"WooCommerce\\PayPalCommerce\\": "tests/PHPUnit/", "WooCommerce\\PayPalCommerce\\": "tests/PHPUnit/",
"WooCommerce\\PayPalCommerce\\Tests\\E2e\\": "tests/e2e/PHPUnit/" "WooCommerce\\PayPalCommerce\\Tests\\Integration\\": "tests/integration/PHPUnit/"
} }
}, },
"minimum-stability": "dev", "minimum-stability": "dev",

View file

@ -91,9 +91,12 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); $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( if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', '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" )(); $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
} }

View file

@ -80,12 +80,11 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig; use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
return array( return array(
'api.host' => static function( ContainerInterface $container ) : string { 'api.host' => static function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment ); assert( $environment instanceof Environment );
if ( $environment->is_sandbox() ) { if ( $environment->is_sandbox() ) {
@ -671,6 +670,7 @@ return array(
'FR' => $default_currencies, 'FR' => $default_currencies,
'DE' => $default_currencies, 'DE' => $default_currencies,
'GR' => $default_currencies, 'GR' => $default_currencies,
'HK' => $default_currencies,
'HU' => $default_currencies, 'HU' => $default_currencies,
'IE' => $default_currencies, 'IE' => $default_currencies,
'IT' => $default_currencies, 'IT' => $default_currencies,
@ -688,6 +688,7 @@ return array(
'PT' => $default_currencies, 'PT' => $default_currencies,
'RO' => $default_currencies, 'RO' => $default_currencies,
'SK' => $default_currencies, 'SK' => $default_currencies,
'SG' => $default_currencies,
'SI' => $default_currencies, 'SI' => $default_currencies,
'ES' => $default_currencies, 'ES' => $default_currencies,
'SE' => $default_currencies, 'SE' => $default_currencies,
@ -736,6 +737,7 @@ return array(
'FR' => $mastercard_visa_amex, 'FR' => $mastercard_visa_amex,
'GB' => $mastercard_visa_amex, 'GB' => $mastercard_visa_amex,
'GR' => $mastercard_visa_amex, 'GR' => $mastercard_visa_amex,
'HK' => $mastercard_visa_amex,
'HU' => $mastercard_visa_amex, 'HU' => $mastercard_visa_amex,
'IE' => $mastercard_visa_amex, 'IE' => $mastercard_visa_amex,
'IT' => $mastercard_visa_amex, 'IT' => $mastercard_visa_amex,
@ -765,6 +767,7 @@ return array(
'SE' => $mastercard_visa_amex, 'SE' => $mastercard_visa_amex,
'SI' => $mastercard_visa_amex, 'SI' => $mastercard_visa_amex,
'SK' => $mastercard_visa_amex, 'SK' => $mastercard_visa_amex,
'SG' => $mastercard_visa_amex,
'JP' => array( 'JP' => array(
'mastercard' => array(), 'mastercard' => array(),
'visa' => array(), 'visa' => array(),

View file

@ -160,7 +160,7 @@ class PartnersEndpoint {
$this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY ); $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; return $status;
} }
} }

View file

@ -28,15 +28,23 @@ class SellerStatus {
*/ */
private $capabilities; private $capabilities;
/**
* Merchant country on PayPal.
*
* @var string
*/
private string $country;
/** /**
* SellerStatus constructor. * SellerStatus constructor.
* *
* @param SellerStatusProduct[] $products The products. * @param SellerStatusProduct[] $products The products.
* @param SellerStatusCapability[] $capabilities The capabilities. * @param SellerStatusCapability[] $capabilities The capabilities.
* @param string $country Merchant country on PayPal.
* *
* @psalm-suppress RedundantConditionGivenDocblockType * @psalm-suppress RedundantConditionGivenDocblockType
*/ */
public function __construct( array $products, array $capabilities ) { public function __construct( array $products, array $capabilities, string $country = '' ) {
foreach ( $products as $key => $product ) { foreach ( $products as $key => $product ) {
if ( is_a( $product, SellerStatusProduct::class ) ) { if ( is_a( $product, SellerStatusProduct::class ) ) {
continue; continue;
@ -52,6 +60,7 @@ class SellerStatus {
$this->products = $products; $this->products = $products;
$this->capabilities = $capabilities; $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 * @return array
*/ */
@ -95,6 +113,7 @@ class SellerStatus {
return array( return array(
'products' => $products, 'products' => $products,
'capabilities' => $capabilities, 'capabilities' => $capabilities,
'country' => $this->country,
); );
} }
} }

View file

@ -25,7 +25,7 @@ class SellerStatusFactory {
* *
* @return SellerStatus * @return SellerStatus
*/ */
public function from_paypal_reponse( \stdClass $json ) : SellerStatus { public function from_paypal_response( \stdClass $json ) : SellerStatus {
$products = array_map( $products = array_map(
function( $json ) : SellerStatusProduct { function( $json ) : SellerStatusProduct {
$product = new SellerStatusProduct( $product = new SellerStatusProduct(
@ -49,6 +49,6 @@ class SellerStatusFactory {
isset( $json->capabilities ) ? (array) $json->capabilities : array() isset( $json->capabilities ) ? (array) $json->capabilities : array()
); );
return new SellerStatus( $products, $capabilities ); return new SellerStatus( $products, $capabilities, $json->country ?? '' );
} }
} }

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\ApiClient\Repository * @package WooCommerce\PayPalCommerce\ApiClient\Repository
*/ */
declare(strict_types=1); declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Repository; namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
@ -18,43 +18,21 @@ class PartnerReferralsData {
/** /**
* The DCC Applies Helper object. * 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 * @var DccApplies
*/ */
private $dcc_applies; private DccApplies $dcc_applies;
/**
* The list of products ('PPCP', 'EXPRESS_CHECKOUT').
*
* @var string[]
*/
private $products;
/** /**
* PartnerReferralsData constructor. * PartnerReferralsData constructor.
* *
* @param DccApplies $dcc_applies The DCC Applies helper. * @param DccApplies $dcc_applies The DCC Applies helper.
*/ */
public function __construct( public function __construct( DccApplies $dcc_applies ) {
DccApplies $dcc_applies
) {
$this->dcc_applies = $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 * @return string
*/ */
public function nonce(): string { public function nonce() : string {
return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG'; return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG';
} }
/** /**
* Returns the data. * 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 * @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( $return_url = apply_filters(
'ppcp_partner_referrals_data', 'woocommerce_paypal_payments_partner_config_override_return_url',
array( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' )
'partner_config_override' => array( );
/**
* Returns the URL which will be opened at the end of onboarding. /**
*/ * Filter the label of the "Return to your shop" button.
'return_url' => apply_filters( * It's displayed on the very last page of the onboarding popup.
'woocommerce_paypal_payments_partner_config_override_return_url', */
admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' ) $return_url_label = apply_filters(
), 'woocommerce_paypal_payments_partner_config_override_return_url_description',
/** __( 'Return to your shop.', 'woocommerce-paypal-payments' )
* Returns the description of the URL which will be opened at the end of onboarding. );
*/
'return_url_description' => apply_filters( $capabilities = array();
'woocommerce_paypal_payments_partner_config_override_return_url_description', $first_party_features = array(
__( 'Return to your shop.', 'woocommerce-paypal-payments' ) 'PAYMENT',
), 'REFUND',
'show_add_credit_card' => true, '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( 'operations' => array(
array( array(
'type' => 'SHARE_DATA_CONSENT', 'operation' => 'API_INTEGRATION',
'granted' => true, 'api_integration_preference' => array(
), 'rest_api_integration' => array(
), 'integration_method' => 'PAYPAL',
'operations' => array( 'integration_type' => 'FIRST_PARTY',
array( 'first_party_details' => array(
'operation' => 'API_INTEGRATION', 'features' => $first_party_features,
'api_integration_preference' => array( 'seller_nonce' => $this->nonce(),
'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(),
),
), ),
), ),
), ),
), ),
) ),
); );
}
/** /**
* Append the validation token to the return_url * Filter the final partners referrals data collection.
* */
* @param array $data The referral data. $payload = apply_filters( 'ppcp_partner_referrals_data', $payload );
* @param string $token The token to be appended.
* @return array // An empty array is not permitted.
*/ if ( isset( $payload['capabilities'] ) && ! $payload['capabilities'] ) {
public function append_onboarding_token( array $data, string $token ): array { unset( $payload['capabilities'] );
$data['partner_config_override']['return_url'] = }
add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] );
return $data; // 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;
} }
} }

View file

@ -42,7 +42,7 @@ return array(
assert( $display_manager instanceof DisplayManager ); assert( $display_manager instanceof DisplayManager );
// Domain registration. // Domain registration.
$env = $container->get( 'onboarding.environment' ); $env = $container->get( 'settings.environment' );
assert( $env instanceof Environment ); assert( $env instanceof Environment );
$domain_registration_url = 'https://www.paypal.com/uccservicing/apm/applepay'; $domain_registration_url = 'https://www.paypal.com/uccservicing/apm/applepay';

View file

@ -20,7 +20,6 @@ use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array( return array(
@ -191,6 +190,7 @@ return array(
'FR', // France 'FR', // France
'DE', // Germany 'DE', // Germany
'GR', // Greece 'GR', // Greece
'HK', // Hong Kong
'HU', // Hungary 'HU', // Hungary
'IE', // Ireland 'IE', // Ireland
'IT', // Italy 'IT', // Italy
@ -204,6 +204,7 @@ return array(
'PL', // Poland 'PL', // Poland
'PT', // Portugal 'PT', // Portugal
'RO', // Romania 'RO', // Romania
'SG', // Singapore
'SK', // Slovakia 'SK', // Slovakia
'SI', // Slovenia 'SI', // Slovenia
'ES', // Spain 'ES', // Spain
@ -233,6 +234,7 @@ return array(
'CZK', // Czech Koruna 'CZK', // Czech Koruna
'DKK', // Danish Krone 'DKK', // Danish Krone
'EUR', // Euro 'EUR', // Euro
'HKD', // Hong Kong Dollar
'GBP', // British Pound Sterling 'GBP', // British Pound Sterling
'HUF', // Hungarian Forint 'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel 'ILS', // Israeli New Shekel
@ -242,6 +244,7 @@ return array(
'NZD', // New Zealand Dollar 'NZD', // New Zealand Dollar
'PHP', // Philippine Peso 'PHP', // Philippine Peso
'PLN', // Polish Zloty 'PLN', // Polish Zloty
'SGD', // Singapur-Dollar
'SEK', // Swedish Krona 'SEK', // Swedish Krona
'THB', // Thai Baht 'THB', // Thai Baht
'TWD', // New Taiwan Dollar 'TWD', // New Taiwan Dollar
@ -260,15 +263,15 @@ return array(
}, },
'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string { 'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' ); $is_connected = $container->get( 'settings.flag.is-connected' );
if ( $state->current_state() < State::STATE_ONBOARDED ) { if ( ! $is_connected ) {
return ''; return '';
} }
$product_status = $container->get( 'applepay.apple-product-status' ); $product_status = $container->get( 'applepay.apple-product-status' );
assert( $product_status instanceof AppleProductStatus ); assert( $product_status instanceof AppleProductStatus );
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment ); assert( $environment instanceof Environment );
$enabled = $product_status->is_active(); $enabled = $product_status->is_active();

View file

@ -368,7 +368,7 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
if ( ! $button->is_enabled() ) { if ( ! $button->is_enabled() ) {
return; return;
} }
$env = $c->get( 'onboarding.environment' ); $env = $c->get( 'settings.environment' );
assert( $env instanceof Environment ); assert( $env instanceof Environment );
$is_sandobx = $env->current_environment_is( Environment::SANDBOX ); $is_sandobx = $env->current_environment_is( Environment::SANDBOX );
$this->load_domain_association_file( $is_sandobx ); $this->load_domain_association_file( $is_sandobx );

View file

@ -1018,6 +1018,10 @@ class ApplePayButton implements ButtonInterface {
* @return void * @return void
*/ */
public function enqueue(): void { public function enqueue(): void {
if ( ! $this->is_enabled() ) {
return;
}
wp_register_script( wp_register_script(
'wc-ppcp-applepay', 'wc-ppcp-applepay',
untrailingslashit( $this->module_url ) . '/assets/js/boot.js', untrailingslashit( $this->module_url ) . '/assets/js/boot.js',

View file

@ -89,6 +89,7 @@ class AppleProductStatus extends ProductStatus {
} }
} }
// Settings used as a cache; `settings->set` is compatible with new UI.
if ( $has_capability ) { if ( $has_capability ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else { } else {

View file

@ -36,7 +36,7 @@ return array(
fn(): SmartButtonInterface => $container->get( 'button.smart-button' ), fn(): SmartButtonInterface => $container->get( 'button.smart-button' ),
$container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.configuration.dcc' ), $container->get( 'wcgateway.configuration.dcc' ),
$container->get( 'onboarding.environment' ), $container->get( 'settings.environment' ),
$container->get( 'wcgateway.url' ), $container->get( 'wcgateway.url' ),
$container->get( 'axo.payment_method_selected_map' ), $container->get( 'axo.payment_method_selected_map' ),
$container->get( 'axo.supported-country-card-type-matrix' ) $container->get( 'axo.supported-country-card-type-matrix' )

View file

@ -107,11 +107,9 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule
'woocommerce_blocks_payment_method_type_registration', 'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
/* /*
* Only register the method if we are not in the admin * Only register the method if we are not in the admin or the customer is not logged in.
* (to avoid two Debit & Credit Cards gateways in the
* checkout block in the editor: one from ACDC one from Axo).
*/ */
if ( ! is_admin() ) { if ( ! is_user_logged_in() ) {
$payment_method_registry->register( $c->get( 'axoblock.method' ) ); $payment_method_registry->register( $c->get( 'axoblock.method' ) );
} }
} }

View file

@ -10,7 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo; namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Axo\Helper\NoticeRenderer; use WooCommerce\PayPalCommerce\Axo\Helper\NoticeRenderer;
use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager; use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;

View file

@ -12,7 +12,7 @@ namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager; use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies; 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\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -38,8 +38,8 @@ return array(
); );
}, },
'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator { 'axo.helpers.compatibility-checker' => static function ( ContainerInterface $container ) : CompatibilityChecker {
return new SettingsNoticeGenerator( $container->get( 'axo.fastlane-incompatible-plugin-names' ) ); return new CompatibilityChecker( $container->get( 'axo.fastlane-incompatible-plugin-names' ) );
}, },
// If AXO is configured and onboarded. // If AXO is configured and onboarded.
@ -66,7 +66,7 @@ return array(
$container->get( 'ppcp.asset-version' ), $container->get( 'ppcp.asset-version' ),
$container->get( 'session.handler' ), $container->get( 'session.handler' ),
$container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.environment' ), $container->get( 'settings.environment' ),
$container->get( 'axo.insights' ), $container->get( 'axo.insights' ),
$container->get( 'wcgateway.settings.status' ), $container->get( 'wcgateway.settings.status' ),
$container->get( 'api.shop.currency.getter' ), $container->get( 'api.shop.currency.getter' ),
@ -89,7 +89,7 @@ return array(
$container->get( 'api.factory.purchase-unit' ), $container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.factory.shipping-preference' ), $container->get( 'api.factory.shipping-preference' ),
$container->get( 'wcgateway.transaction-url-provider' ), $container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'onboarding.environment' ), $container->get( 'settings.environment' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
@ -190,29 +190,44 @@ return array(
); );
}, },
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string { 'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); assert( $compatibility_checker instanceof CompatibilityChecker );
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof 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 { 'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); $compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); 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 { 'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' ); $settings_notice_generator = $container->get( 'axo.helpers.compatibility-checker' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator ); assert( $settings_notice_generator instanceof CompatibilityChecker );
return $settings_notice_generator->generate_incompatible_plugins_notice(); 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 { 'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' ); $dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration ); assert( $dcc_configuration instanceof DCCGatewayConfiguration );

View file

@ -475,7 +475,7 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
* @return void * @return void
*/ */
private function add_feature_detection_tag( bool $axo_enabled ) { 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 ) { if ( ! $show_tag ) {
return; return;

View file

@ -0,0 +1,254 @@
<?php
/**
* Fastlane compatibility checker.
* Detects compatibility issues and generates relevant notices.
*
* @package WooCommerce\PayPalCommerce\Axo\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Helper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
/**
* Class CompatibilityChecker
*/
class CompatibilityChecker {
/**
* The list of Fastlane incompatible plugin names.
*
* @var string[]
*/
protected array $incompatible_plugin_names;
/**
* Stores the result of checkout compatibility checks.
*
* @var array
*/
protected array $checkout_compatibility;
/**
* Stores whether DCC is enabled.
*
* @var bool|null
*/
protected ?bool $is_dcc_enabled = null;
/**
* CompatibilityChecker constructor.
*
* @param string[] $incompatible_plugin_names The list of Fastlane incompatible plugin names.
*/
public function __construct( array $incompatible_plugin_names ) {
$this->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(
'<div class="ppcp-notice %1$s"><p>%2$s</p></div>',
$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. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the <code>Elementor Checkout widget</code>. To enable Fastlane and accelerate payments, the page must include either the <code>Checkout</code> block, <code>Classic Checkout</code>, or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> 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. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store does not seem to be properly configured or uses an incompatible <code>third-party Checkout</code> solution. To enable Fastlane and accelerate payments, the page must include either the <code>Checkout</code> block, <code>Classic Checkout</code>, or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> 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. */
__(
'<span class="highlight">Note:</span> The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following <a href="%1$s">active plugins</a>: <ul class="ppcp-notice-list">%2$s</ul>',
'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 = __(
'<span class="highlight">Warning:</span> To enable Fastlane and accelerate payments, the <strong>Advanced Card Processing</strong> payment method must also be enabled.',
'woocommerce-paypal-payments'
);
return $this->render_notice( $notice_content, true, $raw_message );
}
}

View file

@ -1,147 +0,0 @@
<?php
/**
* Settings notice generator.
* Generates the settings notices.
*
* @package WooCommerce\PayPalCommerce\Axo\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo\Helper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
/**
* Class SettingsNoticeGenerator
*/
class SettingsNoticeGenerator {
/**
* The list of Fastlane incompatible plugin names.
*
* @var string[]
*/
protected $incompatible_plugin_names;
/**
* SettingsNoticeGenerator constructor.
*
* @param string[] $incompatible_plugin_names The list of Fastlane incompatible plugin names.
*/
public function __construct( array $incompatible_plugin_names ) {
$this->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(
'<div class="ppcp-notice %1$s"><p>%2$s</p></div>',
$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. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the <code>Elementor Checkout widget</code>. To enable Fastlane and accelerate payments, the page must include either the <code>Checkout</code> block, <code>Classic Checkout</code>, or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> 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. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store does not seem to be properly configured or uses an incompatible <code>third-party Checkout</code> solution. To enable Fastlane and accelerate payments, the page must include either the <code>Checkout</code> block, <code>Classic Checkout</code>, or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> 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 ? '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>' : '';
}
/**
* 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. */
__(
'<span class="highlight">Note:</span> The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following <a href="%1$s">active plugins</a>: <ul class="ppcp-notice-list">%2$s</ul>',
'woocommerce-paypal-payments'
),
$plugins_settings_link,
implode( '', $this->incompatible_plugin_names )
);
return '<div class="ppcp-notice"><p>' . $notice_content . '</p></div>';
}
/**
* 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 = __(
'<span class="highlight">Warning:</span> To enable Fastlane and accelerate payments, the <strong>Advanced Card Processing</strong> payment method must also be enabled.',
'woocommerce-paypal-payments'
);
}
return $this->render_notice( $notice_content, true );
}
}

View file

@ -67,27 +67,6 @@ export const PayPalComponent = ( {
? `${ config.id }-${ fundingSource }` ? `${ config.id }-${ fundingSource }`
: config.id; : 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( () => { useEffect( () => {
// fill the form if in continuation (for product or mini-cart buttons) // fill the form if in continuation (for product or mini-cart buttons)
if ( continuationFilled || ! config.scriptData.continuation?.order ) { if ( continuationFilled || ! config.scriptData.continuation?.order ) {
@ -339,7 +318,6 @@ export const PayPalComponent = ( {
shouldskipFinalConfirmation, shouldskipFinalConfirmation,
getCheckoutRedirectUrl, getCheckoutRedirectUrl,
setGotoContinuationOnError, setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit, onSubmit,
onError, onError,
onClose onClose
@ -439,7 +417,6 @@ export const PayPalComponent = ( {
shouldskipFinalConfirmation, shouldskipFinalConfirmation,
getCheckoutRedirectUrl, getCheckoutRedirectUrl,
setGotoContinuationOnError, setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit, onSubmit,
onError, onError,
onClose onClose
@ -476,7 +453,6 @@ export const PayPalComponent = ( {
shouldskipFinalConfirmation, shouldskipFinalConfirmation,
getCheckoutRedirectUrl, getCheckoutRedirectUrl,
setGotoContinuationOnError, setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit, onSubmit,
onError, onError,
onClose onClose

View file

@ -81,12 +81,14 @@ export const paypalShippingToWc = ( shipping ) => {
export const paypalPayerToWc = ( payer ) => { export const paypalPayerToWc = ( payer ) => {
const firstName = payer?.name?.given_name ?? ''; const firstName = payer?.name?.given_name ?? '';
const lastName = payer?.name?.surname ?? ''; const lastName = payer?.name?.surname ?? '';
const phone = payer?.phone?.phone_number?.national_number ?? '';
const address = payer.address ? paypalAddressToWc( payer.address ) : {}; const address = payer.address ? paypalAddressToWc( payer.address ) : {};
return { return {
...address, ...address,
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
email: payer.email_address, email: payer.email_address,
phone: phone
}; };
}; };

View file

@ -61,7 +61,6 @@ export const handleApprove = async (
shouldskipFinalConfirmation, shouldskipFinalConfirmation,
getCheckoutRedirectUrl, getCheckoutRedirectUrl,
setGotoContinuationOnError, setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit, onSubmit,
onError, onError,
onClose onClose
@ -132,7 +131,6 @@ export const handleApprove = async (
location.href = getCheckoutRedirectUrl(); location.href = getCheckoutRedirectUrl();
} else { } else {
setGotoContinuationOnError( true ); setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit(); onSubmit();
} }
} catch ( err ) { } catch ( err ) {
@ -171,7 +169,6 @@ export const handleApproveSubscription = async (
shouldskipFinalConfirmation, shouldskipFinalConfirmation,
getCheckoutRedirectUrl, getCheckoutRedirectUrl,
setGotoContinuationOnError, setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit, onSubmit,
onError, onError,
onClose onClose
@ -242,7 +239,6 @@ export const handleApproveSubscription = async (
location.href = getCheckoutRedirectUrl(); location.href = getCheckoutRedirectUrl();
} else { } else {
setGotoContinuationOnError( true ); setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit(); onSubmit();
} }
} catch ( err ) { } catch ( err ) {

View file

@ -28,13 +28,6 @@ return array(
); );
}, },
'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod { '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( return new PayPalPaymentMethod(
$container->get( 'blocks.url' ), $container->get( 'blocks.url' ),
$container->get( 'ppcp.asset-version' ), $container->get( 'ppcp.asset-version' ),
@ -53,7 +46,6 @@ return array(
$container->get( 'wcgateway.place-order-button-text' ), $container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'wcgateway.place-order-button-description' ), $container->get( 'wcgateway.place-order-button-description' ),
$container->get( 'wcgateway.all-funding-sources' ), $container->get( 'wcgateway.all-funding-sources' ),
$cart && $cart->needs_shipping()
); );
}, },
'blocks.advanced-card-method' => static function( ContainerInterface $container ): AdvancedCardPaymentMethod { 'blocks.advanced-card-method' => static function( ContainerInterface $container ): AdvancedCardPaymentMethod {

View file

@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
*/ */
class UpdateShippingEndpoint implements EndpointInterface { class UpdateShippingEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-update-shipping'; 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. * The Request Data Helper.

View file

@ -130,13 +130,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/ */
private $all_funding_sources; 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. * 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_text The text for the standard "Place order" button.
* @param string $place_order_button_description The text for additional "Place order" description. * @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 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( public function __construct(
string $module_url, string $module_url,
@ -172,8 +164,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
bool $use_place_order, bool $use_place_order,
string $place_order_button_text, string $place_order_button_text,
string $place_order_button_description, string $place_order_button_description,
array $all_funding_sources, array $all_funding_sources
bool $need_shipping
) { ) {
$this->name = PayPalGateway::ID; $this->name = PayPalGateway::ID;
$this->module_url = $module_url; $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_text = $place_order_button_text;
$this->place_order_button_description = $place_order_button_description; $this->place_order_button_description = $place_order_button_description;
$this->all_funding_sources = $all_funding_sources; $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' ); && $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 ) $place_order_enabled = ( $this->use_place_order || $this->add_place_order_method )
&& ! $this->subscription_helper->cart_contains_subscription(); && ! $this->subscription_helper->cart_contains_subscription();
$cart = WC()->cart;
return array( return array(
'id' => $this->gateway->id, 'id' => $this->gateway->id,
@ -284,7 +275,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
), ),
), ),
'scriptData' => $script_data, 'scriptData' => $script_data,
'needShipping' => $this->need_shipping, 'needShipping' => $cart && $cart->needs_shipping(),
); );
} }

View file

@ -21,8 +21,8 @@ export const handleShippingOptionsChange = async ( data, actions, config ) => {
credentials: 'same-origin', credentials: 'same-origin',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-WC-Store-API-Nonce': 'Nonce':
config.ajax.update_customer_shipping.wp_rest_nonce, config.ajax.update_customer_shipping.wp_rest_nonce,
}, },
body: JSON.stringify( { body: JSON.stringify( {
rate_id: shippingOptionId, rate_id: shippingOptionId,
@ -106,9 +106,9 @@ export const handleShippingAddressChange = async ( data, actions, config ) => {
credentials: 'same-origin', credentials: 'same-origin',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-WC-Store-API-Nonce': 'Nonce':
config.ajax.update_customer_shipping config.ajax.update_customer_shipping
.wp_rest_nonce, .wp_rest_nonce,
}, },
body: JSON.stringify( { body: JSON.stringify( {
shipping_address: cartData.shipping_address, shipping_address: cartData.shipping_address,

View file

@ -36,7 +36,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
@ -49,7 +48,7 @@ return array(
return $client_id; return $client_id;
} }
$env = $container->get( 'onboarding.environment' ); $env = $container->get( 'settings.environment' );
/** /**
* The environment. * The environment.
* *
@ -125,8 +124,8 @@ return array(
} }
} }
$state = $container->get( 'onboarding.state' ); $is_connected = $container->get( 'settings.flag.is-connected' );
if ( $state->current_state() !== State::STATE_ONBOARDED ) { if ( ! $is_connected ) {
return new DisabledSmartButton(); return new DisabledSmartButton();
} }
@ -142,7 +141,7 @@ return array(
$dcc_applies = $container->get( 'api.helpers.dccapplies' ); $dcc_applies = $container->get( 'api.helpers.dccapplies' );
$subscription_helper = $container->get( 'wc-subscriptions.helper' ); $subscription_helper = $container->get( 'wc-subscriptions.helper' );
$messages_apply = $container->get( 'button.helper.messages-apply' ); $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' ); $payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
return new SmartButton( return new SmartButton(
$container->get( 'button.url' ), $container->get( 'button.url' ),
@ -241,11 +240,11 @@ return array(
); );
}, },
'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler { 'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler {
return new EarlyOrderHandler(
$state = $container->get( 'onboarding.state' ); $container->get( 'settings.flag.is-connected' ),
$order_processor = $container->get( 'wcgateway.order-processor' ); $container->get( 'wcgateway.order-processor' ),
$session_handler = $container->get( 'session.handler' ); $container->get( 'session.handler' )
return new EarlyOrderHandler( $state, $order_processor, $session_handler ); );
}, },
'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint { 'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint {
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );

View file

@ -272,8 +272,9 @@ class ApproveOrderEndpoint implements EndpointInterface {
* @return void * @return void
*/ */
protected function toggle_final_review_enabled_setting(): 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->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(); $this->settings->persist();
} }
} }

View file

@ -104,6 +104,6 @@ class DisabledFundingSources {
$disable_funding = $all_sources; $disable_funding = $all_sources;
} }
return $disable_funding; return apply_filters( 'woocommerce_paypal_payments_disabled_funding_sources', $disable_funding );
} }
} }

View file

@ -11,8 +11,6 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; 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\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
@ -23,11 +21,11 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
class EarlyOrderHandler { 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. * The Order Processor.
@ -46,17 +44,17 @@ class EarlyOrderHandler {
/** /**
* EarlyOrderHandler constructor. * EarlyOrderHandler constructor.
* *
* @param State $state The State. * @param bool $is_connected Whether onboarding was completed.
* @param OrderProcessor $order_processor The Order Processor. * @param OrderProcessor $order_processor The Order Processor.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session Handler.
*/ */
public function __construct( public function __construct(
State $state, bool $is_connected,
OrderProcessor $order_processor, OrderProcessor $order_processor,
SessionHandler $session_handler SessionHandler $session_handler
) { ) {
$this->state = $state; $this->is_connected = $is_connected;
$this->order_processor = $order_processor; $this->order_processor = $order_processor;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
} }
@ -67,7 +65,7 @@ class EarlyOrderHandler {
* @return bool * @return bool
*/ */
public function should_create_early_order(): 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 //phpcs:disable WordPress.Security.NonceVerification.Recommended

View file

@ -199,8 +199,9 @@ class WooCommerceOrderCreator {
$shipping_options = null; $shipping_options = null;
if ( $payer ) { if ( $payer ) {
$address = $payer->address(); $address = $payer->address();
$payer_name = $payer->name(); $payer_name = $payer->name();
$payer_phone = $payer->phone();
$wc_email = null; $wc_email = null;
$wc_customer = WC()->customer; $wc_customer = WC()->customer;
@ -220,6 +221,7 @@ class WooCommerceOrderCreator {
'state' => $address ? $address->admin_area_1() : '', 'state' => $address ? $address->admin_area_1() : '',
'postcode' => $address ? $address->postal_code() : '', 'postcode' => $address ? $address->postal_code() : '',
'country' => $address ? $address->country_code() : '', 'country' => $address ? $address->country_code() : '',
'phone' => $payer_phone ? $payer_phone->phone()->national_number() : '',
); );
} }

View file

@ -43,6 +43,7 @@ return array(
'FR', 'FR',
'DE', 'DE',
'GR', 'GR',
'HK',
'HU', 'HU',
'IE', 'IE',
'IT', 'IT',
@ -56,6 +57,7 @@ return array(
'PT', 'PT',
'RO', 'RO',
'SK', 'SK',
'SG',
'SI', 'SI',
'ES', 'ES',
'SE', 'SE',

View file

@ -10,9 +10,13 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat; namespace WooCommerce\PayPalCommerce\Compat;
use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets; 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\SettingsMap;
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper; use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsTabMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\StylingSettingsMapHelper; use WooCommerce\PayPalCommerce\Compat\Settings\StylingSettingsMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\SubscriptionSettingsMapHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array( return array(
@ -133,30 +137,26 @@ return array(
$styling_settings_map_helper = $container->get( 'compat.settings.styling_map_helper' ); $styling_settings_map_helper = $container->get( 'compat.settings.styling_map_helper' );
assert( $styling_settings_map_helper instanceof StylingSettingsMapHelper ); 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( return array(
new SettingsMap( new SettingsMap(
$container->get( 'settings.data.general' ), $container->get( 'settings.data.general' ),
/** $general_map_helper->map()
* The new GeneralSettings class stores the current connection ),
* details, without adding an environment-suffix (no `_sandbox` new SettingsMap(
* or `_production` in the field name) $container->get( 'settings.data.settings' ),
* Only the `sandbox_merchant` flag indicates, which environment $settings_tab_map_helper->map()
* 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',
)
), ),
new SettingsMap( new SettingsMap(
$container->get( 'settings.data.styling' ), $container->get( 'settings.data.styling' ),
@ -172,15 +172,50 @@ return array(
*/ */
$styling_settings_map_helper->map() $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 { 'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper {
return new SettingsMapHelper( return new SettingsMapHelper(
$container->get( 'compat.setting.new-to-old-map' ), $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 { 'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper {
return new 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();
},
); );

View file

@ -0,0 +1,70 @@
<?php
/**
* A helper for mapping old and new general settings.
*
* @package WooCommerce\PayPalCommerce\Compat\Settings
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\Settings;
/**
* Handles mapping between old and new general settings.
*
* @psalm-import-type newSettingsKey from SettingsMap
* @psalm-import-type oldSettingsKey from SettingsMap
*/
class GeneralSettingsMapHelper {
/**
* Maps old setting keys to new setting keys.
*
* 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.
*
* @psalm-return array<oldSettingsKey, newSettingsKey>
*/
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<string, scalar|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;
}
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* A helper for mapping the old/new payment method settings.
*
* @package WooCommerce\PayPalCommerce\Compat\Settings
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\Settings;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
/**
* A map of old to new payment method settings.
*
* @psalm-import-type newSettingsKey from SettingsMap
* @psalm-import-type oldSettingsKey from SettingsMap
*/
class PaymentMethodSettingsMapHelper {
/**
* Maps old setting keys to new payment method settings names.
*
* @psalm-return array<oldSettingsKey, newSettingsKey>
*/
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';
}
}

View file

@ -10,6 +10,10 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Compat\Settings; namespace WooCommerce\PayPalCommerce\Compat\Settings;
use RuntimeException; 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; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
/** /**
@ -49,17 +53,70 @@ class SettingsMapHelper {
*/ */
protected StylingSettingsMapHelper $styling_settings_map_helper; 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. * Constructor.
* *
* @param SettingsMap[] $settings_map A list of settings maps containing key definitions. * @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 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. * @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->validate_settings_map( $settings_map );
$this->settings_map = $settings_map; $this->settings_map = $settings_map;
$this->styling_settings_map_helper = $styling_settings_map_helper; $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. * @return mixed|null The value of the mapped setting, or null if not found.
*/ */
public function mapped_value( string $old_key ) { public function mapped_value( string $old_key ) {
if ( ! $this->new_settings_module_enabled ) {
return null;
}
$this->ensure_map_initialized(); $this->ensure_map_initialized();
if ( ! isset( $this->key_to_model[ $old_key ] ) ) { if ( ! isset( $this->key_to_model[ $old_key ] ) ) {
return null; return null;
@ -112,6 +173,10 @@ class SettingsMapHelper {
* @return bool True if the key exists in the new settings, false otherwise. * @return bool True if the key exists in the new settings, false otherwise.
*/ */
public function has_mapped_key( string $old_key ) : bool { public function has_mapped_key( string $old_key ) : bool {
if ( ! $this->new_settings_module_enabled ) {
return false;
}
$this->ensure_map_initialized(); $this->ensure_map_initialized();
return isset( $this->key_to_model[ $old_key ] ); return isset( $this->key_to_model[ $old_key ] );
@ -134,7 +199,23 @@ class SettingsMapHelper {
switch ( true ) { switch ( true ) {
case $model instanceof StylingSettings: 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: default:
return $this->model_cache[ $model_id ][ $new_key ] ?? null; 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;
}
} }

View file

@ -0,0 +1,146 @@
<?php
/**
* A helper for mapping the old/new settings tab settings.
*
* @package WooCommerce\PayPalCommerce\Compat\Settings
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
/**
* A map of old to new styling settings.
*
* @psalm-import-type newSettingsKey from SettingsMap
* @psalm-import-type oldSettingsKey from SettingsMap
*/
class SettingsTabMapHelper {
use ContextTrait;
/**
* Maps old setting keys to new setting keys.
*
* @psalm-return array<oldSettingsKey, newSettingsKey>
*/
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<string, scalar|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<string, scalar|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<string, scalar|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<string, scalar|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<string, scalar|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;
}
}

View file

@ -10,7 +10,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\Settings; namespace WooCommerce\PayPalCommerce\Compat\Settings;
use RuntimeException; use RuntimeException;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; 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; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO;
/** /**
@ -23,6 +27,8 @@ class StylingSettingsMapHelper {
use ContextTrait; use ContextTrait;
protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID );
/** /**
* Maps old setting keys to new setting style names. * Maps old setting keys to new setting style names.
* *
@ -40,11 +46,13 @@ class StylingSettingsMapHelper {
public function map(): array { public function map(): array {
$mapped_settings = array( $mapped_settings = array(
'smart_button_locations' => '', 'smart_button_locations' => '',
'pay_later_button_locations' => '', 'pay_later_button_locations' => '',
'disable_funding' => '', 'disable_funding' => '',
'googlepay_button_enabled' => '', 'googlepay_button_enabled' => '',
'applepay_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 ) { 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. * Retrieves the value of a mapped key from the new settings.
* *
* @param string $old_key The key from the legacy settings. * @param string $old_key The key from the legacy 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 mixed The value of the mapped setting, (null if not found). * @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 ) { switch ( $old_key ) {
case 'smart_button_locations': case 'smart_button_locations':
return $this->mapped_smart_button_locations_value( $styling_models ); return $this->mapped_smart_button_locations_value( $styling_models );
case 'smart_button_enable_styling_per_location':
return true;
case 'pay_later_button_locations': case 'pay_later_button_locations':
return $this->mapped_pay_later_button_locations_value( $styling_models ); return $this->mapped_pay_later_button_locations_value( $styling_models );
case 'disable_funding': 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': 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': 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: default:
foreach ( $this->locations_map() as $old_location_name => $new_location_name ) { foreach ( $this->locations_map() as $old_location_name => $new_location_name ) {
@ -198,7 +213,7 @@ class StylingSettingsMapHelper {
$enabled_locations = array(); $enabled_locations = array();
$locations = array_flip( $this->locations_map() ); $locations = array_flip( $this->locations_map() );
foreach ( $styling_models as $model ) { 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; continue;
} }
@ -214,52 +229,116 @@ class StylingSettingsMapHelper {
/** /**
* Retrieves the mapped disabled funding value from the new settings. * 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. * @return array|null The list of disabled funding, or null if none are disabled.
*/ */
protected function mapped_disabled_funding_value( array $styling_models ): ?array { protected function mapped_disabled_funding_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?array {
$disabled_funding = array(); if ( is_null( $payment_settings ) ) {
$locations_to_context_map = $this->current_context_to_new_button_location_map(); return null;
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;
} }
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 LocationStylingDTO[] $styling_models The list of location styling models.
* @param 'googlepay'|'applepay' $button_name The button name ('googlepay' or 'applepay'). * @param AbstractDataModel|null $payment_settings The payment settings model.
* @return int The enabled (1) or disabled (0) state. * @return int|null The enabled (1) or disabled (0) state or null if it should fall back to old settings value.
* @throws RuntimeException If an invalid button name is provided.
*/ */
protected function mapped_google_pay_or_apple_pay_enabled_value( array $styling_models, string $button_name ): ?int { protected function mapped_pay_later_button_enabled_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?int {
if ( $button_name !== 'googlepay' && $button_name !== 'applepay' ) {
throw new RuntimeException( 'Wrong button name is provided. Either "googlepay" or "applepay" can be used' ); if ( ! $payment_settings instanceof PaymentSettings ) {
return null;
} }
$locations_to_context_map = $this->current_context_to_new_button_location_map(); $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 ) { foreach ( $styling_models as $model ) {
if ( ! $model->enabled if ( $model->enabled && $model->location === $current_context ) {
|| $model->location !== $locations_to_context_map[ $this->context() ] if ( in_array( 'pay-later', $model->methods, true ) && $payment_settings->get_paylater_enabled() ) {
|| ! in_array( $button_name, $model->methods, true ) return 1;
) { }
continue;
} }
return 1;
} }
return 0; 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 {
?>
<style data-hide-gateway='<?php echo esc_attr( GooglePayGateway::ID ); ?>'>
.wc_payment_method.payment_method_ppcp-googlepay {
display: none;
}
</style>
<?php
}
);
}
return 0;
}
/**
* 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';
}
} }

View file

@ -0,0 +1,85 @@
<?php
/**
* A helper for mapping old and new subscription settings.
*
* @package WooCommerce\PayPalCommerce\Compat\Settings
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Handles mapping between old and new subscription settings.
*
* In the new settings UI, the Subscriptions mode value is set automatically based on the merchant type.
* This class fakes the mapping and injects the appropriate value based on the merchant:
* - Non-vaulting merchants will use PayPal Subscriptions.
* - Merchants with vaulting will use PayPal Vaulting.
* - Disabled subscriptions can be controlled using a filter.
*
* @psalm-import-type newSettingsKey from SettingsMap
* @psalm-import-type oldSettingsKey from SettingsMap
*/
class SubscriptionSettingsMapHelper {
public const OLD_SETTINGS_SUBSCRIPTION_MODE_VALUE_VAULTING = 'vaulting_api';
public const OLD_SETTINGS_SUBSCRIPTION_MODE_VALUE_SUBSCRIPTIONS = 'subscriptions_api';
public const OLD_SETTINGS_SUBSCRIPTION_MODE_VALUE_DISABLED = 'disable_paypal_subscriptions';
/**
* The subscription helper.
*
* @var SubscriptionHelper $subscription_helper
*/
protected SubscriptionHelper $subscription_helper;
/**
* Constructor.
*
* @param SubscriptionHelper $subscription_helper The subscription helper.
*/
public function __construct( SubscriptionHelper $subscription_helper ) {
$this->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<oldSettingsKey, newSettingsKey>
*/
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<string, scalar|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;
}
}

View file

@ -81,22 +81,23 @@ const GooglePayComponent = ( { isEditing, buttonAttributes } ) => {
}; };
const features = [ 'products' ]; const features = [ 'products' ];
if ( buttonConfig?.is_enabled ) {
registerExpressPaymentMethod( { registerExpressPaymentMethod( {
name: buttonData.id, name: buttonData.id,
title: `PayPal - ${ buttonData.title }`, title: `PayPal - ${ buttonData.title }`,
description: __( description: __(
'Eligible users will see the PayPal button.', 'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
gatewayId: 'ppcp-gateway', gatewayId: 'ppcp-gateway',
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />, label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
content: <GooglePayComponent isEditing={ false } />, content: <GooglePayComponent isEditing={ false } />,
edit: <GooglePayComponent isEditing={ true } />, edit: <GooglePayComponent isEditing={ true } />,
ariaLabel: buttonData.title, ariaLabel: buttonData.title,
canMakePayment: () => buttonData.enabled, canMakePayment: () => buttonData.enabled,
supports: { supports: {
features, features,
style: [ 'height', 'borderRadius' ], style: [ 'height', 'borderRadius' ],
}, },
} ); } );
}

View file

@ -19,7 +19,6 @@ use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmApplies;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus; use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array( return array(
@ -106,6 +105,7 @@ return array(
'FR', // France 'FR', // France
'DE', // Germany 'DE', // Germany
'GR', // Greece 'GR', // Greece
'HK', // Hong Kong
'HU', // Hungary 'HU', // Hungary
'IE', // Ireland 'IE', // Ireland
'IT', // Italy 'IT', // Italy
@ -119,6 +119,7 @@ return array(
'PL', // Poland 'PL', // Poland
'PT', // Portugal 'PT', // Portugal
'RO', // Romania 'RO', // Romania
'SG', // Singapore
'SK', // Slovakia 'SK', // Slovakia
'SI', // Slovenia 'SI', // Slovenia
'ES', // Spain 'ES', // Spain
@ -148,6 +149,7 @@ return array(
'CZK', // Czech Koruna 'CZK', // Czech Koruna
'DKK', // Danish Krone 'DKK', // Danish Krone
'EUR', // Euro 'EUR', // Euro
'HKD', // Hong Kong Dollar
'GBP', // British Pound Sterling 'GBP', // British Pound Sterling
'HUF', // Hungarian Forint 'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel 'ILS', // Israeli New Shekel
@ -157,6 +159,7 @@ return array(
'NZD', // New Zealand Dollar 'NZD', // New Zealand Dollar
'PHP', // Philippine Peso 'PHP', // Philippine Peso
'PLN', // Polish Zloty 'PLN', // Polish Zloty
'SGD', // Singapur-Dollar
'SEK', // Swedish Krona 'SEK', // Swedish Krona
'THB', // Thai Baht 'THB', // Thai Baht
'TWD', // New Taiwan Dollar 'TWD', // New Taiwan Dollar
@ -174,7 +177,7 @@ return array(
$container->get( 'session.handler' ), $container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' ), $container->get( 'wc-subscriptions.helper' ),
$container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.environment' ), $container->get( 'settings.environment' ),
$container->get( 'wcgateway.settings.status' ), $container->get( 'wcgateway.settings.status' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
@ -221,15 +224,15 @@ return array(
}, },
'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string { 'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' ); $is_connected = $container->get( 'settings.flag.is-connected' );
if ( $state->current_state() < State::STATE_ONBOARDED ) { if ( ! $is_connected ) {
return ''; return '';
} }
$product_status = $container->get( 'googlepay.helpers.apm-product-status' ); $product_status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus ); assert( $product_status instanceof ApmProductStatus );
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment ); assert( $environment instanceof Environment );
$enabled = $product_status->is_active(); $enabled = $product_status->is_active();

View file

@ -89,6 +89,7 @@ class ApmProductStatus extends ProductStatus {
} }
} }
// Settings used as a cache; `settings->set` is compatible with new UI.
if ( $has_capability ) { if ( $has_capability ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else { } else {

View file

@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Order; use WC_Order;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; 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\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -43,8 +42,31 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
/** /**
* {@inheritDoc} * {@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( add_filter(
'woocommerce_payment_gateways', 'woocommerce_payment_gateways',
/** /**
@ -53,14 +75,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function ( $methods ) use ( $c ) { 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 ) ) { if ( ! is_array( $methods ) ) {
return $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( add_filter(
'woocommerce_available_payment_gateways', 'woocommerce_available_payment_gateways',
/** /**
@ -82,29 +100,22 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function ( $methods ) use ( $c ) { function ( $methods ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) { if ( ! is_array( $methods ) || is_admin() || empty( WC()->customer ) ) {
return $methods; // Don't restrict the gateway list on wp-admin or when no customer is known.
}
if ( ! is_array( $methods ) ) {
return $methods; return $methods;
} }
if ( ! is_admin() ) { $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
if ( ! isset( WC()->customer ) ) { $customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country();
return $methods; $site_currency = get_woocommerce_currency();
}
$customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country(); // Remove unsupported gateways from the customer's payment options.
$site_currency = get_woocommerce_currency(); 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' ); if ( ! $is_currency_supported || ! $is_country_supported ) {
foreach ( $payment_methods as $payment_method ) { unset( $methods[ $payment_method['id'] ] );
if (
! in_array( $customer_country, $payment_method['countries'], true )
|| ! in_array( $site_currency, $payment_method['currencies'], true )
) {
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( add_action(
'woocommerce_blocks_payment_method_type_registration', 'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { 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' ); $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $key => $value ) { foreach ( $payment_methods as $key => $value ) {
$payment_method_registry->register( $c->get( 'ppcp-local-apms.' . $key . '.payment-method' ) ); $payment_method_registry->register( $c->get( 'ppcp-local-apms.' . $key . '.payment-method' ) );
@ -128,9 +142,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
add_filter( add_filter(
'woocommerce_paypal_payments_localized_script_data', 'woocommerce_paypal_payments_localized_script_data',
function ( array $data ) use ( $c ) { function ( array $data ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return $data;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' ); $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
$default_disable_funding = $data['url_params']['disable-funding'] ?? ''; $default_disable_funding = $data['url_params']['disable-funding'] ?? '';
@ -149,9 +160,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function( $order_id ) use ( $c ) { function( $order_id ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return;
}
$order = wc_get_order( $order_id ); $order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) { if ( ! $order instanceof WC_Order ) {
return; return;
@ -184,9 +192,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
add_action( add_action(
'woocommerce_paypal_payments_payment_capture_completed_webhook_handler', 'woocommerce_paypal_payments_payment_capture_completed_webhook_handler',
function( WC_Order $wc_order, string $order_id ) use ( $c ) { 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' ); $payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
if ( if (
! $this->is_local_apm( $wc_order->get_payment_method(), $payment_methods ) ! $this->is_local_apm( $wc_order->get_payment_method(), $payment_methods )
@ -202,8 +207,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
10, 10,
2 2
); );
return true;
} }
/** /**
@ -229,12 +232,42 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @param ContainerInterface $container Container. * @param ContainerInterface $container Container.
* @return bool * @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' ); $settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
return $settings->has( 'enabled' ) if ( ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' ) ) {
&& $settings->get( 'enabled' ) === true return false;
&& $settings->has( 'allow_local_apm_gateways' ) }
// Register APM gateways, when the relevant setting is active.
return $settings->has( 'allow_local_apm_gateways' )
&& $settings->get( 'allow_local_apm_gateways' ) === true; && $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/' );
}
} }

View file

@ -71,6 +71,7 @@ class LocalApmProductStatus extends ProductStatus {
} }
} }
// Settings used as a cache; `settings->set` is compatible with new UI.
if ( $has_capability ) { if ( $has_capability ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else { } else {

View file

@ -326,9 +326,9 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
isDisconnecting = true; isDisconnecting = true;
const saveButton = document.querySelector( '.woocommerce-save-button' ); const saveButton = document.querySelector( '.woocommerce-save-button' );
saveButton.removeAttribute( 'disabled' ); saveButton.removeAttribute( 'disabled' );
saveButton.click(); saveButton.click();
}; };
// Prevent the message about unsaved checkbox/radiobutton when reloading the page. // 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' ); const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' );
sandboxSwitchElement?.addEventListener( 'click', () => { sandboxSwitchElement?.addEventListener( 'click', () => {
document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' ); document
}); .querySelector( '.woocommerce-save-button' )
?.removeAttribute( 'disabled' );
} );
const validate = () => { const validate = () => {
const selectors = sandboxSwitchElement.checked const selectors = sandboxSwitchElement.checked
@ -389,7 +391,8 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
const isSandboxInBackend = const isSandboxInBackend =
PayPalCommerceGatewayOnboarding.current_env === 'sandbox'; PayPalCommerceGatewayOnboarding.current_env === 'sandbox';
if ( sandboxSwitchElement.checked !== isSandboxInBackend ) {
if ( sandboxSwitchElement?.checked !== isSandboxInBackend ) {
sandboxSwitchElement.checked = isSandboxInBackend; sandboxSwitchElement.checked = isSandboxInBackend;
} }

View file

@ -18,10 +18,11 @@ use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array( return array(
'api.paypal-host' => function( ContainerInterface $container ) : string { 'api.paypal-host' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'settings.environment' );
/** /**
* The current environment. * The current environment.
* *
@ -34,7 +35,7 @@ return array(
}, },
'api.paypal-website-url' => function( ContainerInterface $container ) : string { 'api.paypal-website-url' => function( ContainerInterface $container ) : string {
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment ); assert( $environment instanceof Environment );
if ( $environment->current_environment_is( Environment::SANDBOX ) ) { if ( $environment->current_environment_is( Environment::SANDBOX ) ) {
return $container->get( 'api.paypal-website-url-sandbox' ); return $container->get( 'api.paypal-website-url-sandbox' );
@ -56,9 +57,16 @@ return array(
return $state->current_state() >= State::STATE_ONBOARDED; 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' ); $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 { 'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
@ -68,7 +76,7 @@ return array(
$container->get( 'onboarding.url' ), $container->get( 'onboarding.url' ),
$container->get( 'ppcp.asset-version' ), $container->get( 'ppcp.asset-version' ),
$state, $state,
$container->get( 'onboarding.environment' ), $container->get( 'settings.environment' ),
$login_seller_endpoint, $login_seller_endpoint,
$container->get( 'wcgateway.current-ppcp-settings-page-id' ) $container->get( 'wcgateway.current-ppcp-settings-page-id' )
); );

View file

@ -133,9 +133,11 @@ class OnboardingRESTController {
* @return array * @return array
*/ */
public function get_status( $request ) { public function get_status( $request ) {
$environment = $this->container->get( 'onboarding.environment' ); $environment = $this->container->get( 'settings.environment' );
$state = $this->container->get( 'onboarding.state' ); $state = $this->container->get( 'onboarding.state' );
// Legacy onboarding module; using `State::STATE_ONBOARDED` checks is valid here.
return array( return array(
'environment' => $environment->current_environment(), 'environment' => $environment->current_environment(),
'onboarded' => ( $state->current_state() >= State::STATE_ONBOARDED ), 'onboarded' => ( $state->current_state() >= State::STATE_ONBOARDED ),

View file

@ -103,12 +103,8 @@ class OnboardingRenderer {
'displayMode' => 'minibrowser', 'displayMode' => 'minibrowser',
); );
$data = $this->partner_referrals_data
->with_products( $products )
->data();
$environment = $is_production ? 'production' : 'sandbox'; $environment = $is_production ? 'production' : 'sandbox';
$product = 'PPCP' === $data['products'][0] ? 'ppcp' : 'express_checkout'; $product = strtolower( $products[0] ?? 'express_checkout' );
$cache_key = $environment . '-' . $product; $cache_key = $environment . '-' . $product;
$onboarding_url = new OnboardingUrl( $this->cache, $cache_key, get_current_user_id() ); $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, get_current_user_id() );
@ -122,8 +118,7 @@ class OnboardingRenderer {
$onboarding_url->init(); $onboarding_url->init();
$data = $this->partner_referrals_data $data = $this->partner_referrals_data->data( $products, $onboarding_url->token() ?: '' );
->append_onboarding_token( $data, $onboarding_url->token() ?: '' );
$url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data ); $url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data );
$url = add_query_arg( $args, $url ); $url = add_query_arg( $args, $url );

View file

@ -95,6 +95,7 @@ class SaveConfig {
* @param array $config The configurator config. * @param array $config The configurator config.
*/ */
public function save_config( array $config ): void { 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_enable_styling_per_messaging_location', true );
$this->settings->set( 'pay_later_messaging_enabled', true ); $this->settings->set( 'pay_later_messaging_enabled', true );

View file

@ -374,7 +374,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
function( $subscription ) use ( $c ) { function( $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) { 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'; $host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
?> ?>
<tr> <tr>
@ -488,7 +488,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
return; return;
} }
$environment = $c->get( 'onboarding.environment' ); $environment = $c->get( 'settings.environment' );
echo '<div class="options_group subscription_pricing show_if_subscription hidden">'; echo '<div class="options_group subscription_pricing show_if_subscription hidden">';
$this->render_paypal_subscription_fields( $product, $environment ); $this->render_paypal_subscription_fields( $product, $environment );
echo '</div>'; echo '</div>';
@ -522,7 +522,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
return; return;
} }
$environment = $c->get( 'onboarding.environment' ); $environment = $c->get( 'settings.environment' );
$this->render_paypal_subscription_fields( $product, $environment ); $this->render_paypal_subscription_fields( $product, $environment );
} }

View file

@ -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&section=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 | ❌ |

View file

@ -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.
<details>
<summary><strong>Setup the PayPal REST app</strong></summary>
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
</details>
**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<br/>(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.

View file

@ -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.
---

View file

@ -17,6 +17,7 @@ $color-text-text: #070707;
$color-border: #AEAEAE; $color-border: #AEAEAE;
$color-divider: #F0F0F0; $color-divider: #F0F0F0;
$color-error-red: #cc1818; $color-error-red: #cc1818;
$color-warning: #e2a030;
$shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15); $shadow-card: 0 3px 6px 0 rgba(0, 0, 0, 0.15);
$color-gradient-dark: #001435; $color-gradient-dark: #001435;
@ -66,6 +67,7 @@ $card-vertical-gap: 48px;
--color-text-teriary: #{$color-text-tertiary}; --color-text-teriary: #{$color-text-tertiary};
--color-text-description: #{$color-gray-700}; --color-text-description: #{$color-gray-700};
--color-error: #{$color-error-red}; --color-error: #{$color-error-red};
--color-warning: #{$color-warning};
// Default settings-block theme. // Default settings-block theme.
--block-item-gap: 16px; --block-item-gap: 16px;

View file

@ -63,6 +63,10 @@
&:hover { &:hover {
transform: rotate(45deg); transform: rotate(45deg);
} }
&.components-button {
height: auto;
}
} }
.ppcp--method-icon { .ppcp--method-icon {
@ -79,6 +83,234 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: auto; 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;
} }
} }
} }

View file

@ -73,7 +73,6 @@
gap: 8px; gap: 8px;
&--save { &--save {
margin-top: -4px;
align-items: flex-end; align-items: flex-end;
} }
@ -87,7 +86,7 @@
&__field-rows { &__field-rows {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 18px;
&--acdc { &--acdc {
gap: 18px; gap: 18px;
@ -98,19 +97,11 @@
} }
.components-radio-control { .components-radio-control {
.components-flex {
gap: 18px;
}
label { label {
@include font(14, 20, 400); @include font(14, 20, 400);
color: $color-black; color: $color-black;
} }
&__option {
gap: 18px;
}
&__input { &__input {
border-color: $color-gray-700; border-color: $color-gray-700;
margin-right: 0; margin-right: 0;

View file

@ -24,6 +24,10 @@
padding-top: var(--block-separator-gap, 32px); padding-top: var(--block-separator-gap, 32px);
border-top: var(--block-separator-size, 1px) solid var(--block-separator-color); border-top: var(--block-separator-size, 1px) solid var(--block-separator-color);
} }
&.ppcp--pull-right {
float: right;
}
} }
.ppcp-r-settings-block { .ppcp-r-settings-block {

View file

@ -69,4 +69,34 @@ $width_gap: 24px;
color: var(--color-text-teriary); color: var(--color-text-teriary);
margin: 0; 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;
}
}
}
} }

View file

@ -13,14 +13,3 @@
padding-bottom: 36px; 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;
}
}

View file

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

View file

@ -226,30 +226,91 @@
} }
} }
// Payment Methods .ppcp-r-settings {
.ppcp-r-payment-methods { .ppcp-highlight {
display: flex; position: relative;
flex-direction: column; z-index: 1;
gap: 48px;
}
.ppcp-highlight { &::before {
animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1); content: '';
border: 1px solid $color-blueberry; position: absolute;
border-radius: var(--container-border-radius); top: -8px;
position: relative; left: -12px;
z-index: 1; 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 { &::after {
0%, 20% { content: '';
background-color: rgba($color-blueberry, 0.08); position: absolute;
border-color: $color-blueberry; top: -8px;
border-width: 1px; 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; @keyframes ppcp-setting-highlight-bg {
border-color: $color-gray-300; 0%, 15% {
border-width: 1px; 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;
}
}
} }
} }

View file

@ -13,7 +13,7 @@
#configurator-eligibleContainer.css-4nclxm.e1vy3g880 { #configurator-eligibleContainer.css-4nclxm.e1vy3g880 {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
padding: 48px 0px 48px 48px; padding: 16px 0px 16px 16px;
#configurator-controlPanelContainer.css-5urmrq.e1vy3g880 { #configurator-controlPanelContainer.css-5urmrq.e1vy3g880 {
width: 374px; width: 374px;
@ -25,6 +25,7 @@
.css-7xkxom, .css-8tvj6u { .css-7xkxom, .css-8tvj6u {
height: auto; height: auto;
width: 1.2rem;
} }
.css-10nkerk.ej6n7t60 { .css-10nkerk.ej6n7t60 {
@ -37,14 +38,19 @@
} }
.css-1vc34jy-handler { .css-1vc34jy-handler {
height: 1.6rem; height: 1.7em;
width: 1.6rem; width: 1.5rem;
} }
.css-8vwtr6-state { .css-8vwtr6-state {
height: 1.6rem; height: 1.4rem;
width: 3rem;
} }
} }
.css-1s8clkf.etu8a6w2 {
width: 374px;
}
} }
&__subheader, #configurator-controlPanelSubHeader { &__subheader, #configurator-controlPanelSubHeader {
@ -68,6 +74,7 @@
.css-rok10q, .css-dfgbdq-text_body_strong { .css-rok10q, .css-dfgbdq-text_body_strong {
margin-top: 0; margin-top: 0;
margin-bottom: 0;
} }
&__publish-button { &__publish-button {
@ -110,4 +117,30 @@
width: 100%; 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;
}
}
} }

View file

@ -3,8 +3,8 @@
--block-separator-gap: 24px; --block-separator-gap: 24px;
--block-header-gap: 18px; --block-header-gap: 18px;
--panel-width: 422px; --panel-width: 422px;
--sticky-offset-top: 92px; // 32px admin-bar + 60px TopNavigation height --sticky-offset-top: 132px; // 32px admin-bar + 100px TopNavigation height
--preview-height-reduction: 236px; // 32px admin-bar + 60px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin --preview-height-reduction: 276px; // 32px admin-bar + 100px TopNavigation height + 48px TopNavigation margin + 48px TabList height + 48px TabList margin
display: flex; display: flex;
border: 1px solid var(--color-separators); border: 1px solid var(--color-separators);

View file

@ -11,3 +11,4 @@
@import './components/reusable-components/payment-method-modal'; @import './components/reusable-components/payment-method-modal';
@import './components/screens/fullscreen'; @import './components/screens/fullscreen';
@import './components/screens/modals';

View file

@ -6,13 +6,13 @@ import SpinnerOverlay from './ReusableComponents/SpinnerOverlay';
import SendOnlyMessage from './Screens/SendOnlyMessage'; import SendOnlyMessage from './Screens/SendOnlyMessage';
import OnboardingScreen from './Screens/Onboarding'; import OnboardingScreen from './Screens/Onboarding';
import SettingsScreen from './Screens/Settings'; import SettingsScreen from './Screens/Settings';
import { getQuery } from '../utils/navigation'; import { getQuery, cleanUrlQueryParams } from '../utils/navigation';
const SettingsApp = () => { const SettingsApp = () => {
const { isReady: onboardingIsReady, completed: onboardingCompleted } = const { isReady: onboardingIsReady, completed: onboardingCompleted } =
OnboardingHooks.useSteps(); OnboardingHooks.useSteps();
const { isReady: merchantIsReady } = CommonHooks.useStore();
const { const {
isReady: merchantIsReady,
merchant: { isSendOnlyCountry }, merchant: { isSendOnlyCountry },
} = CommonHooks.useMerchantInfo(); } = CommonHooks.useMerchantInfo();
@ -32,9 +32,19 @@ const SettingsApp = () => {
loading: ! onboardingIsReady, loading: ! onboardingIsReady,
} ); } );
const [ activePanel, setActivePanel ] = useState( const [ activePanel, setActivePanel ] = useState( getQuery().panel );
getQuery().panel || 'overview'
); const removeUnsupportedArgs = () => {
const urlWasCleaned = cleanUrlQueryParams( [
'page',
'tab',
'section',
] );
if ( urlWasCleaned ) {
setActivePanel( '' );
}
};
const Content = useMemo( () => { const Content = useMemo( () => {
if ( ! onboardingIsReady || ! merchantIsReady ) { if ( ! onboardingIsReady || ! merchantIsReady ) {
@ -42,16 +52,18 @@ const SettingsApp = () => {
} }
if ( isSendOnlyCountry ) { if ( isSendOnlyCountry ) {
removeUnsupportedArgs();
return <SendOnlyMessage />; return <SendOnlyMessage />;
} }
if ( ! onboardingCompleted ) { if ( ! onboardingCompleted ) {
removeUnsupportedArgs();
return <OnboardingScreen />; return <OnboardingScreen />;
} }
return ( return (
<SettingsScreen <SettingsScreen
activePanel={ activePanel } activePanel={ activePanel || 'overview' }
setActivePanel={ setActivePanel } setActivePanel={ setActivePanel }
/> />
); );

View file

@ -2,7 +2,7 @@ import { Icon } from '@wordpress/components';
import { chevronDown, chevronUp } from '@wordpress/icons'; import { chevronDown, chevronUp } from '@wordpress/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { useAccordionState } from '../../hooks/useAccordionState'; import { useToggleState } from '../../hooks/useToggleState';
import { import {
Content, Content,
Description, Description,
@ -21,7 +21,7 @@ const Accordion = ( {
children = null, children = null,
className = '', className = '',
} ) => { } ) => {
const { isOpen, toggleOpen } = useAccordionState( { id, initiallyOpen } ); const { isOpen, toggleOpen } = useToggleState( id, initiallyOpen );
const wrapperClasses = classNames( 'ppcp-r-accordion', className, { const wrapperClasses = classNames( 'ppcp-r-accordion', className, {
'ppcp--is-open': isOpen, 'ppcp--is-open': isOpen,
} ); } );

View file

@ -1,8 +1,15 @@
import { ToggleControl } from '@wordpress/components'; import { ToggleControl } from '@wordpress/components';
import { Action, Description } from '../Elements'; import { Action, Description } from '../Elements';
const ControlToggleButton = ( { label, description, value, onChange } ) => ( const ControlToggleButton = ( {
<Action> id = '',
label,
description,
value,
onChange,
disabled = false,
} ) => (
<Action id={ id }>
<ToggleControl <ToggleControl
className="ppcp--control-toggle" className="ppcp--control-toggle"
__nextHasNoMarginBottom __nextHasNoMarginBottom
@ -12,6 +19,7 @@ const ControlToggleButton = ( { label, description, value, onChange } ) => (
help={ help={
description ? <Description>{ description }</Description> : null description ? <Description>{ description }</Description> : null
} }
disabled={ disabled }
/> />
</Action> </Action>
); );

View file

@ -18,6 +18,7 @@ const DataStoreControl = React.forwardRef(
control: ControlComponent, control: ControlComponent,
value: externalValue, value: externalValue,
onChange, onChange,
onConfirm = null,
delay = 300, delay = 300,
...props ...props
}, },
@ -25,7 +26,9 @@ const DataStoreControl = React.forwardRef(
) => { ) => {
const [ internalValue, setInternalValue ] = useState( externalValue ); const [ internalValue, setInternalValue ] = useState( externalValue );
const onChangeRef = useRef( onChange ); const onChangeRef = useRef( onChange );
const onConfirmRef = useRef( onConfirm );
onChangeRef.current = onChange; onChangeRef.current = onChange;
onConfirmRef.current = onConfirm;
const debouncedUpdate = useRef( const debouncedUpdate = useRef(
debounce( ( value ) => { debounce( ( value ) => {
@ -36,7 +39,7 @@ const DataStoreControl = React.forwardRef(
useEffect( () => { useEffect( () => {
setInternalValue( externalValue ); setInternalValue( externalValue );
debouncedUpdate?.cancel(); debouncedUpdate?.cancel();
}, [ externalValue ] ); }, [ debouncedUpdate, externalValue ] );
useEffect( () => { useEffect( () => {
return () => debouncedUpdate?.cancel(); return () => debouncedUpdate?.cancel();
@ -50,12 +53,25 @@ const DataStoreControl = React.forwardRef(
[ debouncedUpdate ] [ debouncedUpdate ]
); );
const handleKeyDown = useCallback(
( event ) => {
if ( onConfirmRef.current && event.key === 'Enter' ) {
event.preventDefault();
debouncedUpdate.flush();
onConfirmRef.current();
return false;
}
},
[ debouncedUpdate ]
);
return ( return (
<ControlComponent <ControlComponent
ref={ ref } ref={ ref }
{ ...props } { ...props }
value={ internalValue } value={ internalValue }
onChange={ handleChange } onChange={ handleChange }
onKeyDown={ handleKeyDown }
/> />
); );
} }

View file

@ -1,5 +1,7 @@
const Action = ( { children } ) => ( const Action = ( { id, children } ) => (
<div className="ppcp--action">{ children }</div> <div className="ppcp--action" { ...( id ? { id } : {} ) }>
{ children }
</div>
); );
export default Action; export default Action;

View file

@ -0,0 +1,11 @@
import classNames from 'classnames';
const CardActions = ( { isDimmed = false, children } ) => {
const className = classNames( 'ppcp--card-actions', {
'ppcp--dimmed': isDimmed,
} );
return <div className={ className }>{ children }</div>;
};
export default CardActions;

View file

@ -3,6 +3,7 @@
*/ */
export { default as Action } from './Action'; export { default as Action } from './Action';
export { default as CardActions } from './CardActions';
export { default as Content } from './Content'; export { default as Content } from './Content';
export { default as ContentWrapper } from './ContentWrapper'; export { default as ContentWrapper } from './ContentWrapper';
export { default as Description } from './Description'; export { default as Description } from './Description';

View file

@ -9,7 +9,13 @@ const OptionSelector = ( {
} ) => ( } ) => (
<div className="ppcp-r-select-box-wrapper"> <div className="ppcp-r-select-box-wrapper">
{ options.map( { options.map(
( { value: itemValue, title, description, contents } ) => { ( {
value: itemValue,
title,
description,
contents,
isDisabled = false,
} ) => {
let isSelected; let isSelected;
if ( Array.isArray( value ) ) { if ( Array.isArray( value ) ) {
@ -27,6 +33,7 @@ const OptionSelector = ( {
onChange={ onChange } onChange={ onChange }
isMulti={ multiSelect } isMulti={ multiSelect }
isSelected={ isSelected } isSelected={ isSelected }
isDisabled={ isDisabled }
> >
{ contents } { contents }
</OptionItem> </OptionItem>
@ -46,13 +53,13 @@ const OptionItem = ( {
isMulti, isMulti,
isSelected, isSelected,
children, children,
isDisabled = false,
} ) => { } ) => {
const boxClassName = classNames( 'ppcp-r-select-box', { const boxClassName = classNames( 'ppcp-r-select-box', {
'ppcp--selected': isSelected, 'ppcp--selected': isSelected,
'ppcp--multiselect': isMulti, 'ppcp--multiselect': isMulti,
'ppcp--no-title': ! itemTitle, 'ppcp--no-title': ! itemTitle,
} ); } );
return ( return (
// eslint-disable-next-line jsx-a11y/label-has-associated-control -- label has a nested input control. // eslint-disable-next-line jsx-a11y/label-has-associated-control -- label has a nested input control.
<label className={ boxClassName }> <label className={ boxClassName }>
@ -61,6 +68,7 @@ const OptionItem = ( {
isRadio={ ! isMulti } isRadio={ ! isMulti }
onChange={ onChange } onChange={ onChange }
isSelected={ isSelected } isSelected={ isSelected }
isDisabled={ isDisabled }
/> />
<div className="ppcp--box-content"> <div className="ppcp--box-content">
@ -80,7 +88,7 @@ const OptionItem = ( {
); );
}; };
const InputField = ( { value, onChange, isRadio, isSelected } ) => { const InputField = ( { value, onChange, isRadio, isSelected, isDisabled } ) => {
if ( isRadio ) { if ( isRadio ) {
return ( return (
<PayPalRdb <PayPalRdb
@ -96,6 +104,7 @@ const InputField = ( { value, onChange, isRadio, isSelected } ) => {
value={ value } value={ value }
onChange={ onChange } onChange={ onChange }
checked={ isSelected } checked={ isSelected }
disabled={ isDisabled }
/> />
); );
}; };

View file

@ -1,20 +1,11 @@
import { Icon } from '@wordpress/components'; import { Icon } from '@wordpress/components';
import data from '../../utils/data'; import data from '../../utils/data';
const PaymentMethodIcon = ( { icons, type } ) => { const PaymentMethodIcon = ( { type } ) => (
const validIcon = Array.isArray( icons ) && icons.includes( type ); <Icon
icon={ data().getImage( `icon-button-${ type }.svg` ) }
if ( validIcon || icons === 'all' ) { className="ppcp--method-icon"
return ( />
<Icon );
icon={ data().getImage( 'icon-button-' + type + '.svg' ) }
className="ppcp--method-icon"
/>
);
}
return null;
};
export default PaymentMethodIcon; export default PaymentMethodIcon;

View file

@ -1,20 +1,11 @@
import PaymentMethodIcon from './PaymentMethodIcon'; import PaymentMethodIcon from './PaymentMethodIcon';
const PaymentMethodIcons = ( props ) => { const PaymentMethodIcons = ( { icons = [] } ) => (
return ( <div className="ppcp-r-payment-method-icons">
<div className="ppcp-r-payment-method-icons"> { icons.map( ( type ) => (
<PaymentMethodIcon type="paypal" icons={ props.icons } /> <PaymentMethodIcon key={ type } type={ type } />
<PaymentMethodIcon type="venmo" icons={ props.icons } /> ) ) }
<PaymentMethodIcon type="visa" icons={ props.icons } /> </div>
<PaymentMethodIcon type="mastercard" icons={ props.icons } /> );
<PaymentMethodIcon type="amex" icons={ props.icons } />
<PaymentMethodIcon type="discover" icons={ props.icons } />
<PaymentMethodIcon type="apple-pay" icons={ props.icons } />
<PaymentMethodIcon type="google-pay" icons={ props.icons } />
<PaymentMethodIcon type="ideal" icons={ props.icons } />
<PaymentMethodIcon type="bancontact" icons={ props.icons } />
</div>
);
};
export default PaymentMethodIcons; export default PaymentMethodIcons;

View file

@ -1,39 +1,44 @@
import { ToggleControl, Icon, Button } from '@wordpress/components'; import { ToggleControl, Icon, Button } from '@wordpress/components';
import { cog } from '@wordpress/icons'; import { cog } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
import { useActiveHighlight } from '../../../data/common/hooks';
import SettingsBlock from '../SettingsBlock'; import SettingsBlock from '../SettingsBlock';
import PaymentMethodIcon from '../PaymentMethodIcon'; import PaymentMethodIcon from '../PaymentMethodIcon';
import WarningMessages from '../../../Components/Screens/Settings/Components/Payment/WarningMessages';
const PaymentMethodItemBlock = ( { const PaymentMethodItemBlock = ( {
paymentMethod, paymentMethod,
onTriggerModal, onTriggerModal,
onSelect, onSelect,
isSelected, isSelected,
isDisabled,
disabledMessage,
warningMessages,
} ) => { } ) => {
const { activeHighlight, setActiveHighlight } = useActiveHighlight(); const hasWarning =
const isHighlighted = activeHighlight === paymentMethod.id; warningMessages && Object.keys( warningMessages ).length > 0;
// Reset the active highlight after 2 seconds // Determine class names based on states
useEffect( () => { const methodItemClasses = [
if ( isHighlighted ) { 'ppcp--method-item',
const timer = setTimeout( () => { isDisabled ? 'ppcp--method-item--disabled' : '',
setActiveHighlight( null ); hasWarning && ! isDisabled ? 'ppcp--method-item--warning' : '',
}, 2000 ); ]
.filter( Boolean )
return () => clearTimeout( timer ); .join( ' ' );
}
}, [ isHighlighted, setActiveHighlight ] );
return ( return (
<SettingsBlock <SettingsBlock
id={ paymentMethod.id } id={ paymentMethod.id }
className={ `ppcp--method-item ${ className={ methodItemClasses }
isHighlighted ? 'ppcp-highlight' : ''
}` }
separatorAndGap={ false } separatorAndGap={ false }
> >
{ isDisabled && (
<div className="ppcp--method-disabled-overlay">
<p className="ppcp--method-disabled-message">
{ disabledMessage }
</p>
</div>
) }
<div className="ppcp--method-inner"> <div className="ppcp--method-inner">
<div className="ppcp--method-title-wrapper"> <div className="ppcp--method-title-wrapper">
{ paymentMethod?.icon && ( { paymentMethod?.icon && (
@ -50,11 +55,18 @@ const PaymentMethodItemBlock = ( {
{ paymentMethod.itemDescription } { paymentMethod.itemDescription }
</p> </p>
<div className="ppcp--method-footer"> <div className="ppcp--method-footer">
<ToggleControl <div className="ppcp--method-toggle-wrapper">
__nextHasNoMarginBottom <ToggleControl
checked={ isSelected } __nextHasNoMarginBottom
onChange={ onSelect } checked={ isSelected }
/> onChange={ onSelect }
/>
{ hasWarning && ! isDisabled && isSelected && (
<WarningMessages
warningMessages={ warningMessages }
/>
) }
</div>
{ paymentMethod?.fields && onTriggerModal && ( { paymentMethod?.fields && onTriggerModal && (
<Button <Button
className="ppcp--method-settings" className="ppcp--method-settings"

View file

@ -19,20 +19,25 @@ const PaymentMethodsBlock = ( { paymentMethods = [], onTriggerModal } ) => {
<SettingsBlock className="ppcp--grid ppcp-r-settings-block__payment-methods"> <SettingsBlock className="ppcp--grid ppcp-r-settings-block__payment-methods">
{ paymentMethods { paymentMethods
// Remove empty/invalid payment method entries. // Remove empty/invalid payment method entries.
.filter( ( m ) => m.id ) .filter( ( m ) => m && m.id )
.map( ( paymentMethod ) => ( .map( ( paymentMethod ) => {
<PaymentMethodItemBlock return (
key={ paymentMethod.id } <PaymentMethodItemBlock
paymentMethod={ paymentMethod } key={ paymentMethod.id }
isSelected={ paymentMethod.enabled } paymentMethod={ paymentMethod }
onSelect={ ( checked ) => isSelected={ paymentMethod.enabled }
handleSelect( paymentMethod.id, checked ) isDisabled={ paymentMethod.isDisabled }
} disabledMessage={ paymentMethod.disabledMessage }
onTriggerModal={ () => onSelect={ ( checked ) =>
onTriggerModal?.( paymentMethod.id ) handleSelect( paymentMethod.id, checked )
} }
/> onTriggerModal={ () =>
) ) } onTriggerModal?.( paymentMethod.id )
}
warningMessages={ paymentMethod.warningMessages }
/>
);
} ) }
</SettingsBlock> </SettingsBlock>
); );
}; };

View file

@ -7,7 +7,6 @@ const TodoSettingsBlock = ( {
todosData, todosData,
className = '', className = '',
setActiveModal, setActiveModal,
setActiveHighlight,
onDismissTodo, onDismissTodo,
} ) => { } ) => {
const [ dismissingIds, setDismissingIds ] = useState( new Set() ); const [ dismissingIds, setDismissingIds ] = useState( new Set() );
@ -44,29 +43,30 @@ const TodoSettingsBlock = ( {
}; };
const handleClick = async ( todo ) => { const handleClick = async ( todo ) => {
if ( todo.action.type === 'tab' ) { const { action } = todo;
const tabId = TAB_IDS[ todo.action.tab.toUpperCase() ]; const highlight = Boolean( action.highlight );
await selectTab( tabId, todo.action.section );
} else if ( todo.action.type === 'external' ) { // Handle different action types.
window.open( todo.action.url, '_blank' ); if ( action.type === 'tab' ) {
// If it has completeOnClick flag, trigger the action const tabId = TAB_IDS[ action.tab.toUpperCase() ];
if ( todo.action.completeOnClick === true ) { await selectTab( tabId, action.section, highlight );
await completeOnClick( todo.id ); } else if ( action.type === 'external' ) {
} window.open( action.url, '_blank' );
} }
if ( todo.action.modal ) { if ( action.completeOnClick ) {
setActiveModal( todo.action.modal ); await completeOnClick( todo.id );
} }
if ( todo.action.highlight ) {
setActiveHighlight( todo.action.highlight ); if ( action.modal ) {
setActiveModal( action.modal );
} }
}; };
// Filter out dismissed todos for display // Filter out dismissed todos for display and limit to 5.
const visibleTodos = todosData.filter( const visibleTodos = todosData
( todo ) => ! dismissedTodos.includes( todo.id ) .filter( ( todo ) => ! dismissedTodos.includes( todo.id ) )
); .slice( 0, 5 );
return ( return (
<div <div

View file

@ -32,7 +32,6 @@ const ButtonOrPlaceholder = ( {
if ( href ) { if ( href ) {
buttonProps.href = href; buttonProps.href = href;
buttonProps.target = 'PPFrame';
buttonProps[ 'data-paypal-button' ] = 'true'; buttonProps[ 'data-paypal-button' ] = 'true';
buttonProps[ 'data-paypal-onboard-button' ] = 'true'; buttonProps[ 'data-paypal-onboard-button' ] = 'true';
} }

View file

@ -157,6 +157,7 @@ const ManualConnectionForm = () => {
label={ clientIdLabel } label={ clientIdLabel }
value={ manualClientId } value={ manualClientId }
onChange={ setManualClientId } onChange={ setManualClientId }
onConfirm={ handleManualConnect }
className={ classNames( { className={ classNames( {
'ppcp--has-error': ! clientValid, 'ppcp--has-error': ! clientValid,
} ) } } ) }
@ -173,6 +174,7 @@ const ManualConnectionForm = () => {
label={ secretKeyLabel } label={ secretKeyLabel }
value={ manualClientSecret } value={ manualClientSecret }
onChange={ setManualClientSecret } onChange={ setManualClientSecret }
onConfirm={ handleManualConnect }
type="password" type="password"
/> />
<Button <Button

View file

@ -8,7 +8,6 @@ import { usePaymentConfig } from '../hooks/usePaymentConfig';
const PaymentFlow = ( { const PaymentFlow = ( {
useAcdc, useAcdc,
isFastlane, isFastlane,
isPayLater,
storeCountry, storeCountry,
onlyOptional = false, onlyOptional = false,
} ) => { } ) => {
@ -18,7 +17,8 @@ const PaymentFlow = ( {
optionalTitle, optionalTitle,
optionalDescription, optionalDescription,
learnMoreConfig, learnMoreConfig,
} = usePaymentConfig( storeCountry, isPayLater, useAcdc, isFastlane ); paypalCheckoutDescription,
} = usePaymentConfig( storeCountry, useAcdc, isFastlane );
if ( onlyOptional ) { if ( onlyOptional ) {
return ( return (
@ -34,6 +34,7 @@ const PaymentFlow = ( {
<DefaultMethodsSection <DefaultMethodsSection
methods={ includedMethods } methods={ includedMethods }
learnMoreConfig={ learnMoreConfig } learnMoreConfig={ learnMoreConfig }
paypalCheckoutDescription={ paypalCheckoutDescription }
/> />
<OptionalMethodsSection <OptionalMethodsSection
@ -48,10 +49,17 @@ const PaymentFlow = ( {
export default PaymentFlow; export default PaymentFlow;
const DefaultMethodsSection = ( { methods, learnMoreConfig } ) => { const DefaultMethodsSection = ( {
methods,
learnMoreConfig,
paypalCheckoutDescription,
} ) => {
return ( return (
<div className="ppcp-r-welcome-docs__col"> <div className="ppcp-r-welcome-docs__col">
<PayPalCheckout learnMore={ learnMoreConfig.PayPalCheckout } /> <PayPalCheckout
learnMore={ learnMoreConfig.PayPalCheckout }
description={ paypalCheckoutDescription }
/>
<BadgeBox <BadgeBox
title={ __( title={ __(
'Included in PayPal Checkout', 'Included in PayPal Checkout',

View file

@ -5,15 +5,15 @@ import BadgeBox from '../../../../ReusableComponents/BadgeBox';
const PayPalCheckout = ( { const PayPalCheckout = ( {
learnMore = 'https://www.paypal.com/us/business/accept-payments/checkout', learnMore = 'https://www.paypal.com/us/business/accept-payments/checkout',
description,
} ) => { } ) => {
const title = __( 'PayPal Checkout', 'woocommerce-paypal-payments' );
return ( return (
<BadgeBox <BadgeBox
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) } title={ title }
textBadge={ <PricingTitleBadge item="checkout" /> } textBadge={ <PricingTitleBadge item="checkout" /> }
description={ __( description={ description }
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
) }
learnMoreLink={ learnMore } learnMoreLink={ learnMore }
/> />
); );

View file

@ -11,7 +11,7 @@ const PricingDescription = () => {
return null; return null;
} }
const lastDate = 'October 25th, 2024'; // TODO -- needs to be the last plugin update date. const lastDate = 'February 1st, 2025'; // TODO -- needs to be the last plugin update date.
const countryLinks = learnMoreLinks[ storeCountry ] || learnMoreLinks.US; const countryLinks = learnMoreLinks[ storeCountry ] || learnMoreLinks.US;
const label = sprintf( const label = sprintf(

View file

@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import PricingDescription from './PricingDescription'; import PricingDescription from './PricingDescription';
import PaymentFlow from './PaymentFlow'; import PaymentFlow from './PaymentFlow';
const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => { const WelcomeDocs = ( { useAcdc, isFastlane, storeCountry } ) => {
return ( return (
<div className="ppcp-r-welcome-docs"> <div className="ppcp-r-welcome-docs">
<h2 className="ppcp-r-welcome-docs__title"> <h2 className="ppcp-r-welcome-docs__title">
@ -15,7 +15,6 @@ const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => {
<PaymentFlow <PaymentFlow
useAcdc={ useAcdc } useAcdc={ useAcdc }
isFastlane={ isFastlane } isFastlane={ isFastlane }
isPayLater={ isPayLater }
storeCountry={ storeCountry } storeCountry={ storeCountry }
/> />
<PricingDescription /> <PricingDescription />

View file

@ -22,9 +22,34 @@ const StepBusiness = ( {} ) => {
); );
useEffect( () => { useEffect( () => {
if ( ! businessChoice ) {
return;
}
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === businessChoice ); setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === businessChoice );
}, [ businessChoice, setIsCasualSeller ] ); }, [ businessChoice, setIsCasualSeller ] );
const { canUseSubscriptions } = OnboardingHooks.useFlags();
const businessChoices = [
{
value: BUSINESS_TYPES.BUSINESS,
title: __( 'Business', 'woocommerce-paypal-payments' ),
description: __(
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
'woocommerce-paypal-payments'
),
},
{
value: BUSINESS_TYPES.CASUAL_SELLER,
title: __( 'Personal Account', 'woocommerce-paypal-payments' ),
description: __(
'Ideal for those who primarily make purchases or send personal transactions to family and friends.',
'woocommerce-paypal-payments'
),
contents: canUseSubscriptions ? <DetailsAccountType /> : null,
},
];
return ( return (
<div className="ppcp-r-page-business"> <div className="ppcp-r-page-business">
<OnboardingHeader <OnboardingHeader
@ -45,23 +70,13 @@ const StepBusiness = ( {} ) => {
); );
}; };
const businessChoices = [ const DetailsAccountType = () => (
{ <p>
value: BUSINESS_TYPES.BUSINESS, { __(
title: __( 'Business', 'woocommerce-paypal-payments' ), '* Business account is required for subscriptions.',
description: __(
'Recommended for individuals and organizations that primarily use PayPal to sell goods or services or receive donations, even if your business is not incorporated.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ) }
}, </p>
{ );
value: BUSINESS_TYPES.CASUAL_SELLER,
title: __( 'Personal Account', 'woocommerce-paypal-payments' ),
description: __(
'Ideal for those who primarily make purchases or send personal transactions to family and friends.',
'woocommerce-paypal-payments'
),
},
];
export default StepBusiness; export default StepBusiness;

View file

@ -59,33 +59,18 @@ const StepPaymentMethods = () => {
export default StepPaymentMethods; export default StepPaymentMethods;
const PaymentStepTitle = () => { const PaymentStepTitle = () => {
const { storeCountry } = CommonHooks.useWooSettings(); return __( 'Add Credit and Debit Cards', 'woocommerce-paypal-payments' );
if ( 'US' === storeCountry ) {
return __(
'Add Expanded Checkout for More Ways to Pay',
'woocommerce-paypal-payments'
);
}
return __(
'Add optional payment methods to your Checkout',
'woocommerce-paypal-payments'
);
}; };
const OptionalMethodDescription = () => { const OptionalMethodDescription = () => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const { isCasualSeller } = OnboardingHooks.useBusiness(); const { isCasualSeller } = OnboardingHooks.useBusiness();
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
const { canUseCardPayments } = OnboardingHooks.useFlags();
/**
* Casual sellers = Personal accounts. Those accounts have no ACDC-capabilities, but should get
* the choice for BCDC-payments.
*/
return ( return (
<PaymentFlow <PaymentFlow
onlyOptional={ true } onlyOptional={ true }
useAcdc={ ! isCasualSeller } useAcdc={ ! isCasualSeller && canUseCardPayments }
isFastlane={ true } isFastlane={ true }
isPayLater={ true } isPayLater={ true }
storeCountry={ storeCountry } storeCountry={ storeCountry }

View file

@ -1,4 +1,4 @@
import { __ } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element'; import { useEffect, useState } from '@wordpress/element';
import { OptionSelector } from '../../../ReusableComponents/Fields'; import { OptionSelector } from '../../../ReusableComponents/Fields';
@ -10,26 +10,28 @@ const StepProducts = () => {
const { canUseSubscriptions } = OnboardingHooks.useFlags(); const { canUseSubscriptions } = OnboardingHooks.useFlags();
const [ optionState, setOptionState ] = useState( null ); const [ optionState, setOptionState ] = useState( null );
const [ productChoices, setProductChoices ] = useState( [] ); const [ productChoices, setProductChoices ] = useState( [] );
const { isCasualSeller } = OnboardingHooks.useBusiness();
useEffect( () => { useEffect( () => {
const initChoices = () => { const initChoices = () => {
if ( optionState === canUseSubscriptions ) { const choices = productChoicesFull.map( ( choice ) => {
return; if (
} choice.value === PRODUCT_TYPES.SUBSCRIPTIONS &&
! canUseSubscriptions
let choices = productChoicesFull; ) {
return {
// Remove subscription details, if not available. ...choice,
if ( ! canUseSubscriptions ) { isDisabled: true,
choices = choices.filter( contents: (
( { value } ) => value !== PRODUCT_TYPES.SUBSCRIPTIONS <DetailsSubscriptions
); showLink={ true }
setProducts( showNotice={ isCasualSeller }
products.filter( />
( value ) => value !== PRODUCT_TYPES.SUBSCRIPTIONS ),
) };
); }
} return choice;
} );
setProductChoices( choices ); setProductChoices( choices );
setOptionState( canUseSubscriptions ); setOptionState( canUseSubscriptions );
@ -48,7 +50,46 @@ const StepProducts = () => {
setProducts( getNewValue() ); setProducts( getNewValue() );
}; };
const productChoicesFull = [
{
value: PRODUCT_TYPES.VIRTUAL,
title: __( 'Virtual', 'woocommerce-paypal-payments' ),
description: __(
'Items do not require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsVirtual />,
},
{
value: PRODUCT_TYPES.PHYSICAL,
title: __( 'Physical Goods', 'woocommerce-paypal-payments' ),
description: __(
'Items require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsPhysical />,
},
{
value: PRODUCT_TYPES.SUBSCRIPTIONS,
title: __( 'Subscriptions', 'woocommerce-paypal-payments' ),
description: __(
'Recurring payments for either physical goods or services.',
'woocommerce-paypal-payments'
),
isDisabled: isCasualSeller,
contents: (
/*
* Note: The link should be only displayed if the subscriptions plugin is not installed.
* But when the plugin is not active, this option is completely hidden;
* This means: In the current configuration, we never show the link.
*/
<DetailsSubscriptions
showLink={ false }
showNotice={ isCasualSeller }
/>
),
},
];
return ( return (
<div className="ppcp-r-page-products"> <div className="ppcp-r-page-products">
<OnboardingHeader <OnboardingHeader
@ -87,41 +128,29 @@ const DetailsPhysical = () => (
</ul> </ul>
); );
const DetailsSubscriptions = () => ( const DetailsSubscriptions = ( { showLink, showNotice } ) => (
<a <>
target="__blank" { showLink && (
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq" <p
> dangerouslySetInnerHTML={ {
{ __( 'WooCommerce Subscriptions', 'woocommerce-paypal-payments' ) } __html: sprintf(
</a> /* translators: %s is the URL to the WooCommerce Subscriptions product page */
__(
'* To use subscriptions, you must have <a target="_blank" href="%s">WooCommerce Subscriptions</a> enabled.',
'woocommerce-paypal-payments'
),
'https://woocommerce.com/products/woocommerce-subscriptions/'
),
} }
/>
) }
{ showNotice && (
<p>
{ __(
'* Business account is required for subscriptions.',
'woocommerce-paypal-payments'
) }
</p>
) }
</>
); );
const productChoicesFull = [
{
value: PRODUCT_TYPES.VIRTUAL,
title: __( 'Virtual', 'woocommerce-paypal-payments' ),
description: __(
'Items do not require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsVirtual />,
},
{
value: PRODUCT_TYPES.PHYSICAL,
title: __( 'Physical Goods', 'woocommerce-paypal-payments' ),
description: __(
'Items require shipping.',
'woocommerce-paypal-payments'
),
contents: <DetailsPhysical />,
},
{
value: PRODUCT_TYPES.SUBSCRIPTIONS,
title: __( 'Subscriptions', 'woocommerce-paypal-payments' ),
description: __(
'Recurring payments for either physical goods or services.',
'woocommerce-paypal-payments'
),
contents: <DetailsSubscriptions />,
},
];

View file

@ -4,14 +4,32 @@ import { Button } from '@wordpress/components';
import PaymentMethodIcons from '../../../ReusableComponents/PaymentMethodIcons'; import PaymentMethodIcons from '../../../ReusableComponents/PaymentMethodIcons';
import { Separator } from '../../../ReusableComponents/Elements'; import { Separator } from '../../../ReusableComponents/Elements';
import Accordion from '../../../ReusableComponents/AccordionSection'; import Accordion from '../../../ReusableComponents/AccordionSection';
import { CommonHooks } from '../../../../data'; import { CommonHooks, OnboardingHooks } from '../../../../data';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper'; import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import OnboardingHeader from '../Components/OnboardingHeader'; import OnboardingHeader from '../Components/OnboardingHeader';
import WelcomeDocs from '../Components/WelcomeDocs'; import WelcomeDocs from '../Components/WelcomeDocs';
import AdvancedOptionsForm from '../Components/AdvancedOptionsForm'; import AdvancedOptionsForm from '../Components/AdvancedOptionsForm';
import { usePaymentConfig } from '../hooks/usePaymentConfig';
const StepWelcome = ( { setStep, currentStep } ) => { const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry } = CommonHooks.useWooSettings(); const { storeCountry } = CommonHooks.useWooSettings();
const { canUseCardPayments, canUseFastlane } = OnboardingHooks.useFlags();
const { acdcIcons, bcdcIcons } = usePaymentConfig(
storeCountry,
canUseCardPayments,
canUseFastlane
);
const onboardingHeaderDescription = canUseCardPayments
? __(
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
'woocommerce-paypal-payments'
)
: __(
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, and more.',
'woocommerce-paypal-payments'
);
return ( return (
<div className="ppcp-r-page-welcome"> <div className="ppcp-r-page-welcome">
@ -20,17 +38,16 @@ const StepWelcome = ( { setStep, currentStep } ) => {
'Welcome to PayPal Payments', 'Welcome to PayPal Payments',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
description={ __( description={ onboardingHeaderDescription }
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
'woocommerce-paypal-payments'
) }
/> />
<div className="ppcp-r-inner-container"> <div className="ppcp-r-inner-container">
<WelcomeFeatures /> <WelcomeFeatures />
<PaymentMethodIcons icons="all" /> <PaymentMethodIcons
icons={ canUseCardPayments ? acdcIcons : bcdcIcons }
/>
<p className="ppcp-r-button__description"> <p className="ppcp-r-button__description">
{ __( { __(
`Click the button below to be guided through connecting your existing PayPal account or creating a new one.You will be able to choose the payment options that are right for your store.`, 'Click the button below to be guided through connecting your existing PayPal account or creating a new one. You will be able to choose the payment options that are right for your store.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
) } ) }
</p> </p>
@ -49,9 +66,8 @@ const StepWelcome = ( { setStep, currentStep } ) => {
</div> </div>
<Separator className="ppcp-r-page-welcome-mode-separator" /> <Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs <WelcomeDocs
useAcdc={ true } useAcdc={ canUseCardPayments }
isFastlane={ true } isFastlane={ canUseFastlane }
isPayLater={ true }
storeCountry={ storeCountry } storeCountry={ storeCountry }
/> />
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } /> <Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />

View file

@ -36,8 +36,7 @@ const ALL_STEPS = [
id: 'methods', id: 'methods',
title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ), title: __( 'Choose checkout options', 'woocommerce-paypal-payments' ),
StepComponent: StepPaymentMethods, StepComponent: StepPaymentMethods,
canProceed: ( { methods } ) => canProceed: ( { methods } ) => methods.optionalMethods !== null,
methods.areOptionalPaymentMethodsEnabled !== null,
}, },
{ {
id: 'complete', id: 'complete',
@ -60,8 +59,8 @@ export const getSteps = ( flags ) => {
const steps = filterSteps( ALL_STEPS, [ const steps = filterSteps( ALL_STEPS, [
// Casual selling: Unlock the "Personal Account" choice. // Casual selling: Unlock the "Personal Account" choice.
( step ) => flags.canUseCasualSelling || step.id !== 'business', ( step ) => flags.canUseCasualSelling || step.id !== 'business',
// Card payments: Unlocks the "Extended Checkout" choice. // Skip payment methods screen.
( step ) => flags.canUseCardPayments || step.id !== 'methods', ( step ) => ! flags.shouldSkipPaymentMethods || step.id !== 'methods',
] ); ] );
const totalStepsCount = steps.length; const totalStepsCount = steps.length;

View file

@ -28,6 +28,7 @@ const defaultConfig = {
// Extended: Items on right side for ACDC-flow. // Extended: Items on right side for ACDC-flow.
extendedMethods: [ extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'DigitalWallets', Component: DigitalWallets }, { name: 'DigitalWallets', Component: DigitalWallets },
{ name: 'APMs', Component: AlternativePaymentMethods }, { name: 'APMs', Component: AlternativePaymentMethods },
], ],
@ -41,6 +42,26 @@ const defaultConfig = {
'with additional application', 'with additional application',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
// PayPal Checkout description.
paypalCheckoutDescription: __(
'Our all-in-one checkout solution lets you offer PayPal, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
),
// Icon groups.
bcdcIcons: [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ],
acdcIcons: [
'paypal',
'visa',
'mastercard',
'amex',
'discover',
'apple-pay',
'google-pay',
'ideal',
'bancontact',
],
}; };
const countrySpecificConfigs = { const countrySpecificConfigs = {
@ -60,11 +81,35 @@ const countrySpecificConfigs = {
{ name: 'APMs', Component: AlternativePaymentMethods }, { name: 'APMs', Component: AlternativePaymentMethods },
{ name: 'Fastlane', Component: Fastlane }, { name: 'Fastlane', Component: Fastlane },
], ],
paypalCheckoutDescription: __(
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
'woocommerce-paypal-payments'
),
optionalTitle: __( 'Expanded Checkout', 'woocommerce-paypal-payments' ), optionalTitle: __( 'Expanded Checkout', 'woocommerce-paypal-payments' ),
optionalDescription: __( optionalDescription: __(
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods', 'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
bcdcIcons: [
'paypal',
'venmo',
'visa',
'mastercard',
'amex',
'discover',
],
acdcIcons: [
'paypal',
'venmo',
'visa',
'mastercard',
'amex',
'discover',
'apple-pay',
'google-pay',
'ideal',
'bancontact',
],
}, },
GB: { GB: {
includedMethods: [ includedMethods: [
@ -72,6 +117,12 @@ const countrySpecificConfigs = {
{ name: 'PayInThree', Component: PayInThree }, { name: 'PayInThree', Component: PayInThree },
], ],
}, },
MX: {
extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'APMs', Component: AlternativePaymentMethods },
],
},
}; };
const filterMethods = ( methods, conditions ) => { const filterMethods = ( methods, conditions ) => {
@ -80,22 +131,12 @@ const filterMethods = ( methods, conditions ) => {
); );
}; };
export const usePaymentConfig = ( export const usePaymentConfig = ( country, useAcdc, isFastlane ) => {
country,
isPayLater,
useAcdc,
isFastlane
) => {
return useMemo( () => { return useMemo( () => {
const countryConfig = countrySpecificConfigs[ country ] || {}; const countryConfig = countrySpecificConfigs[ country ] || {};
const config = { ...defaultConfig, ...countryConfig }; const config = { ...defaultConfig, ...countryConfig };
const learnMoreConfig = learnMoreLinks[ country ] || {}; const learnMoreConfig = learnMoreLinks[ country ] || {};
// Filter the "left side" list. PayLater is conditional.
const includedMethods = filterMethods( config.includedMethods, [
( method ) => isPayLater || method.name !== 'PayLater',
] );
// Determine the "right side" items: Either BCDC or ACDC items. // Determine the "right side" items: Either BCDC or ACDC items.
const optionalMethods = useAcdc const optionalMethods = useAcdc
? config.extendedMethods ? config.extendedMethods
@ -108,9 +149,10 @@ export const usePaymentConfig = (
return { return {
...config, ...config,
includedMethods,
optionalMethods: availableOptionalMethods, optionalMethods: availableOptionalMethods,
learnMoreConfig, learnMoreConfig,
acdcIcons: config.acdcIcons,
bcdcIcons: config.bcdcIcons,
}; };
}, [ country, isPayLater, useAcdc, isFastlane ] ); }, [ country, useAcdc, isFastlane ] );
}; };

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { PayLaterMessagingHooks } from '../../../data'; import { PayLaterMessagingHooks } from '../../../data';
import { useEffect } from '@wordpress/element';
const TabPayLaterMessaging = () => { const TabPayLaterMessaging = () => {
const { const {

View file

@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import TopNavigation from '../../../ReusableComponents/TopNavigation'; import TopNavigation from '../../../ReusableComponents/TopNavigation';
import { useSaveSettings } from '../../../../hooks/useSaveSettings'; import { useStoreManager } from '../../../../hooks/useStoreManager';
import { CommonHooks } from '../../../../data'; import { CommonHooks } from '../../../../data';
import TabBar from '../../../ReusableComponents/TabBar'; import TabBar from '../../../ReusableComponents/TabBar';
import classNames from 'classnames'; import classNames from 'classnames';
@ -20,7 +20,7 @@ const SettingsNavigation = ( {
activePanel, activePanel,
setActivePanel, setActivePanel,
} ) => { } ) => {
const { persistAll } = useSaveSettings(); const { persistAll } = useStoreManager();
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );

View file

@ -0,0 +1,36 @@
import { __ } from '@wordpress/i18n';
import { Button, Icon } from '@wordpress/components';
import { reusableBlock } from '@wordpress/icons';
const FeatureDescription = ( { refreshHandler, isRefreshing } ) => {
const buttonLabel = isRefreshing
? __( 'Refreshing…', 'woocommerce-paypal-payments' )
: __( 'Refresh', 'woocommerce-paypal-payments' );
return (
<>
<p>
{ __(
'Enable additional features and capabilities on your WooCommerce store.',
'woocommerce-paypal-payments'
) }
</p>
<p>
{ __(
'Click Refresh to update your current features after making changes.',
'woocommerce-paypal-payments'
) }
</p>
<Button
variant="tertiary"
onClick={ refreshHandler }
disabled={ isRefreshing }
>
<Icon icon={ reusableBlock } size={ 18 } />
{ buttonLabel }
</Button>
</>
);
};
export default FeatureDescription;

View file

@ -0,0 +1,74 @@
import { __ } from '@wordpress/i18n';
import { FeatureSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import { Content } from '../../../../../ReusableComponents/Elements';
import { TITLE_BADGE_POSITIVE } from '../../../../../ReusableComponents/TitleBadge';
import { selectTab, TAB_IDS } from '../../../../../../utils/tabSelector';
import { useDispatch } from '@wordpress/data';
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
const FeatureItem = ( {
isBusy,
isSandbox,
title,
description,
buttons,
enabled,
notes,
} ) => {
const { setActiveModal } = useDispatch( COMMON_STORE_NAME );
const getButtonUrl = ( button ) => {
if ( button.urls ) {
return isSandbox ? button.urls.sandbox : button.urls.live;
}
return button.url;
};
const visibleButtons = buttons.filter(
( button ) =>
! button.showWhen || // Learn more buttons
( enabled && button.showWhen === 'enabled' ) ||
( ! enabled && button.showWhen === 'disabled' )
);
const handleClick = async ( feature ) => {
if ( feature.action?.type === 'tab' ) {
const highlight = Boolean( feature.action?.highlight );
const tabId = TAB_IDS[ feature.action.tab.toUpperCase() ];
await selectTab( tabId, feature.action.section, highlight );
}
if ( feature.action?.modal ) {
setActiveModal( feature.action.modal );
}
};
const actionProps = {
isBusy,
enabled,
notes,
buttons: visibleButtons.map( ( button ) => ( {
...button,
url: getButtonUrl( button ),
onClick: () => handleClick( button ),
} ) ),
};
if ( enabled ) {
actionProps.badge = {
text: __( 'Active', 'woocommerce-paypal-payments' ),
type: TITLE_BADGE_POSITIVE,
};
}
return (
<Content>
<FeatureSettingsBlock
title={ title }
description={ description }
actionProps={ actionProps }
/>
</Content>
);
};
export default FeatureItem;

View file

@ -0,0 +1,95 @@
import { __, sprintf } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import FeatureItem from './FeatureItem';
import FeatureDescription from './FeatureDescription';
import { ContentWrapper } from '../../../../../ReusableComponents/Elements';
import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
import { useMerchantInfo } from '../../../../../../data/common/hooks';
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
import {
NOTIFICATION_ERROR,
NOTIFICATION_SUCCESS,
} from '../../../../../ReusableComponents/Icons';
import { useFeatures } from '../../../../../../data/features/hooks';
const Features = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo();
const { features, fetchFeatures } = useFeatures();
const { refreshFeatureStatuses } = useDispatch( COMMON_STORE_NAME );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
if ( ! features || features.length === 0 ) {
return null;
}
const refreshHandler = async () => {
setIsRefreshing( true );
try {
const statusResult = await refreshFeatureStatuses();
if ( ! statusResult?.success ) {
throw new Error(
statusResult?.message || 'Failed to refresh status'
);
}
const featuresResult = await fetchFeatures();
if ( featuresResult.success ) {
createSuccessNotice(
__(
'Features refreshed successfully.',
'woocommerce-paypal-payments'
),
{ icon: NOTIFICATION_SUCCESS }
);
} else {
throw new Error(
featuresResult?.message || 'Failed to fetch features'
);
}
} catch ( error ) {
createErrorNotice(
sprintf(
/* translators: %s: error message */
__( 'Operation failed: %s', 'woocommerce-paypal-payments' ),
error.message ||
__( 'Unknown error', 'woocommerce-paypal-payments' )
),
{ icon: NOTIFICATION_ERROR }
);
} finally {
setIsRefreshing( false );
}
};
return (
<SettingsCard
className="ppcp-r-tab-overview-features"
title={ __( 'Features', 'woocommerce-paypal-payments' ) }
description={
<FeatureDescription
refreshHandler={ refreshHandler }
isRefreshing={ isRefreshing }
/>
}
contentContainer={ false }
>
<ContentWrapper>
{ features.map( ( { id, enabled, ...feature } ) => (
<FeatureItem
key={ id }
isBusy={ isRefreshing }
isSandbox={ merchant.isSandbox }
enabled={ enabled }
{ ...feature }
/>
) ) }
</ContentWrapper>
</SettingsCard>
);
};
export default Features;

View file

@ -0,0 +1,72 @@
import { __ } from '@wordpress/i18n';
import { FeatureSettingsBlock } from '../../../../../ReusableComponents/SettingsBlocks';
import {
Content,
ContentWrapper,
} from '../../../../../ReusableComponents/Elements';
import SettingsCard from '../../../../../ReusableComponents/SettingsCard';
const Help = () => {
return (
<SettingsCard
className="ppcp-r-tab-overview-help"
title={ __( 'Help Center', 'woocommerce-paypal-payments' ) }
description={ __(
'Access detailed guides and responsive support to streamline setup and enhance your experience.',
'woocommerce-paypal-payments'
) }
contentContainer={ false }
>
<ContentWrapper>
<Content>
<FeatureSettingsBlock
title={ __(
'Documentation',
'woocommerce-paypal-payments'
) }
description={ __(
'Find detailed guides and resources to help you set up, manage, and optimize your PayPal integration.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttons: [
{
type: 'tertiary',
text: __(
'View full documentation',
'woocommerce-paypal-payments'
),
url: 'https://woocommerce.com/document/woocommerce-paypal-payments/',
},
],
} }
/>
</Content>
<Content>
<FeatureSettingsBlock
title={ __( 'Support', 'woocommerce-paypal-payments' ) }
description={ __(
'Need help? Access troubleshooting tips or contact our support team for personalized assistance.',
'woocommerce-paypal-payments'
) }
actionProps={ {
buttons: [
{
type: 'tertiary',
text: __(
'View support options',
'woocommerce-paypal-payments'
),
url: 'https://woocommerce.com/document/woocommerce-paypal-payments/#get-help ',
},
],
} }
/>
</Content>
</ContentWrapper>
</SettingsCard>
);
};
export default Help;

Some files were not shown because too many files have changed in this diff Show more