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"
AUTHORIZATION="Bearer ABC123"

View file

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

View file

@ -94,6 +94,45 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group =
return 0;
}
/**
* Retrieves the number of times a filter has been applied during the current request.
*
* @since 6.1.0
*
* @global int[] $wp_filters Stores the number of times each filter was triggered.
*
* @param string $hook_name The name of the filter hook.
* @return int The number of times the filter hook has been applied.
*/
function did_filter( $hook_name ) {
return 0;
}
/**
* Returns whether or not a filter hook is currently being processed.
*
* The function current_filter() only returns the most recent filter being executed.
* did_filter() returns the number of times a filter has been applied during
* the current request.
*
* This function allows detection for any filter currently being executed
* (regardless of whether it's the most recent filter to fire, in the case of
* hooks called from hook callbacks) to be verified.
*
* @since 3.9.0
*
* @see current_filter()
* @see did_filter()
* @global string[] $wp_current_filter Current filter.
*
* @param string|null $hook_name Optional. Filter hook to check. Defaults to null,
* which checks if any filter is currently being run.
* @return bool Whether the filter is currently in the stack.
*/
function doing_filter( $hook_name = null ) {
return false;
}
/**
* HTML API: WP_HTML_Tag_Processor class
*/

View file

@ -1,5 +1,21 @@
*** Changelog ***
= 3.0.0 - 2025-03-17 =
* Enhancement - Redesigned settings UI for new users #2908
* Enhancement - Enable Fastlane by default on new store setups when eligible #3199
* Enhancement - Enable support for advanced card payments and features for Hong Kong & Singapore #3089
* Fix - Dependency conflict with more recent psr/log versions on PHP8+ #2993
* Fix - PayPal Checkout Gateway subscription migration layer not renewing subscriptions #2699
* Fix - Fatal error when gateway settings initialized too early by third-party plugin #2766
* Fix - Next Payment date for Subscriptions not updating when processing a PayPal Subscriptions renewal order #2959
* Fix - Changing the subscription payment method to ACDC triggers error #2891
* Fix - Standard Card button not appearing in standalone gateway for free trial subscription products #2935
* Fix - Validation error when using Trustly payment method #3031
* Fix - Error in continuation mode due to wrong gateway selection on Checkout block #2996
* Fix - Error in error in PayLaterConfigurator #2989
* Tweak - Removed currency requirement for Vault v3 #2919
* Tweak - Update plugin author from WooCommerce to PayPal
= 2.9.6 - 2025-01-06 =
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872

View file

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

View file

@ -91,9 +91,12 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )();
}
$show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' );
$preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' );
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
$show_new_ux || $preview_new_ux
) ) {
$modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
}

View file

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

View file

@ -160,7 +160,7 @@ class PartnersEndpoint {
$this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY );
$status = $this->seller_status_factory->from_paypal_reponse( $json );
$status = $this->seller_status_factory->from_paypal_response( $json );
return $status;
}
}

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
@ -18,43 +18,21 @@ class PartnerReferralsData {
/**
* The DCC Applies Helper object.
*
* @deprecated Deprecates with the new UI. In this class, the products are
* always explicit, and should not be deducted from the
* DccApplies state at this point.
* Remove this with the legacy UI code.
* @var DccApplies
*/
private $dcc_applies;
/**
* The list of products ('PPCP', 'EXPRESS_CHECKOUT').
*
* @var string[]
*/
private $products;
private DccApplies $dcc_applies;
/**
* PartnerReferralsData constructor.
*
* @param DccApplies $dcc_applies The DCC Applies helper.
*/
public function __construct(
DccApplies $dcc_applies
) {
public function __construct( DccApplies $dcc_applies ) {
$this->dcc_applies = $dcc_applies;
$this->products = array(
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
);
}
/**
* Returns a new copy of this object with the given value set.
*
* @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT').
* @return static
*/
public function with_products( array $products ): self {
$obj = clone $this;
$obj->products = $products;
return $obj;
}
/**
@ -62,40 +40,81 @@ class PartnerReferralsData {
*
* @return string
*/
public function nonce(): string {
public function nonce() : string {
return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG';
}
/**
* Returns the data.
*
* @param string[] $products The list of products to use ('PPCP', 'EXPRESS_CHECKOUT').
* Default is based on DCC availability.
* @param string $onboarding_token A security token to finalize the onboarding process.
* @param bool $use_subscriptions If the merchant requires subscription features.
* @param bool $use_card_payments If the merchant wants to process credit card payments.
* @return array
*/
public function data(): array {
public function data( array $products = array(), string $onboarding_token = '', bool $use_subscriptions = null, bool $use_card_payments = true ) : array {
if ( ! $products ) {
$products = array(
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
);
}
/**
* Returns the partners referrals data.
* Filter the return-URL, which is called at the end of the OAuth onboarding
* process, when the merchant clicks the "Return to your shop" button.
*/
return apply_filters(
'ppcp_partner_referrals_data',
array(
'partner_config_override' => array(
/**
* Returns the URL which will be opened at the end of onboarding.
*/
'return_url' => apply_filters(
$return_url = apply_filters(
'woocommerce_paypal_payments_partner_config_override_return_url',
admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' )
),
);
/**
* Returns the description of the URL which will be opened at the end of onboarding.
* Filter the label of the "Return to your shop" button.
* It's displayed on the very last page of the onboarding popup.
*/
'return_url_description' => apply_filters(
$return_url_label = apply_filters(
'woocommerce_paypal_payments_partner_config_override_return_url_description',
__( 'Return to your shop.', 'woocommerce-paypal-payments' )
);
$capabilities = array();
$first_party_features = array(
'PAYMENT',
'REFUND',
'ADVANCED_TRANSACTIONS_SEARCH',
'TRACKING_SHIPMENT_READWRITE',
);
if ( true === $use_subscriptions ) {
$capabilities[] = 'PAYPAL_WALLET_VAULTING_ADVANCED';
$first_party_features[] = 'BILLING_AGREEMENT';
}
// Backwards compatibility. Keep those features in the legacy UI (null-value).
// Move this into the previous condition, once legacy code is removed.
if ( false !== $use_subscriptions ) {
$first_party_features[] = 'FUTURE_PAYMENT';
$first_party_features[] = 'VAULT';
}
if ( false === $use_subscriptions ) {
// Only use "ADVANCED_VAULTING" product for onboarding with subscriptions.
$products = array_filter(
$products,
static fn( $product ) => $product !== 'ADVANCED_VAULTING'
);
}
$payload = array(
'partner_config_override' => array(
'return_url' => $return_url,
'return_url_description' => $return_url_label,
'show_add_credit_card' => $use_card_payments,
),
'show_add_credit_card' => true,
),
'products' => $this->products,
'products' => $products,
'capabilities' => $capabilities,
'legal_consents' => array(
array(
'type' => 'SHARE_DATA_CONSENT',
@ -110,34 +129,31 @@ class PartnerReferralsData {
'integration_method' => 'PAYPAL',
'integration_type' => 'FIRST_PARTY',
'first_party_details' => array(
'features' => array(
'PAYMENT',
'FUTURE_PAYMENT',
'REFUND',
'ADVANCED_TRANSACTIONS_SEARCH',
'VAULT',
'TRACKING_SHIPMENT_READWRITE',
),
'features' => $first_party_features,
'seller_nonce' => $this->nonce(),
),
),
),
),
),
)
);
}
/**
* Append the validation token to the return_url
*
* @param array $data The referral data.
* @param string $token The token to be appended.
* @return array
* Filter the final partners referrals data collection.
*/
public function append_onboarding_token( array $data, string $token ): array {
$data['partner_config_override']['return_url'] =
add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] );
return $data;
$payload = apply_filters( 'ppcp_partner_referrals_data', $payload );
// An empty array is not permitted.
if ( isset( $payload['capabilities'] ) && ! $payload['capabilities'] ) {
unset( $payload['capabilities'] );
}
// Add the nonce in the end, to maintain backwards compatibility of filters.
$payload['partner_config_override']['return_url'] = add_query_arg(
array( 'ppcpToken' => $onboarding_token ),
$payload['partner_config_override']['return_url']
);
return $payload;
}
}

View file

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

View file

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

View file

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

View file

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

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 ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else {

View file

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

View file

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

View file

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

View file

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

View file

@ -475,7 +475,7 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
* @return void
*/
private function add_feature_detection_tag( bool $axo_enabled ) {
$show_tag = is_checkout() || is_cart() || is_shop();
$show_tag = is_home() || is_checkout() || is_cart() || is_shop();
if ( ! $show_tag ) {
return;

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;
/**
* The block cart displays express checkout buttons. Those buttons are handled by the
* PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
*
* A possible bug in WooCommerce does not use the correct payment method ID for the express
* payment buttons inside the cart, but sends the ID of the _first_ active payment method.
*
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
*/
const enforcePaymentMethodForCart = () => {
// Do nothing, unless we're handling block cart express payment buttons.
if ( 'cart-block' !== config.scriptData.context ) {
return;
}
// Set the active payment method to PAYPAL_GATEWAY_ID.
wp.data
.dispatch( 'wc/store/payment' )
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
};
useEffect( () => {
// fill the form if in continuation (for product or mini-cart buttons)
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
@ -339,7 +318,6 @@ export const PayPalComponent = ( {
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
@ -439,7 +417,6 @@ export const PayPalComponent = ( {
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose
@ -476,7 +453,6 @@ export const PayPalComponent = ( {
shouldskipFinalConfirmation,
getCheckoutRedirectUrl,
setGotoContinuationOnError,
enforcePaymentMethodForCart,
onSubmit,
onError,
onClose

View file

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

View file

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

View file

@ -28,13 +28,6 @@ return array(
);
},
'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod {
/**
* Cart instance; might be null, esp. in customizer or in Block Editor.
*
* @var null|WC_Cart $cart
*/
$cart = WC()->cart;
return new PayPalPaymentMethod(
$container->get( 'blocks.url' ),
$container->get( 'ppcp.asset-version' ),
@ -53,7 +46,6 @@ return array(
$container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'wcgateway.place-order-button-description' ),
$container->get( 'wcgateway.all-funding-sources' ),
$cart && $cart->needs_shipping()
);
},
'blocks.advanced-card-method' => static function( ContainerInterface $container ): AdvancedCardPaymentMethod {

View file

@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
*/
class UpdateShippingEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-update-shipping';
const WC_STORE_API_ENDPOINT = '/wp-json/wc/store/cart/';
const WC_STORE_API_ENDPOINT = '/wp-json/wc/store/v1/cart/';
/**
* The Request Data Helper.

View file

@ -130,13 +130,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/
private $all_funding_sources;
/**
* Whether shipping details must be collected during checkout; i.e. paying for physical goods?
*
* @var bool
*/
private $need_shipping;
/**
* Assets constructor.
*
@ -155,7 +148,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* @param string $place_order_button_text The text for the standard "Place order" button.
* @param string $place_order_button_description The text for additional "Place order" description.
* @param array $all_funding_sources All existing funding sources for PayPal buttons.
* @param bool $need_shipping Whether shipping details are required for the purchase.
*/
public function __construct(
string $module_url,
@ -172,8 +164,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
bool $use_place_order,
string $place_order_button_text,
string $place_order_button_description,
array $all_funding_sources,
bool $need_shipping
array $all_funding_sources
) {
$this->name = PayPalGateway::ID;
$this->module_url = $module_url;
@ -191,7 +182,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
$this->place_order_button_text = $place_order_button_text;
$this->place_order_button_description = $place_order_button_description;
$this->all_funding_sources = $all_funding_sources;
$this->need_shipping = $need_shipping;
}
/**
@ -258,6 +248,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
&& $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ?? 'block-checkout' );
$place_order_enabled = ( $this->use_place_order || $this->add_place_order_method )
&& ! $this->subscription_helper->cart_contains_subscription();
$cart = WC()->cart;
return array(
'id' => $this->gateway->id,
@ -284,7 +275,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
),
),
'scriptData' => $script_data,
'needShipping' => $this->need_shipping,
'needShipping' => $cart && $cart->needs_shipping(),
);
}

View file

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

View file

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

View file

@ -272,8 +272,9 @@ class ApproveOrderEndpoint implements EndpointInterface {
* @return void
*/
protected function toggle_final_review_enabled_setting(): void {
// TODO new-ux: This flag must also be updated in the new settings.
$final_review_enabled_setting = $this->settings->has( 'blocks_final_review_enabled' ) && $this->settings->get( 'blocks_final_review_enabled' );
$final_review_enabled_setting ? $this->settings->set( 'blocks_final_review_enabled', false ) : $this->settings->set( 'blocks_final_review_enabled', true );
$this->settings->set( 'blocks_final_review_enabled', ! $final_review_enabled_setting );
$this->settings->persist();
}
}

View file

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

View file

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

View file

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

View file

@ -10,9 +10,13 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat;
use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets;
use WooCommerce\PayPalCommerce\Compat\Settings\GeneralSettingsMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\PaymentMethodSettingsMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMap;
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsTabMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\StylingSettingsMapHelper;
use WooCommerce\PayPalCommerce\Compat\Settings\SubscriptionSettingsMapHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
@ -133,30 +137,26 @@ return array(
$styling_settings_map_helper = $container->get( 'compat.settings.styling_map_helper' );
assert( $styling_settings_map_helper instanceof StylingSettingsMapHelper );
$settings_tab_map_helper = $container->get( 'compat.settings.settings_tab_map_helper' );
assert( $settings_tab_map_helper instanceof SettingsTabMapHelper );
$subscription_map_helper = $container->get( 'compat.settings.subscription_map_helper' );
assert( $subscription_map_helper instanceof SubscriptionSettingsMapHelper );
$general_map_helper = $container->get( 'compat.settings.general_map_helper' );
assert( $general_map_helper instanceof GeneralSettingsMapHelper );
$payment_methods_map_helper = $container->get( 'compat.settings.payment_methods_map_helper' );
assert( $payment_methods_map_helper instanceof PaymentMethodSettingsMapHelper );
return array(
new SettingsMap(
$container->get( 'settings.data.general' ),
/**
* The new GeneralSettings class stores the current connection
* details, without adding an environment-suffix (no `_sandbox`
* or `_production` in the field name)
* Only the `sandbox_merchant` flag indicates, which environment
* the credentials are used for.
*/
array(
'merchant_id' => 'merchant_id',
'client_id' => 'client_id',
'client_secret' => 'client_secret',
'sandbox_on' => 'sandbox_merchant',
'live_client_id' => 'client_id',
'live_client_secret' => 'client_secret',
'live_merchant_id' => 'merchant_id',
'live_merchant_email' => 'merchant_email',
'sandbox_client_id' => 'client_id',
'sandbox_client_secret' => 'client_secret',
'sandbox_merchant_id' => 'merchant_id',
'sandbox_merchant_email' => 'merchant_email',
)
$general_map_helper->map()
),
new SettingsMap(
$container->get( 'settings.data.settings' ),
$settings_tab_map_helper->map()
),
new SettingsMap(
$container->get( 'settings.data.styling' ),
@ -172,15 +172,50 @@ return array(
*/
$styling_settings_map_helper->map()
),
new SettingsMap(
$container->get( 'settings.data.settings' ),
$subscription_map_helper->map()
),
/**
* We need to pass the PaymentSettings model instance to use it in some helpers.
* Once the new settings module is permanently enabled,
* this model can be passed as a dependency to the appropriate helper classes.
* For now, we must pass it this way to avoid errors when the new settings module is disabled.
*/
new SettingsMap(
$container->get( 'settings.data.payment' ),
array()
),
new SettingsMap(
$container->get( 'settings.data.payment' ),
$payment_methods_map_helper->map()
),
);
},
'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper {
return new SettingsMapHelper(
$container->get( 'compat.setting.new-to-old-map' ),
$container->get( 'compat.settings.styling_map_helper' )
$container->get( 'compat.settings.styling_map_helper' ),
$container->get( 'compat.settings.settings_tab_map_helper' ),
$container->get( 'compat.settings.subscription_map_helper' ),
$container->get( 'compat.settings.general_map_helper' ),
$container->get( 'compat.settings.payment_methods_map_helper' ),
$container->get( 'wcgateway.settings.admin-settings-enabled' )
);
},
'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper {
return new StylingSettingsMapHelper();
},
'compat.settings.settings_tab_map_helper' => static function() : SettingsTabMapHelper {
return new SettingsTabMapHelper();
},
'compat.settings.subscription_map_helper' => static function( ContainerInterface $container ) : SubscriptionSettingsMapHelper {
return new SubscriptionSettingsMapHelper( $container->get( 'wc-subscriptions.helper' ) );
},
'compat.settings.general_map_helper' => static function() : GeneralSettingsMapHelper {
return new GeneralSettingsMapHelper();
},
'compat.settings.payment_methods_map_helper' => static function() : PaymentMethodSettingsMapHelper {
return new PaymentMethodSettingsMapHelper();
},
);

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;
use RuntimeException;
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
/**
@ -49,17 +53,70 @@ class SettingsMapHelper {
*/
protected StylingSettingsMapHelper $styling_settings_map_helper;
/**
* A helper for mapping the old/new settings tab settings.
*
* @var SettingsTabMapHelper
*/
protected SettingsTabMapHelper $settings_tab_map_helper;
/**
* A helper for mapping old and new subscription settings.
*
* @var SubscriptionSettingsMapHelper
*/
protected SubscriptionSettingsMapHelper $subscription_map_helper;
/**
* A helper for mapping old and new general settings.
*
* @var GeneralSettingsMapHelper
*/
protected GeneralSettingsMapHelper $general_settings_map_helper;
/**
* A helper for mapping old and new payment method settings.
*
* @var PaymentMethodSettingsMapHelper
*/
protected PaymentMethodSettingsMapHelper $payment_method_settings_map_helper;
/**
* Whether the new settings module is enabled.
*
* @var bool
*/
protected bool $new_settings_module_enabled;
/**
* Constructor.
*
* @param SettingsMap[] $settings_map A list of settings maps containing key definitions.
* @param StylingSettingsMapHelper $styling_settings_map_helper A helper for mapping the old/new styling settings.
* @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings.
* @param SubscriptionSettingsMapHelper $subscription_map_helper A helper for mapping old and new subscription settings.
* @param GeneralSettingsMapHelper $general_settings_map_helper A helper for mapping old and new general settings.
* @param PaymentMethodSettingsMapHelper $payment_method_settings_map_helper A helper for mapping old and new payment method settings.
* @param bool $new_settings_module_enabled Whether the new settings module is enabled.
* @throws RuntimeException When an old key has multiple mappings.
*/
public function __construct( array $settings_map, StylingSettingsMapHelper $styling_settings_map_helper ) {
public function __construct(
array $settings_map,
StylingSettingsMapHelper $styling_settings_map_helper,
SettingsTabMapHelper $settings_tab_map_helper,
SubscriptionSettingsMapHelper $subscription_map_helper,
GeneralSettingsMapHelper $general_settings_map_helper,
PaymentMethodSettingsMapHelper $payment_method_settings_map_helper,
bool $new_settings_module_enabled
) {
$this->validate_settings_map( $settings_map );
$this->settings_map = $settings_map;
$this->styling_settings_map_helper = $styling_settings_map_helper;
$this->settings_tab_map_helper = $settings_tab_map_helper;
$this->subscription_map_helper = $subscription_map_helper;
$this->general_settings_map_helper = $general_settings_map_helper;
$this->payment_method_settings_map_helper = $payment_method_settings_map_helper;
$this->new_settings_module_enabled = $new_settings_module_enabled;
}
/**
@ -89,6 +146,10 @@ class SettingsMapHelper {
* @return mixed|null The value of the mapped setting, or null if not found.
*/
public function mapped_value( string $old_key ) {
if ( ! $this->new_settings_module_enabled ) {
return null;
}
$this->ensure_map_initialized();
if ( ! isset( $this->key_to_model[ $old_key ] ) ) {
return null;
@ -112,6 +173,10 @@ class SettingsMapHelper {
* @return bool True if the key exists in the new settings, false otherwise.
*/
public function has_mapped_key( string $old_key ) : bool {
if ( ! $this->new_settings_module_enabled ) {
return false;
}
$this->ensure_map_initialized();
return isset( $this->key_to_model[ $old_key ] );
@ -134,7 +199,23 @@ class SettingsMapHelper {
switch ( true ) {
case $model instanceof StylingSettings:
return $this->styling_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
return $this->styling_settings_map_helper->mapped_value(
$old_key,
$this->model_cache[ $model_id ],
$this->get_payment_settings_model()
);
case $model instanceof GeneralSettings:
return $this->general_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
case $model instanceof SettingsModel:
return $old_key === 'subscriptions_mode'
? $this->subscription_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] )
: $this->settings_tab_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
case $model instanceof PaymentSettings:
return $this->payment_method_settings_map_helper->mapped_value( $old_key );
default:
return $this->model_cache[ $model_id ][ $new_key ] ?? null;
}
@ -173,4 +254,23 @@ class SettingsMapHelper {
}
}
}
/**
* Retrieves the PaymentSettings model instance.
*
* Once the new settings module is permanently enabled,
* this model can be passed as a dependency to the appropriate helper classes.
* For now, we must pass it this way to avoid errors when the new settings module is disabled.
*
* @return AbstractDataModel|null
*/
protected function get_payment_settings_model() : ?AbstractDataModel {
foreach ( $this->settings_map as $settings_map_instance ) {
if ( $settings_map_instance->get_model() instanceof PaymentSettings ) {
return $settings_map_instance->get_model();
}
}
return null;
}
}

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;
use RuntimeException;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO;
/**
@ -23,6 +27,8 @@ class StylingSettingsMapHelper {
use ContextTrait;
protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID );
/**
* Maps old setting keys to new setting style names.
*
@ -45,6 +51,8 @@ class StylingSettingsMapHelper {
'disable_funding' => '',
'googlepay_button_enabled' => '',
'applepay_button_enabled' => '',
'smart_button_enable_styling_per_location' => '',
'pay_later_button_enabled' => '',
);
foreach ( $this->locations_map() as $old_location_name => $new_location_name ) {
@ -62,25 +70,32 @@ class StylingSettingsMapHelper {
*
* @param string $old_key The key from the legacy settings.
* @param LocationStylingDTO[] $styling_models The list of location styling models.
* @param AbstractDataModel|null $payment_settings The payment settings model.
*
* @return mixed The value of the mapped setting, (null if not found).
*/
public function mapped_value( string $old_key, array $styling_models ) {
public function mapped_value( string $old_key, array $styling_models, ?AbstractDataModel $payment_settings ) {
switch ( $old_key ) {
case 'smart_button_locations':
return $this->mapped_smart_button_locations_value( $styling_models );
case 'smart_button_enable_styling_per_location':
return true;
case 'pay_later_button_locations':
return $this->mapped_pay_later_button_locations_value( $styling_models );
case 'disable_funding':
return $this->mapped_disabled_funding_value( $styling_models );
return $this->mapped_disabled_funding_value( $styling_models, $payment_settings );
case 'googlepay_button_enabled':
return $this->mapped_google_pay_or_apple_pay_enabled_value( $styling_models, 'googlepay' );
return $this->mapped_button_enabled_value( $styling_models, GooglePayGateway::ID );
case 'applepay_button_enabled':
return $this->mapped_google_pay_or_apple_pay_enabled_value( $styling_models, 'applepay' );
return $this->mapped_button_enabled_value( $styling_models, ApplePayGateway::ID );
case 'pay_later_button_enabled':
return $this->mapped_pay_later_button_enabled_value( $styling_models, $payment_settings );
default:
foreach ( $this->locations_map() as $old_location_name => $new_location_name ) {
@ -198,7 +213,7 @@ class StylingSettingsMapHelper {
$enabled_locations = array();
$locations = array_flip( $this->locations_map() );
foreach ( $styling_models as $model ) {
if ( ! $model->enabled || ! in_array( 'paylater', $model->methods, true ) ) {
if ( ! $model->enabled || ! in_array( 'pay-later', $model->methods, true ) ) {
continue;
}
@ -215,51 +230,115 @@ class StylingSettingsMapHelper {
* Retrieves the mapped disabled funding value from the new settings.
*
* @param LocationStylingDTO[] $styling_models The list of location styling models.
* @param AbstractDataModel|null $payment_settings The payment settings model.
* @return array|null The list of disabled funding, or null if none are disabled.
*/
protected function mapped_disabled_funding_value( array $styling_models ): ?array {
$disabled_funding = array();
$locations_to_context_map = $this->current_context_to_new_button_location_map();
foreach ( $styling_models as $model ) {
if ( $model->location !== $locations_to_context_map[ $this->context() ] || in_array( 'venmo', $model->methods, true ) ) {
continue;
protected function mapped_disabled_funding_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?array {
if ( is_null( $payment_settings ) ) {
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 or disabled PayLater button value from the new settings.
*
* @param LocationStylingDTO[] $styling_models The list of location styling models.
* @param AbstractDataModel|null $payment_settings The payment settings model.
* @return int|null The enabled (1) or disabled (0) state or null if it should fall back to old settings value.
*/
protected function mapped_pay_later_button_enabled_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?int {
if ( ! $payment_settings instanceof PaymentSettings ) {
return null;
}
/**
* Retrieves the mapped enabled/disabled Google Pay or Apple Pay value from the new settings.
*
* @param LocationStylingDTO[] $styling_models The list of location styling models.
* @param 'googlepay'|'applepay' $button_name The button name ('googlepay' or 'applepay').
* @return int The enabled (1) or disabled (0) state.
* @throws RuntimeException If an invalid button name is provided.
*/
protected function mapped_google_pay_or_apple_pay_enabled_value( array $styling_models, string $button_name ): ?int {
if ( $button_name !== 'googlepay' && $button_name !== 'applepay' ) {
throw new RuntimeException( 'Wrong button name is provided. Either "googlepay" or "applepay" can be used' );
}
$locations_to_context_map = $this->current_context_to_new_button_location_map();
$current_context = $locations_to_context_map[ $this->context() ] ?? '';
foreach ( $styling_models as $model ) {
if ( ! $model->enabled
|| $model->location !== $locations_to_context_map[ $this->context() ]
|| ! in_array( $button_name, $model->methods, true )
) {
continue;
}
if ( $model->enabled && $model->location === $current_context ) {
if ( in_array( 'pay-later', $model->methods, true ) && $payment_settings->get_paylater_enabled() ) {
return 1;
}
}
}
return 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,8 +81,8 @@ const GooglePayComponent = ( { isEditing, buttonAttributes } ) => {
};
const features = [ 'products' ];
registerExpressPaymentMethod( {
if ( buttonConfig?.is_enabled ) {
registerExpressPaymentMethod( {
name: buttonData.id,
title: `PayPal - ${ buttonData.title }`,
description: __(
@ -99,4 +99,5 @@ registerExpressPaymentMethod( {
features,
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\AvailabilityNotice;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
@ -106,6 +105,7 @@ return array(
'FR', // France
'DE', // Germany
'GR', // Greece
'HK', // Hong Kong
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
@ -119,6 +119,7 @@ return array(
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SG', // Singapore
'SK', // Slovakia
'SI', // Slovenia
'ES', // Spain
@ -148,6 +149,7 @@ return array(
'CZK', // Czech Koruna
'DKK', // Danish Krone
'EUR', // Euro
'HKD', // Hong Kong Dollar
'GBP', // British Pound Sterling
'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel
@ -157,6 +159,7 @@ return array(
'NZD', // New Zealand Dollar
'PHP', // Philippine Peso
'PLN', // Polish Zloty
'SGD', // Singapur-Dollar
'SEK', // Swedish Krona
'THB', // Thai Baht
'TWD', // New Taiwan Dollar
@ -174,7 +177,7 @@ return array(
$container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.environment' ),
$container->get( 'settings.environment' ),
$container->get( 'wcgateway.settings.status' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
@ -221,15 +224,15 @@ return array(
},
'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
$is_connected = $container->get( 'settings.flag.is-connected' );
if ( ! $is_connected ) {
return '';
}
$product_status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $product_status instanceof ApmProductStatus );
$environment = $container->get( 'onboarding.environment' );
$environment = $container->get( 'settings.environment' );
assert( $environment instanceof Environment );
$enabled = $product_status->is_active();

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 ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else {

View file

@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods;
use WC_Order;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@ -43,8 +42,31 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
public function run( ContainerInterface $c ) : bool {
add_action( 'after_setup_theme', fn() => $this->run_with_translations( $c ) );
return true;
}
/**
* Set up WP hooks that depend on translation features.
* Runs after the theme setup, when translations are available, which is fired
* before the `init` hook, which usually contains most of the logic.
*
* @param ContainerInterface $c The DI container.
* @return void
*/
private function run_with_translations( ContainerInterface $c ) : void {
// When Local APMs are disabled, none of the following hooks are needed.
if ( ! $this->should_add_local_apm_gateways( $c ) ) {
return;
}
/**
* The "woocommerce_payment_gateways" filter is responsible for ADDING
* custom payment gateways to WooCommerce. Here, we add all the local
* APM gateways to the filtered list, so they become available later on.
*/
add_filter(
'woocommerce_payment_gateways',
/**
@ -53,14 +75,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return $methods;
}
$onboarding_state = $c->get( 'onboarding.state' );
if ( $onboarding_state->current_state() === State::STATE_START ) {
return $methods;
}
if ( ! is_array( $methods ) ) {
return $methods;
}
@ -74,6 +88,10 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
}
);
/**
* Filters the "available gateways" list by REMOVING gateways that
* are not available for the current customer.
*/
add_filter(
'woocommerce_available_payment_gateways',
/**
@ -82,42 +100,38 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return $methods;
}
if ( ! is_array( $methods ) ) {
return $methods;
}
if ( ! is_admin() ) {
if ( ! isset( WC()->customer ) ) {
if ( ! is_array( $methods ) || is_admin() || empty( WC()->customer ) ) {
// Don't restrict the gateway list on wp-admin or when no customer is known.
return $methods;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
$customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country();
$site_currency = get_woocommerce_currency();
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
// Remove unsupported gateways from the customer's payment options.
foreach ( $payment_methods as $payment_method ) {
if (
! in_array( $customer_country, $payment_method['countries'], true )
|| ! in_array( $site_currency, $payment_method['currencies'], true )
) {
$is_currency_supported = in_array( $site_currency, $payment_method['currencies'], true );
$is_country_supported = in_array( $customer_country, $payment_method['countries'], true );
if ( ! $is_currency_supported || ! $is_country_supported ) {
unset( $methods[ $payment_method['id'] ] );
}
}
}
return $methods;
}
);
/**
* Adds all local APM gateways in the "payment_method_type" block registry
* to make the payment methods available in the Block Checkout.
*
* @see IntegrationRegistry::initialize
*/
add_action(
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $key => $value ) {
$payment_method_registry->register( $c->get( 'ppcp-local-apms.' . $key . '.payment-method' ) );
@ -128,9 +142,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function ( array $data ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return $data;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
$default_disable_funding = $data['url_params']['disable-funding'] ?? '';
@ -149,9 +160,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType
*/
function( $order_id ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return;
@ -184,9 +192,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
add_action(
'woocommerce_paypal_payments_payment_capture_completed_webhook_handler',
function( WC_Order $wc_order, string $order_id ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
if (
! $this->is_local_apm( $wc_order->get_payment_method(), $payment_methods )
@ -202,8 +207,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
10,
2
);
return true;
}
/**
@ -229,12 +232,42 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @param ContainerInterface $container Container.
* @return bool
*/
private function should_add_local_apm_gateways( ContainerInterface $container ): bool {
private function should_add_local_apm_gateways( ContainerInterface $container ) : bool {
// APMs are only available after merchant onboarding is completed.
$is_connected = $container->get( 'settings.flag.is-connected' );
if ( ! $is_connected ) {
/**
* When the merchant is _not_ connected yet, we still need to
* register the APM gateways in one case:
*
* During the authentication process (which happens via a REST call)
* the gateways need to be present, so they can be correctly
* pre-configured for new merchants.
*/
return $this->is_rest_request();
}
// The general plugin functionality must be enabled.
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
return $settings->has( 'enabled' )
&& $settings->get( 'enabled' ) === true
&& $settings->has( 'allow_local_apm_gateways' )
if ( ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' ) ) {
return false;
}
// Register APM gateways, when the relevant setting is active.
return $settings->has( 'allow_local_apm_gateways' )
&& $settings->get( 'allow_local_apm_gateways' ) === true;
}
/**
* Checks, whether the current request is trying to access a WooCommerce REST endpoint.
*
* @return bool True, if the request path matches the WC-Rest namespace.
*/
private function is_rest_request(): bool {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' );
return str_contains( $request_uri, '/wp-json/wc/' );
}
}

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 ) {
$this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED );
} else {

View file

@ -346,8 +346,10 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' );
sandboxSwitchElement?.addEventListener( 'click', () => {
document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' );
});
document
.querySelector( '.woocommerce-save-button' )
?.removeAttribute( 'disabled' );
} );
const validate = () => {
const selectors = sandboxSwitchElement.checked
@ -389,7 +391,8 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
const isSandboxInBackend =
PayPalCommerceGatewayOnboarding.current_env === 'sandbox';
if ( sandboxSwitchElement.checked !== isSandboxInBackend ) {
if ( sandboxSwitchElement?.checked !== isSandboxInBackend ) {
sandboxSwitchElement.checked = isSandboxInBackend;
}

View file

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

View file

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

View file

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

View file

@ -95,6 +95,7 @@ class SaveConfig {
* @param array $config The configurator config.
*/
public function save_config( array $config ): void {
// TODO new-ux: We should convert this to a new AbstractDataModel class in the settings folder!
$this->settings->set( 'pay_later_enable_styling_per_messaging_location', true );
$this->settings->set( 'pay_later_messaging_enabled', true );

View file

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

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

View file

@ -63,6 +63,10 @@
&:hover {
transform: rotate(45deg);
}
&.components-button {
height: auto;
}
}
.ppcp--method-icon {
@ -79,6 +83,234 @@
justify-content: space-between;
align-items: center;
margin-top: auto;
min-height: 24px;
}
.ppcp--method-toggle-wrapper {
display: flex;
align-items: center;
}
}
}
.ppcp-r-payment-methods {
.ppcp-highlight {
animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid $color-blueberry;
border-radius: var(--container-border-radius);
position: relative;
z-index: 1;
}
@keyframes ppcp-highlight-fade {
0%, 20% {
background-color: rgba($color-blueberry, 0.08);
border-color: $color-blueberry;
border-width: 1px;
}
100% {
background-color: transparent;
border-color: $color-gray-300;
border-width: 1px;
}
}
}
// Disabled state styling.
.ppcp--method-item--disabled {
position: relative;
// Apply grayscale and disable interactions.
.ppcp--method-inner {
opacity: 0.7;
filter: grayscale(1);
pointer-events: none;
transition: filter 0.2s ease;
}
// Override text colors.
.ppcp--method-title {
color: $color-gray-700 !important;
}
.ppcp--method-description p {
color: $color-gray-500 !important;
}
.ppcp--method-disabled-message {
opacity: 0;
transform: translateY(-5px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
// Style all buttons and toggle controls.
.components-button,
.components-form-toggle {
opacity: 0.5;
}
// Hover state - only blur the inner content.
&:hover {
.ppcp--method-inner {
filter: blur(2px) grayscale(1);
}
.ppcp--method-disabled-message {
opacity: 1;
transform: translateY(0);
}
}
}
// Disabled overlay.
.ppcp--method-disabled-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba($color-white, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 8;
border-radius: var(--container-border-radius);
pointer-events: auto;
opacity: 0;
transition: opacity 0.2s ease;
}
.ppcp--method-item--disabled:hover .ppcp--method-disabled-overlay {
opacity: 1;
}
.ppcp--method-disabled-message {
padding: 14px 18px;
text-align: center;
@include font(13, 20, 500);
color: $color-text-tertiary;
position: relative;
z-index: 9;
border: none;
a {
text-decoration: none;
}
}
/* Warning message */
.ppcp--method-warning {
position: relative;
display: inline-flex;
cursor: help;
svg {
fill: currentColor;
color: $color-warning;
}
/* Add invisible bridge to prevent gap between icon and popover */
&:before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 15px;
background-color: transparent;
}
// Popover bubble
.ppcp--method-warning-message {
position: absolute;
bottom: calc(100% + 15px);
display: flex;
flex-direction: column;
gap: 10px;
left: 50%;
transform: translateX(-50%);
width: 250px;
padding: 16px;
background-color: $color-white;
border: 1px solid $color-gray-200;
border-radius: 4px;
z-index: 9;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s;
pointer-events: none;
&:after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -6px;
width: 12px;
height: 12px;
background: $color-white;
border-right: 1px solid $color-gray-200;
border-bottom: 1px solid $color-gray-200;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.01);
transform: rotate(45deg);
margin-top: -6px;
}
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.ppcp--method-notice-list {
margin-bottom: 0;
}
.highlight {
font-weight: 700;
background: inherit;
color: inherit;
}
code {
font-size: 12px;
}
ul {
list-style: inside;
}
}
&:hover .ppcp--method-warning-message,
& .ppcp--method-warning-message:hover {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
}
// For RTL support
html[dir="rtl"] .ppcp--method-warning {
&:before {
left: auto;
right: 50%;
transform: translateX(50%);
}
.ppcp--method-warning-message {
left: auto;
right: 50%;
transform: translateX(50%);
&:after {
left: auto;
right: 50%;
margin-right: -6px;
margin-left: 0;
}
}
}

View file

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

View file

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

View file

@ -69,4 +69,34 @@ $width_gap: 24px;
color: var(--color-text-teriary);
margin: 0;
}
+ .ppcp-r-settings-card {
margin-top: $card-vertical-gap;
padding-top: $card-vertical-gap;
border-top: 1px solid $color-gray-200;
}
.ppcp--card-actions {
transition: opacity 0.3s;
&.ppcp--dimmed {
opacity: 0.5;
&:hover {
opacity: 1;
}
}
.components-button.is-tertiary {
transition: color 0.3s, background 0.3s;
&:first-child {
padding-left: 0;
}
svg {
margin-right: 4px;
}
}
}
}

View file

@ -13,14 +13,3 @@
padding-bottom: 36px;
}
}
.ppcp-r-settings {
> * {
margin-bottom: $card-vertical-gap;
}
> *:not(:last-child) {
padding-bottom: $card-vertical-gap;
border-bottom: 1px solid $color-gray-200;
}
}

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-payment-methods {
display: flex;
flex-direction: column;
gap: 48px;
}
.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);
.ppcp-r-settings {
.ppcp-highlight {
position: relative;
z-index: 1;
}
@keyframes ppcp-highlight-fade {
0%, 20% {
&::before {
content: '';
position: absolute;
top: -8px;
left: -12px;
right: -12px;
bottom: -8px;
border: 1px solid $color-blueberry;
border-radius: 4px;
z-index: -1;
pointer-events: none;
animation: ppcp-setting-highlight-bg 2s cubic-bezier(0.4, 0, 0.2, 1);
animation-fill-mode: forwards;
}
&::after {
content: '';
position: absolute;
top: -8px;
left: -12px;
width: 4px;
bottom: -8px;
background-color: $color-blueberry;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
z-index: -1;
pointer-events: none;
animation: ppcp-setting-highlight-accent 2s cubic-bezier(0.4, 0, 0.2, 1);
animation-fill-mode: forwards;
}
}
@keyframes ppcp-setting-highlight-bg {
0%, 15% {
background-color: rgba($color-blueberry, 0.08);
border-color: $color-blueberry;
border-width: 1px;
}
70% {
background-color: transparent;
border-color: transparent;
}
100% {
background-color: transparent;
border-color: $color-gray-300;
border-width: 1px;
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 {
width: 100%;
max-width: 100%;
padding: 48px 0px 48px 48px;
padding: 16px 0px 16px 16px;
#configurator-controlPanelContainer.css-5urmrq.e1vy3g880 {
width: 374px;
@ -25,6 +25,7 @@
.css-7xkxom, .css-8tvj6u {
height: auto;
width: 1.2rem;
}
.css-10nkerk.ej6n7t60 {
@ -37,14 +38,19 @@
}
.css-1vc34jy-handler {
height: 1.6rem;
width: 1.6rem;
height: 1.7em;
width: 1.5rem;
}
.css-8vwtr6-state {
height: 1.6rem;
height: 1.4rem;
width: 3rem;
}
}
.css-1s8clkf.etu8a6w2 {
width: 374px;
}
}
&__subheader, #configurator-controlPanelSubHeader {
@ -68,6 +74,7 @@
.css-rok10q, .css-dfgbdq-text_body_strong {
margin-top: 0;
margin-bottom: 0;
}
&__publish-button {
@ -110,4 +117,30 @@
width: 100%;
}
}
.css-n4cwz8 {
margin-top: 20px;
}
.css-1ce6bcu-container {
width: 3rem;
height: 1.8rem;
}
#configurator-previewSectionSubHeaderText {
margin-right: 10px;
}
.css-zcyvrz.ej6n7t60 {
margin-bottom: 5px;
.css-3xbhoy-svg-size_md-icon {
width: 1.5rem;
height: 1.5rem;
}
.css-7i5kpm-icon-button_base-size_xl-size_sm-secondary {
padding: 0.5rem;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,7 @@
const Action = ( { children } ) => (
<div className="ppcp--action">{ children }</div>
const Action = ( { id, children } ) => (
<div className="ppcp--action" { ...( id ? { id } : {} ) }>
{ children }
</div>
);
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 CardActions } from './CardActions';
export { default as Content } from './Content';
export { default as ContentWrapper } from './ContentWrapper';
export { default as Description } from './Description';

View file

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

View file

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

View file

@ -1,20 +1,11 @@
import PaymentMethodIcon from './PaymentMethodIcon';
const PaymentMethodIcons = ( props ) => {
return (
const PaymentMethodIcons = ( { icons = [] } ) => (
<div className="ppcp-r-payment-method-icons">
<PaymentMethodIcon type="paypal" icons={ props.icons } />
<PaymentMethodIcon type="venmo" icons={ props.icons } />
<PaymentMethodIcon type="visa" icons={ props.icons } />
<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 } />
{ icons.map( ( type ) => (
<PaymentMethodIcon key={ type } type={ type } />
) ) }
</div>
);
};
);
export default PaymentMethodIcons;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ const PricingDescription = () => {
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 label = sprintf(

View file

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

View file

@ -22,9 +22,34 @@ const StepBusiness = ( {} ) => {
);
useEffect( () => {
if ( ! businessChoice ) {
return;
}
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === businessChoice );
}, [ 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 (
<div className="ppcp-r-page-business">
<OnboardingHeader
@ -45,23 +70,13 @@ const StepBusiness = ( {} ) => {
);
};
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.',
const DetailsAccountType = () => (
<p>
{ __(
'* Business account is required for subscriptions.',
'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'
),
},
];
) }
</p>
);
export default StepBusiness;

View file

@ -59,33 +59,18 @@ const StepPaymentMethods = () => {
export default StepPaymentMethods;
const PaymentStepTitle = () => {
const { storeCountry } = CommonHooks.useWooSettings();
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'
);
return __( 'Add Credit and Debit Cards', 'woocommerce-paypal-payments' );
};
const OptionalMethodDescription = () => {
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
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 (
<PaymentFlow
onlyOptional={ true }
useAcdc={ ! isCasualSeller }
useAcdc={ ! isCasualSeller && canUseCardPayments }
isFastlane={ true }
isPayLater={ true }
storeCountry={ storeCountry }

View file

@ -1,4 +1,4 @@
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState } from '@wordpress/element';
import { OptionSelector } from '../../../ReusableComponents/Fields';
@ -10,26 +10,28 @@ const StepProducts = () => {
const { canUseSubscriptions } = OnboardingHooks.useFlags();
const [ optionState, setOptionState ] = useState( null );
const [ productChoices, setProductChoices ] = useState( [] );
const { isCasualSeller } = OnboardingHooks.useBusiness();
useEffect( () => {
const initChoices = () => {
if ( optionState === canUseSubscriptions ) {
return;
}
let choices = productChoicesFull;
// Remove subscription details, if not available.
if ( ! canUseSubscriptions ) {
choices = choices.filter(
( { value } ) => value !== PRODUCT_TYPES.SUBSCRIPTIONS
);
setProducts(
products.filter(
( value ) => value !== PRODUCT_TYPES.SUBSCRIPTIONS
)
);
const choices = productChoicesFull.map( ( choice ) => {
if (
choice.value === PRODUCT_TYPES.SUBSCRIPTIONS &&
! canUseSubscriptions
) {
return {
...choice,
isDisabled: true,
contents: (
<DetailsSubscriptions
showLink={ true }
showNotice={ isCasualSeller }
/>
),
};
}
return choice;
} );
setProductChoices( choices );
setOptionState( canUseSubscriptions );
@ -48,7 +50,46 @@ const StepProducts = () => {
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 (
<div className="ppcp-r-page-products">
<OnboardingHeader
@ -87,41 +128,29 @@ const DetailsPhysical = () => (
</ul>
);
const DetailsSubscriptions = () => (
<a
target="__blank"
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq"
>
{ __( 'WooCommerce Subscriptions', 'woocommerce-paypal-payments' ) }
</a>
const DetailsSubscriptions = ( { showLink, showNotice } ) => (
<>
{ showLink && (
<p
dangerouslySetInnerHTML={ {
__html: sprintf(
/* 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 { Separator } from '../../../ReusableComponents/Elements';
import Accordion from '../../../ReusableComponents/AccordionSection';
import { CommonHooks } from '../../../../data';
import { CommonHooks, OnboardingHooks } from '../../../../data';
import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
import OnboardingHeader from '../Components/OnboardingHeader';
import WelcomeDocs from '../Components/WelcomeDocs';
import AdvancedOptionsForm from '../Components/AdvancedOptionsForm';
import { usePaymentConfig } from '../hooks/usePaymentConfig';
const StepWelcome = ( { setStep, currentStep } ) => {
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 (
<div className="ppcp-r-page-welcome">
@ -20,17 +38,16 @@ const StepWelcome = ( { setStep, currentStep } ) => {
'Welcome to PayPal Payments',
'woocommerce-paypal-payments'
) }
description={ __(
'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'
) }
description={ onboardingHeaderDescription }
/>
<div className="ppcp-r-inner-container">
<WelcomeFeatures />
<PaymentMethodIcons icons="all" />
<PaymentMethodIcons
icons={ canUseCardPayments ? acdcIcons : bcdcIcons }
/>
<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'
) }
</p>
@ -49,9 +66,8 @@ const StepWelcome = ( { setStep, currentStep } ) => {
</div>
<Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs
useAcdc={ true }
isFastlane={ true }
isPayLater={ true }
useAcdc={ canUseCardPayments }
isFastlane={ canUseFastlane }
storeCountry={ storeCountry }
/>
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />

View file

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

View file

@ -28,6 +28,7 @@ const defaultConfig = {
// Extended: Items on right side for ACDC-flow.
extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'DigitalWallets', Component: DigitalWallets },
{ name: 'APMs', Component: AlternativePaymentMethods },
],
@ -41,6 +42,26 @@ const defaultConfig = {
'with additional application',
'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 = {
@ -60,11 +81,35 @@ const countrySpecificConfigs = {
{ name: 'APMs', Component: AlternativePaymentMethods },
{ 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' ),
optionalDescription: __(
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
'woocommerce-paypal-payments'
),
bcdcIcons: [
'paypal',
'venmo',
'visa',
'mastercard',
'amex',
'discover',
],
acdcIcons: [
'paypal',
'venmo',
'visa',
'mastercard',
'amex',
'discover',
'apple-pay',
'google-pay',
'ideal',
'bancontact',
],
},
GB: {
includedMethods: [
@ -72,6 +117,12 @@ const countrySpecificConfigs = {
{ name: 'PayInThree', Component: PayInThree },
],
},
MX: {
extendedMethods: [
{ name: 'CardFields', Component: CardFields },
{ name: 'APMs', Component: AlternativePaymentMethods },
],
},
};
const filterMethods = ( methods, conditions ) => {
@ -80,22 +131,12 @@ const filterMethods = ( methods, conditions ) => {
);
};
export const usePaymentConfig = (
country,
isPayLater,
useAcdc,
isFastlane
) => {
export const usePaymentConfig = ( country, useAcdc, isFastlane ) => {
return useMemo( () => {
const countryConfig = countrySpecificConfigs[ country ] || {};
const config = { ...defaultConfig, ...countryConfig };
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.
const optionalMethods = useAcdc
? config.extendedMethods
@ -108,9 +149,10 @@ export const usePaymentConfig = (
return {
...config,
includedMethods,
optionalMethods: availableOptionalMethods,
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 { useEffect } from '@wordpress/element';
const TabPayLaterMessaging = () => {
const {

View file

@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import TopNavigation from '../../../ReusableComponents/TopNavigation';
import { useSaveSettings } from '../../../../hooks/useSaveSettings';
import { useStoreManager } from '../../../../hooks/useStoreManager';
import { CommonHooks } from '../../../../data';
import TabBar from '../../../ReusableComponents/TabBar';
import classNames from 'classnames';
@ -20,7 +20,7 @@ const SettingsNavigation = ( {
activePanel,
setActivePanel,
} ) => {
const { persistAll } = useSaveSettings();
const { persistAll } = useStoreManager();
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