Merge branch 'trunk' into PCP-4210-features-refactor-to-use-rest-endpoints

# Conflicts:
#	modules/ppcp-settings/services.php
This commit is contained in:
carmenmaymo 2025-02-24 11:43:05 +01:00
commit 68fe134ed0
No known key found for this signature in database
GPG key ID: 6023F686B0F3102E
32 changed files with 1054 additions and 231 deletions

View file

@ -670,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,
@ -687,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,
@ -735,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,
@ -764,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

@ -190,6 +190,7 @@ return array(
'FR', // France
'DE', // Germany
'GR', // Greece
'HK', // Hong Kong
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
@ -203,6 +204,7 @@ return array(
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SG', // Singapore
'SK', // Slovakia
'SI', // Slovenia
'ES', // Spain
@ -232,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
@ -241,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

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,10 +10,12 @@ 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\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(
@ -137,30 +139,16 @@ return array(
$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 );
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' ),
@ -180,13 +168,19 @@ return array(
*/
$styling_settings_map_helper->map()
),
new SettingsMap(
$container->get( 'settings.data.settings' ),
$subscription_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.settings_tab_map_helper' )
$container->get( 'compat.settings.settings_tab_map_helper' ),
$container->get( 'compat.settings.subscription_map_helper' ),
$container->get( 'compat.settings.general_map_helper' )
);
},
'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper {
@ -195,4 +189,10 @@ return array(
'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();
},
);

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

@ -10,6 +10,7 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Compat\Settings;
use RuntimeException;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
@ -57,23 +58,43 @@ class SettingsMapHelper {
*/
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;
/**
* 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 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.
* @throws RuntimeException When an old key has multiple mappings.
*/
public function __construct(
array $settings_map,
StylingSettingsMapHelper $styling_settings_map_helper,
SettingsTabMapHelper $settings_tab_map_helper
SettingsTabMapHelper $settings_tab_map_helper,
SubscriptionSettingsMapHelper $subscription_map_helper,
GeneralSettingsMapHelper $general_settings_map_helper
) {
$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;
}
/**
@ -150,8 +171,13 @@ class SettingsMapHelper {
case $model instanceof StylingSettings:
return $this->styling_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
case $model instanceof GeneralSettings:
return $this->general_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
case $model instanceof SettingsModel:
return $this->settings_tab_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
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 ] );
default:
return $this->model_cache[ $model_id ][ $new_key ] ?? null;

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

@ -105,6 +105,7 @@ return array(
'FR', // France
'DE', // Germany
'GR', // Greece
'HK', // Hong Kong
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
@ -118,6 +119,7 @@ return array(
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SG', // Singapore
'SK', // Slovakia
'SI', // Slovenia
'ES', // Spain
@ -147,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
@ -156,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

View file

@ -43,7 +43,16 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
// When Local APMs are disabled, none of the following hooks are needed.
if ( ! $this->should_add_local_apm_gateways( $c ) ) {
return true;
}
/**
* 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',
/**
@ -52,15 +61,6 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return $methods;
}
$is_connected = $c->get( 'settings.flag.is-connected' );
if ( ! $is_connected ) {
return $methods;
}
if ( ! is_array( $methods ) ) {
return $methods;
}
@ -74,6 +74,10 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
}
);
/**
* Filters the "available gateways" list by REMOVING gateways that
* are not available for the current customer.
*/
add_filter(
'woocommerce_available_payment_gateways',
/**
@ -82,29 +86,22 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
* @psalm-suppress MissingClosureParamType
*/
function ( $methods ) use ( $c ) {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return $methods;
}
if ( ! is_array( $methods ) ) {
if ( ! is_array( $methods ) || is_admin() || empty( WC()->customer ) ) {
// Don't restrict the gateway list on wp-admin or when no customer is known.
return $methods;
}
if ( ! is_admin() ) {
if ( ! isset( WC()->customer ) ) {
return $methods;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
$customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country();
$site_currency = get_woocommerce_currency();
$customer_country = WC()->customer->get_billing_country() ?: WC()->customer->get_shipping_country();
$site_currency = get_woocommerce_currency();
// Remove unsupported gateways from the customer's payment options.
foreach ( $payment_methods as $payment_method ) {
$is_currency_supported = in_array( $site_currency, $payment_method['currencies'], true );
$is_country_supported = in_array( $customer_country, $payment_method['countries'], true );
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $payment_method ) {
if (
! in_array( $customer_country, $payment_method['countries'], true )
|| ! in_array( $site_currency, $payment_method['currencies'], true )
) {
unset( $methods[ $payment_method['id'] ] );
}
if ( ! $is_currency_supported || ! $is_country_supported ) {
unset( $methods[ $payment_method['id'] ] );
}
}
@ -112,12 +109,15 @@ class LocalAlternativePaymentMethodsModule implements ServiceModule, ExtendingMo
}
);
/**
* Adds all local APM gateways in the "payment_method_type" block registry
* to make the payment methods available in the Block Checkout.
*
* @see IntegrationRegistry::initialize
*/
add_action(
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
if ( ! self::should_add_local_apm_gateways( $c ) ) {
return;
}
$payment_methods = $c->get( 'ppcp-local-apms.payment-methods' );
foreach ( $payment_methods as $key => $value ) {
$payment_method_registry->register( $c->get( 'ppcp-local-apms.' . $key . '.payment-method' ) );
@ -128,9 +128,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 +146,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 +178,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 )
@ -229,12 +220,35 @@ 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 {
// Merchant onboarding must be 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.
*
* TODO is there a cleaner solution for this?
*/
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' );
return str_contains( $request_uri, '/wp-json/wc/' );
}
// 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;
}
}

View file

@ -82,3 +82,85 @@
}
}
}
// 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: 50;
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: 51;
border: none;
a {
text-decoration: none;
}
}

View file

@ -11,6 +11,8 @@ const PaymentMethodItemBlock = ( {
onTriggerModal,
onSelect,
isSelected,
isDisabled,
disabledMessage,
} ) => {
const { activeHighlight, setActiveHighlight } = useActiveHighlight();
const isHighlighted = activeHighlight === paymentMethod.id;
@ -31,9 +33,16 @@ const PaymentMethodItemBlock = ( {
id={ paymentMethod.id }
className={ `ppcp--method-item ${
isHighlighted ? 'ppcp-highlight' : ''
}` }
} ${ isDisabled ? 'ppcp--method-item--disabled' : '' }` }
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 && (

View file

@ -19,12 +19,14 @@ 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 )
.filter( ( m ) => m && m.id )
.map( ( paymentMethod ) => (
<PaymentMethodItemBlock
key={ paymentMethod.id }
paymentMethod={ paymentMethod }
isSelected={ paymentMethod.enabled }
isDisabled={ paymentMethod.isDisabled }
disabledMessage={ paymentMethod.disabledMessage }
onSelect={ ( checked ) =>
handleSelect( paymentMethod.id, checked )
}

View file

@ -0,0 +1,39 @@
import { createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight';
/**
* Component to display a payment method dependency message
*
* @param {Object} props - Component props
* @param {string} props.parentId - ID of the parent payment method
* @param {string} props.parentName - Display name of the parent payment method
* @return {JSX.Element} The formatted message with link
*/
const DependencyMessage = ( { parentId, parentName } ) => {
// Using WordPress createInterpolateElement with proper React elements
return createInterpolateElement(
/* translators: %s: payment method name */
__(
'This payment method requires <methodLink /> to be enabled.',
'woocommerce-paypal-payments'
),
{
methodLink: (
<strong>
<a
href="#"
onClick={ ( e ) => {
e.preventDefault();
scrollAndHighlight( parentId );
} }
>
{ parentName }
</a>
</strong>
),
}
);
};
export default DependencyMessage;

View file

@ -0,0 +1,71 @@
import SettingsCard from '../../../../ReusableComponents/SettingsCard';
import { PaymentMethodsBlock } from '../../../../ReusableComponents/SettingsBlocks';
import usePaymentDependencyState from '../../../../../hooks/usePaymentDependencyState';
import DependencyMessage from './DependencyMessage';
/**
* Renders a payment method card with dependency handling
*
* @param {Object} props - Component props
* @param {string} props.id - Unique identifier for the card
* @param {string} props.title - Title of the payment method card
* @param {string} props.description - Description of the payment method
* @param {string} props.icon - Icon path for the payment method
* @param {Array} props.methods - List of payment methods to display
* @param {Object} props.methodsMap - Map of all payment methods by ID
* @param {Function} props.onTriggerModal - Callback when a method is clicked
* @param {boolean} props.isDisabled - Whether the entire card is disabled
* @param {(string|JSX.Element)} props.disabledMessage - Message to show when disabled
* @return {JSX.Element} The rendered component
*/
const PaymentMethodCard = ( {
id,
title,
description,
icon,
methods,
methodsMap = {},
onTriggerModal,
isDisabled = false,
disabledMessage,
} ) => {
const dependencyState = usePaymentDependencyState( methods, methodsMap );
return (
<SettingsCard
id={ id }
title={ title }
description={ description }
icon={ icon }
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ methods.map( ( method ) => {
const dependency = dependencyState[ method.id ];
const dependencyMessage = dependency ? (
<DependencyMessage
parentId={ dependency.parentId }
parentName={ dependency.parentName }
/>
) : null;
return {
...method,
isDisabled:
method.isDisabled ||
isDisabled ||
Boolean( dependency?.isDisabled ),
disabledMessage:
method.disabledMessage ||
dependencyMessage ||
disabledMessage,
};
} ) }
onTriggerModal={ onTriggerModal }
/>
</SettingsCard>
);
};
export default PaymentMethodCard;

View file

@ -1,17 +1,23 @@
import { __ } from '@wordpress/i18n';
import { useCallback } from '@wordpress/element';
import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { PaymentMethodsBlock } from '../../../ReusableComponents/SettingsBlocks';
import { CommonHooks, OnboardingHooks, PaymentHooks } from '../../../../data';
import { useActiveModal } from '../../../../data/common/hooks';
import Modal from '../Components/Payment/Modal';
import PaymentMethodCard from '../Components/Payment/PaymentMethodCard';
const TabPaymentMethods = () => {
const methods = PaymentHooks.usePaymentMethods();
const { setPersistent, changePaymentSettings } = PaymentHooks.useStore();
const store = PaymentHooks.useStore();
const { setPersistent, changePaymentSettings } = store;
const { activeModal, setActiveModal } = useActiveModal();
// Get all methods as a map for dependency checking
const methodsMap = {};
methods.all.forEach( ( method ) => {
methodsMap[ method.id ] = method;
} );
const getActiveMethod = () => {
if ( ! activeModal ) {
return null;
@ -60,6 +66,7 @@ const TabPaymentMethods = () => {
icon="icon-checkout-standard.svg"
methods={ methods.paypal }
onTriggerModal={ setActiveModal }
methodsMap={ methodsMap }
/>
{ merchant.isBusinessSeller && canUseCardPayments && (
<PaymentMethodCard
@ -75,6 +82,7 @@ const TabPaymentMethods = () => {
icon="icon-checkout-online-methods.svg"
methods={ methods.cardPayment }
onTriggerModal={ setActiveModal }
methodsMap={ methodsMap }
/>
) }
<PaymentMethodCard
@ -90,6 +98,7 @@ const TabPaymentMethods = () => {
icon="icon-checkout-alternative-methods.svg"
methods={ methods.apm }
onTriggerModal={ setActiveModal }
methodsMap={ methodsMap }
/>
{ activeModal && (
@ -104,25 +113,3 @@ const TabPaymentMethods = () => {
};
export default TabPaymentMethods;
const PaymentMethodCard = ( {
id,
title,
description,
icon,
methods,
onTriggerModal,
} ) => (
<SettingsCard
id={ id }
title={ title }
description={ description }
icon={ icon }
contentContainer={ false }
>
<PaymentMethodsBlock
paymentMethods={ methods }
onTriggerModal={ onTriggerModal }
/>
</SettingsCard>
);

View file

@ -7,6 +7,8 @@
export default {
// Transient data.
SET_TRANSIENT: 'PAYMENT:SET_TRANSIENT',
SET_DISABLED_BY_DEPENDENCY: 'PAYMENT:SET_DISABLED_BY_DEPENDENCY',
RESTORE_DEPENDENCY_STATE: 'PAYMENT:RESTORE_DEPENDENCY_STATE',
// Persistent data.
SET_PERSISTENT: 'PAYMENT:SET_PERSISTENT',

View file

@ -7,6 +7,7 @@ import * as actions from './actions';
import * as hooks from './hooks';
import * as resolvers from './resolvers';
import { initTodoSync } from '../sync/todo-state-sync';
import { initPaymentDependencySync } from '../sync/payment-methods-sync';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -24,9 +25,12 @@ export const initStore = () => {
register( store );
// Initialize todo sync after store registration. Potentially should be moved elsewhere.
// Initialize todo sync after store registration.
initTodoSync();
// Initialize payment method dependency sync.
initPaymentDependencySync();
return Boolean( wp.data.select( STORE_NAME ) );
};

View file

@ -88,6 +88,56 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
changePersistent( state, payload.data ),
[ ACTION_TYPES.SET_DISABLED_BY_DEPENDENCY ]: ( state, payload ) => {
const { methodId } = payload;
const method = state.data[ methodId ];
if ( ! method ) {
return state;
}
// Create a new state with the method disabled due to dependency
const updatedData = {
...state.data,
[ methodId ]: {
...method,
enabled: false,
_disabledByDependency: true,
_originalState: method.enabled,
},
};
return {
...state,
data: updatedData,
};
},
[ ACTION_TYPES.RESTORE_DEPENDENCY_STATE ]: ( state, payload ) => {
const { methodId } = payload;
const method = state.data[ methodId ];
if ( ! method || ! method._disabledByDependency ) {
return state;
}
// Restore the method to its original state
const updatedData = {
...state.data,
[ methodId ]: {
...method,
enabled: method._originalState === true,
_disabledByDependency: false,
_originalState: undefined,
},
};
return {
...state,
data: updatedData,
};
},
} );
export default reducer;

View file

@ -0,0 +1,126 @@
import { subscribe, select } from '@wordpress/data';
// Store name
const PAYMENT_STORE = 'wc/paypal/payment';
// Track original states of dependent methods
const originalStates = {};
/**
* Initialize payment method dependency synchronization
*/
export const initPaymentDependencySync = () => {
let previousPaymentState = null;
let isProcessing = false;
const unsubscribe = subscribe( () => {
if ( isProcessing ) {
return;
}
isProcessing = true;
try {
const paymentHooks = select( PAYMENT_STORE );
if ( ! paymentHooks ) {
isProcessing = false;
return;
}
const methods = paymentHooks.persistentData();
if ( ! methods ) {
isProcessing = false;
return;
}
if ( ! previousPaymentState ) {
previousPaymentState = { ...methods };
isProcessing = false;
return;
}
const changedMethods = Object.keys( methods )
.filter(
( key ) =>
key !== '__meta' &&
methods[ key ] &&
previousPaymentState[ key ]
)
.filter(
( methodId ) =>
methods[ methodId ].enabled !==
previousPaymentState[ methodId ].enabled
);
if ( changedMethods.length > 0 ) {
changedMethods.forEach( ( changedId ) => {
const isNowEnabled = methods[ changedId ].enabled;
const dependents = Object.entries( methods )
.filter(
( [ key, method ] ) =>
key !== '__meta' &&
method &&
method.depends_on &&
method.depends_on.includes( changedId )
)
.map( ( [ key ] ) => key );
if ( dependents.length > 0 ) {
if ( ! isNowEnabled ) {
handleDisableDependents( dependents, methods );
} else {
handleRestoreDependents( dependents, methods );
}
}
} );
}
previousPaymentState = { ...methods };
} catch ( error ) {
// Keep error handling without the console.error
} finally {
isProcessing = false;
}
} );
return unsubscribe;
};
const handleDisableDependents = ( dependentIds, methods ) => {
dependentIds.forEach( ( methodId ) => {
if ( methods[ methodId ] ) {
if ( ! ( methodId in originalStates ) ) {
originalStates[ methodId ] = methods[ methodId ].enabled;
}
methods[ methodId ].enabled = false;
methods[ methodId ].isDisabled = true;
}
} );
};
const handleRestoreDependents = ( dependentIds, methods ) => {
dependentIds.forEach( ( methodId ) => {
if (
methods[ methodId ] &&
methodId in originalStates &&
checkAllDependenciesSatisfied( methodId, methods )
) {
methods[ methodId ].enabled = originalStates[ methodId ];
methods[ methodId ].isDisabled = false;
delete originalStates[ methodId ];
}
} );
};
const checkAllDependenciesSatisfied = ( methodId, methods ) => {
const method = methods[ methodId ];
if ( ! method || ! method.depends_on ) {
return true;
}
return ! method.depends_on.some( ( parentId ) => {
const parent = methods[ parentId ];
return ! parent || parent.enabled === false;
} );
};

View file

@ -0,0 +1,81 @@
import { useSelect } from '@wordpress/data';
/**
* Gets the display name for a parent payment method
*
* @param {string} parentId - ID of the parent payment method
* @param {Object} methodsMap - Map of all payment methods by ID
* @return {string} The display name to use for the parent method
*/
const getParentMethodName = ( parentId, methodsMap ) => {
const parentMethod = methodsMap[ parentId ];
return parentMethod
? parentMethod.itemTitle || parentMethod.title || ''
: '';
};
/**
* Finds disabled parent dependencies for a method
*
* @param {Object} method - The payment method to check
* @param {Object} methodsMap - Map of all payment methods by ID
* @return {Array} List of disabled parent IDs, empty if none
*/
const findDisabledParents = ( method, methodsMap ) => {
if ( ! method.depends_on?.length && ! method._disabledByDependency ) {
return [];
}
const parents = method.depends_on || [];
return parents.filter( ( parentId ) => {
const parent = methodsMap[ parentId ];
return parent && ! parent.enabled;
} );
};
/**
* Custom hook to handle payment method dependencies
*
* @param {Array} methods - List of payment methods
* @param {Object} methodsMap - Map of payment methods by ID
* @return {Object} Dependency state object with methods that should be disabled
*/
const usePaymentDependencyState = ( methods, methodsMap ) => {
return useSelect(
( select ) => {
const paymentStore = select( 'wc/paypal/payment' );
if ( ! paymentStore ) {
return {};
}
const result = {};
methods.forEach( ( method ) => {
const disabledParents = findDisabledParents(
method,
methodsMap
);
if ( disabledParents.length > 0 ) {
const parentId = disabledParents[ 0 ];
const parentName = getParentMethodName(
parentId,
methodsMap
);
result[ method.id ] = {
isDisabled: true,
parentId,
parentName,
};
}
} );
return result;
},
[ methods, methodsMap ]
);
};
export default usePaymentDependencyState;

View file

@ -0,0 +1,49 @@
/**
* Scroll to a specific element and highlight it
*
* @param {string} elementId - ID of the element to scroll to
* @param {boolean} [highlight=true] - Whether to highlight the element
* @return {Promise} - Resolves when scroll and highlight are complete
*/
export const scrollAndHighlight = ( elementId, highlight = true ) => {
return new Promise( ( resolve ) => {
const scrollTarget = document.getElementById( elementId );
if ( scrollTarget ) {
const navContainer = document.querySelector(
'.ppcp-r-navigation-container'
);
const navHeight = navContainer ? navContainer.offsetHeight : 0;
// Get the current scroll position and element's position relative to viewport
const rect = scrollTarget.getBoundingClientRect();
// Calculate the final position with offset
const scrollPosition =
rect.top + window.scrollY - ( navHeight + 55 );
window.scrollTo( {
top: scrollPosition,
behavior: 'smooth',
} );
// Add highlight if requested
if ( highlight ) {
scrollTarget.classList.add( 'ppcp-highlight' );
// Remove highlight after animation
setTimeout( () => {
scrollTarget.classList.remove( 'ppcp-highlight' );
}, 2000 );
}
// Resolve after scroll animation
setTimeout( resolve, 300 );
} else {
console.error(
`Failed to scroll: Element with ID "${ elementId }" not found`
);
resolve();
}
} );
};

View file

@ -7,6 +7,8 @@ export const TAB_IDS = {
PAY_LATER_MESSAGING: 'tab-panel-0-pay-later-messaging',
};
import { scrollAndHighlight } from './scrollAndHighlight';
/**
* Select a tab by simulating a click event and scroll to specified element,
* accounting for navigation container height
@ -23,40 +25,8 @@ export const selectTab = ( tabId, scrollToId ) => {
if ( tab ) {
tab.click();
setTimeout( () => {
const scrollTarget = scrollToId
? document.getElementById( scrollToId )
: document.getElementById( 'ppcp-settings-container' );
if ( scrollTarget ) {
const navContainer = document.querySelector(
'.ppcp-r-navigation-container'
);
const navHeight = navContainer
? navContainer.offsetHeight
: 0;
// Get the current scroll position and element's position relative to viewport
const rect = scrollTarget.getBoundingClientRect();
// Calculate the final position with offset
const scrollPosition =
rect.top + window.scrollY - ( navHeight + 55 );
window.scrollTo( {
top: scrollPosition,
behavior: 'smooth',
} );
// Resolve after scroll animation
setTimeout( resolve, 300 );
} else {
console.error(
`Failed to scroll: Element with ID "${
scrollToId || 'ppcp-settings-container'
}" not found`
);
resolve();
}
const targetId = scrollToId || 'ppcp-settings-container';
scrollAndHighlight( targetId, false ).then( resolve );
}, 100 );
} else {
console.error(

View file

@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\FeaturesDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
@ -50,7 +51,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
'settings.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
@ -61,7 +62,7 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
$can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' );
$can_use_vaulting = $container->has( 'save-payment-methods.eligible' ) && $container->get( 'save-payment-methods.eligible' );
$can_use_card_payments = $container->has( 'card-fields.eligible' ) && $container->get( 'card-fields.eligible' );
@ -81,27 +82,27 @@ return array(
$can_use_subscriptions
);
},
'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
return new GeneralSettings(
$container->get( 'api.shop.country' ),
$container->get( 'api.shop.currency.getter' )->get(),
$container->get( 'wcgateway.is-send-only-country' )
);
},
'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
'settings.data.styling' => static function ( ContainerInterface $container ) : StylingSettings {
return new StylingSettings(
$container->get( 'settings.service.sanitizer' )
);
},
'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
'settings.data.payment' => static function ( ContainerInterface $container ) : PaymentSettings {
return new PaymentSettings();
},
'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
'settings.data.settings' => static function ( ContainerInterface $container ) : SettingsModel {
return new SettingsModel(
$container->get( 'settings.service.sanitizer' )
);
},
'settings.data.paylater-messaging' => static function ( ContainerInterface $container ) : array {
'settings.data.paylater-messaging' => static function ( ContainerInterface $container ) : array {
// TODO: Create an AbstractDataModel wrapper for this configuration!
$config_factors = $container->get( 'paylater-configurator.factory.config' );
@ -125,7 +126,7 @@ return array(
* (onboarding/connected) and connection-aware environment checks.
* This is the preferred solution to check environment and connection state.
*/
'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState {
'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState {
$data = $container->get( 'settings.data.general' );
assert( $data instanceof GeneralSettings );
@ -134,7 +135,7 @@ return array(
return new ConnectionState( $is_connected, $environment );
},
'settings.environment' => static function ( ContainerInterface $container ) : Environment {
'settings.environment' => static function ( ContainerInterface $container ) : Environment {
// We should remove this service in favor of directly using `settings.connection-state`.
$state = $container->get( 'settings.connection-state' );
assert( $state instanceof ConnectionState );
@ -144,7 +145,7 @@ return array(
/**
* Checks if valid merchant connection details are stored in the DB.
*/
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
/*
* This service only resolves the connection status once per request.
* We should remove this service in favor of directly using `settings.connection-state`.
@ -157,7 +158,7 @@ return array(
/**
* Checks if the merchant is connected to a sandbox environment.
*/
'settings.flag.is-sandbox' => static function ( ContainerInterface $container ) : bool {
'settings.flag.is-sandbox' => static function ( ContainerInterface $container ) : bool {
/*
* This service only resolves the sandbox flag once per request.
* We should remove this service in favor of directly using `settings.connection-state`.
@ -167,61 +168,61 @@ return array(
return $state->is_sandbox();
},
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
},
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
return new CommonRestEndpoint( $container->get( 'settings.data.general' ) );
},
'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
return new PaymentRestEndpoint(
$container->get( 'settings.data.payment' ),
$container->get( 'settings.data.definition.methods' )
);
},
'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
return new StylingRestEndpoint(
$container->get( 'settings.data.styling' ),
$container->get( 'settings.service.sanitizer' )
);
},
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
'settings.rest.refresh_feature_status' => static function ( ContainerInterface $container ) : RefreshFeatureStatusEndpoint {
return new RefreshFeatureStatusEndpoint(
$container->get( 'wcgateway.settings' ),
new Cache( 'ppcp-timeout' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.rest.authentication' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
'settings.rest.authentication' => static function ( ContainerInterface $container ) : AuthenticationRestEndpoint {
return new AuthenticationRestEndpoint(
$container->get( 'settings.service.authentication_manager' ),
$container->get( 'settings.service.data-manager' )
);
},
'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
return new LoginLinkRestEndpoint(
$container->get( 'settings.service.connection-url-generator' ),
);
},
'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
'settings.rest.webhooks' => static function ( ContainerInterface $container ) : WebhookSettingsEndpoint {
return new WebhookSettingsEndpoint(
$container->get( 'api.endpoint.webhook' ),
$container->get( 'webhook.registrar' ),
$container->get( 'webhook.status.simulation' )
);
},
'settings.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint {
'settings.rest.pay_later_messaging' => static function ( ContainerInterface $container ) : PayLaterMessagingEndpoint {
return new PayLaterMessagingEndpoint(
$container->get( 'wcgateway.settings' ),
$container->get( 'paylater-configurator.endpoint.save-config' )
);
},
'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint {
'settings.rest.settings' => static function ( ContainerInterface $container ) : SettingsRestEndpoint {
return new SettingsRestEndpoint(
$container->get( 'settings.data.settings' )
);
},
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
return array(
'AR',
'AU',
@ -271,13 +272,13 @@ return array(
'VN',
);
},
'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool {
'settings.casual-selling.eligible' => static function ( ContainerInterface $container ) : bool {
$country = $container->get( 'api.shop.country' );
$eligible_countries = $container->get( 'settings.casual-selling.supported-countries' );
return in_array( $country, $eligible_countries, true );
},
'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
'settings.handler.connection-listener' => static function ( ContainerInterface $container ) : ConnectionListener {
$page_id = $container->has( 'wcgateway.current-ppcp-settings-page-id' ) ? $container->get( 'wcgateway.current-ppcp-settings-page-id' ) : '';
return new ConnectionListener(
@ -288,16 +289,16 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
return new Cache( 'ppcp-paypal-signup-link' );
},
'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
'settings.service.onboarding-url-manager' => static function ( ContainerInterface $container ) : OnboardingUrlManager {
return new OnboardingUrlManager(
$container->get( 'settings.service.signup-link-cache' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
'settings.service.connection-url-generator' => static function ( ContainerInterface $container ) : ConnectionUrlGenerator {
return new ConnectionUrlGenerator(
$container->get( 'api.env.endpoint.partner-referrals' ),
$container->get( 'api.repository.partner-referrals-data' ),
@ -305,7 +306,7 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
'settings.service.authentication_manager' => static function ( ContainerInterface $container ) : AuthenticationManager {
return new AuthenticationManager(
$container->get( 'settings.data.general' ),
$container->get( 'api.env.paypal-host' ),
@ -316,10 +317,10 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
'settings.service.sanitizer' => static function ( ContainerInterface $container ) : DataSanitizer {
return new DataSanitizer();
},
'settings.service.data-manager' => static function ( ContainerInterface $container ) : SettingsDataManager {
'settings.service.data-manager' => static function ( ContainerInterface $container ) : SettingsDataManager {
return new SettingsDataManager(
$container->get( 'settings.data.definition.methods' ),
$container->get( 'settings.data.onboarding' ),
@ -331,7 +332,7 @@ return array(
$container->get( 'settings.data.todos' ),
);
},
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
'settings.ajax.switch_ui' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
return new SwitchSettingsUiEndpoint(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.request-data' ),
@ -339,7 +340,7 @@ return array(
$container->get( 'api.merchant_id' ) !== ''
);
},
'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
'settings.rest.todos' => static function ( ContainerInterface $container ) : TodosRestEndpoint {
return new TodosRestEndpoint(
$container->get( 'settings.data.todos' ),
$container->get( 'settings.data.definition.todos' ),
@ -347,22 +348,25 @@ return array(
$container->get( 'settings.service.todos_sorting' )
);
},
'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
'settings.data.todos' => static function ( ContainerInterface $container ) : TodosModel {
return new TodosModel();
},
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
return new TodosDefinition(
$container->get( 'settings.service.todos_eligibilities' ),
$container->get( 'settings.data.general' ),
$container->get( 'wc-subscriptions.helper' )
$container->get( 'settings.data.general' )
);
},
'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
return new PaymentMethodsDefinition(
$container->get( 'settings.data.payment' ),
$container->get( 'settings.data.definition.method_dependencies' )
);
},
'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition {
return new PaymentMethodsDependenciesDefinition();
},
'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
$pay_later_endpoint = $container->get( 'settings.rest.pay_later_messaging' );
$pay_later_settings = $pay_later_endpoint->get_details()->get_data();
@ -383,7 +387,7 @@ return array(
'is_enabled_for_any_location' => $is_pay_later_messaging_enabled_for_any_location,
);
},
'settings.service.button_locations' => static function ( ContainerInterface $container ) : array {
'settings.service.button_locations' => static function ( ContainerInterface $container ) : array {
$styling_endpoint = $container->get( 'settings.rest.styling' );
$styling_data = $styling_endpoint->get_details()->get_data()['data'];
@ -393,7 +397,7 @@ return array(
'product_enabled' => $styling_data['product']->enabled ?? false,
);
},
'settings.service.gateways_status' => static function ( ContainerInterface $container ) : array {
'settings.service.gateways_status' => static function ( ContainerInterface $container ) : array {
$payment_endpoint = $container->get( 'settings.rest.payment' );
$settings = $payment_endpoint->get_details()->get_data();
@ -404,7 +408,7 @@ return array(
'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
);
},
'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array {
'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
@ -420,7 +424,7 @@ return array(
);
},
'settings.service.todos_eligibilities' => static function ( ContainerInterface $container ) : TodosEligibilityService {
'settings.service.todos_eligibilities' => static function ( ContainerInterface $container ) : TodosEligibilityService {
$pay_later_service = $container->get( 'settings.service.pay_later_status' );
$pay_later_statuses = $pay_later_service['statuses'];
$is_pay_later_messaging_enabled_for_any_location = $pay_later_service['is_enabled_for_any_location'];
@ -439,7 +443,6 @@ return array(
* 3. $gateways, $pay_later_statuses, $button_locations - Plugin settings (enabled/disabled status).
*
* @param bool $is_fastlane_eligible - Show if merchant is eligible (ACDC) but hasn't enabled Fastlane gateway.
* @param bool $is_card_payment_eligible - Show if merchant is eligible (ACDC) but hasn't enabled card button gateway.
* @param bool $is_pay_later_messaging_eligible - Show if Pay Later messaging is enabled for at least one location.
* @param bool $is_pay_later_messaging_product_eligible - Show if Pay Later is not enabled anywhere and specifically not on product page.
* @param bool $is_pay_later_messaging_cart_eligible - Show if Pay Later is not enabled anywhere and specifically not on cart.
@ -457,7 +460,6 @@ return array(
*/
return new TodosEligibilityService(
$container->get( 'axo.eligible' ) && $capabilities['acdc'] && ! $gateways['axo'], // Enable Fastlane.
$capabilities['acdc'] && ! $gateways['card-button'], // Enable Credit and Debit Cards on your checkout.
$is_pay_later_messaging_enabled_for_any_location, // Enable Pay Later messaging.
! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['product'], // Add Pay Later messaging (Product page).
! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['cart'], // Add Pay Later messaging (Cart).
@ -477,7 +479,7 @@ return array(
$container->get( 'googlepay.eligible' ) && $capabilities['google_pay'] && ! $gateways['google_pay'],
);
},
'settings.service.todos_sorting' => static function ( ContainerInterface $container ) : TodosSortingAndFilteringService {
'settings.service.todos_sorting' => static function ( ContainerInterface $container ) : TodosSortingAndFilteringService {
return new TodosSortingAndFilteringService(
$container->get( 'settings.data.todos' )
);

View file

@ -58,7 +58,7 @@ abstract class AbstractDataModel {
*/
public function load() : void {
$saved_data = get_option( static::OPTION_KEY, array() );
$filtered_data = array_intersect_key( $saved_data, $this->data );
$filtered_data = array_intersect_key( (array) $saved_data, $this->data );
$this->data = array_merge( $this->data, $filtered_data );
}

View file

@ -1,6 +1,6 @@
<?php
/**
* PayPal Commerce Todos Definitions
* Payment Methods Definitions
*
* @package WooCommerce\PayPalCommerce\Settings\Data\Definition
*/
@ -40,6 +40,13 @@ class PaymentMethodsDefinition {
*/
private PaymentSettings $settings;
/**
* Payment method dependencies definition.
*
* @var PaymentMethodsDependenciesDefinition
*/
private PaymentMethodsDependenciesDefinition $dependencies_definition;
/**
* List of WooCommerce payment gateways.
*
@ -50,10 +57,15 @@ class PaymentMethodsDefinition {
/**
* Constructor.
*
* @param PaymentSettings $settings Payment methods data model.
* @param PaymentSettings $settings Payment methods data model.
* @param PaymentMethodsDependenciesDefinition $dependencies_definition Payment dependencies definition.
*/
public function __construct( PaymentSettings $settings ) {
$this->settings = $settings;
public function __construct(
PaymentSettings $settings,
PaymentMethodsDependenciesDefinition $dependencies_definition
) {
$this->settings = $settings;
$this->dependencies_definition = $dependencies_definition;
}
/**
@ -73,15 +85,30 @@ class PaymentMethodsDefinition {
$result = array();
foreach ( $all_methods as $method ) {
$result[ $method['id'] ] = $this->build_method_definition(
$method['id'],
$method_id = $method['id'];
// Add dependency info if applicable.
$depends_on = $this->dependencies_definition->get_parent_methods( $method_id );
if ( ! empty( $depends_on ) ) {
$method['depends_on'] = $depends_on;
}
$result[ $method_id ] = $this->build_method_definition(
$method_id,
$method['title'],
$method['description'],
$method['icon'],
$method['fields'] ?? array()
$method['fields'] ?? array(),
$depends_on
);
}
// Add dependency maps to metadata.
$result['__meta'] = array(
'dependencies' => $this->dependencies_definition->get_dependencies(),
'dependents' => $this->dependencies_definition->get_dependents_map(),
);
return $result;
}
@ -95,14 +122,15 @@ class PaymentMethodsDefinition {
* @param string $icon Admin-side icon of the payment method.
* @param array|false $fields Optional. Additional fields to display in the edit modal.
* Setting this to false omits all fields.
* @param array $depends_on Optional. IDs of payment methods that this depends on.
* @return array Payment method definition.
*/
private function build_method_definition(
string $gateway_id,
string $title,
string $description,
string $icon,
$fields = array()
string $icon, $fields = array(),
array $depends_on = array()
) : array {
$gateway = $this->wc_gateways[ $gateway_id ] ?? null;
@ -119,6 +147,11 @@ class PaymentMethodsDefinition {
'itemDescription' => $description,
);
// Add dependency information if provided - ensure it's included directly in the config.
if ( ! empty( $depends_on ) ) {
$config['depends_on'] = $depends_on;
}
if ( is_array( $fields ) ) {
$config['fields'] = array_merge(
array(

View file

@ -0,0 +1,111 @@
<?php
/**
* Payment Methods Dependencies Definition
*
* @package WooCommerce\PayPalCommerce\Settings\Data\Definition
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\IDealGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MultibancoGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway;
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
/**
* Class PaymentMethodsDependenciesDefinition
*
* Defines dependency relationships between payment methods.
*/
class PaymentMethodsDependenciesDefinition {
/**
* Get all payment method dependencies
*
* Maps dependent method ID => array of parent method IDs.
* A dependent method is disabled if ANY of its required parents is disabled.
*
* @return array The dependency relationships between payment methods
*/
public function get_dependencies(): array {
$dependencies = array(
CardButtonGateway::ID => array( PayPalGateway::ID ),
CreditCardGateway::ID => array( PayPalGateway::ID ),
AxoGateway::ID => array( PayPalGateway::ID, CreditCardGateway::ID ),
ApplePayGateway::ID => array( PayPalGateway::ID, CreditCardGateway::ID ),
GooglePayGateway::ID => array( PayPalGateway::ID, CreditCardGateway::ID ),
BancontactGateway::ID => array( PayPalGateway::ID ),
BlikGateway::ID => array( PayPalGateway::ID ),
EPSGateway::ID => array( PayPalGateway::ID ),
IDealGateway::ID => array( PayPalGateway::ID ),
MultibancoGateway::ID => array( PayPalGateway::ID ),
MyBankGateway::ID => array( PayPalGateway::ID ),
P24Gateway::ID => array( PayPalGateway::ID ),
TrustlyGateway::ID => array( PayPalGateway::ID ),
PayUponInvoiceGateway::ID => array( PayPalGateway::ID ),
OXXO::ID => array( PayPalGateway::ID ),
'venmo' => array( PayPalGateway::ID ),
'pay-later' => array( PayPalGateway::ID ),
);
return apply_filters(
'woocommerce_paypal_payments_payment_method_dependencies',
$dependencies
);
}
/**
* Create a mapping from parent methods to their dependent methods
*
* @return array Parent-to-child dependency map
*/
public function get_dependents_map(): array {
$result = array();
$dependencies = $this->get_dependencies();
foreach ( $dependencies as $child_id => $parent_ids ) {
foreach ( $parent_ids as $parent_id ) {
if ( ! isset( $result[ $parent_id ] ) ) {
$result[ $parent_id ] = array();
}
$result[ $parent_id ][] = $child_id;
}
}
return $result;
}
/**
* Get all parent methods that a method depends on
*
* @param string $method_id Method ID to check.
* @return array Array of parent method IDs
*/
public function get_parent_methods( string $method_id ): array {
return $this->get_dependencies()[ $method_id ] ?? array();
}
/**
* Get methods that depend on a parent method
*
* @param string $parent_id Parent method ID.
* @return array Array of dependent method IDs
*/
public function get_dependent_methods( string $parent_id ): array {
return $this->get_dependents_map()[ $parent_id ] ?? array();
}
}

View file

@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class TodosDefinition
@ -35,28 +34,18 @@ class TodosDefinition {
*/
protected GeneralSettings $settings;
/**
* The subscription helper.
*
* @var SubscriptionHelper
*/
protected SubscriptionHelper $subscription_helper;
/**
* Constructor.
*
* @param TodosEligibilityService $eligibilities The todos eligibility service.
* @param GeneralSettings $settings The general settings service.
* @param SubscriptionHelper $subscription_helper The subscription helper.
*/
public function __construct(
TodosEligibilityService $eligibilities,
GeneralSettings $settings,
SubscriptionHelper $subscription_helper
GeneralSettings $settings
) {
$this->eligibilities = $eligibilities;
$this->settings = $settings;
$this->subscription_helper = $subscription_helper;
$this->eligibilities = $eligibilities;
$this->settings = $settings;
}
/**
@ -80,18 +69,6 @@ class TodosDefinition {
),
'priority' => 1,
),
'enable_credit_debit_cards' => array(
'title' => __( 'Enable Credit and Debit Cards on your checkout', 'woocommerce-paypal-payments' ),
'description' => __( 'Credit and Debit Cards is now available for Blocks checkout pages', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['enable_credit_debit_cards'],
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-button-gateway',
'highlight' => 'ppcp-card-button-gateway',
),
'priority' => 2,
),
'enable_pay_later_messaging' => array(
'title' => __( 'Enable Pay Later messaging', 'woocommerce-paypal-payments' ),
'description' => __( 'Show Pay Later messaging to boost conversion rate and increase cart size', 'woocommerce-paypal-payments' ),
@ -138,9 +115,7 @@ class TodosDefinition {
'isEligible' => $eligibility_checks['configure_paypal_subscription'],
'action' => array(
'type' => 'external',
'url' => $this->subscription_helper->has_subscription_products()
? admin_url( 'edit.php?post_type=product&product_type=subscription' ) // If subscription products exist, go to the subscriptions products archive page.
: admin_url( 'post-new.php?post_type=product' ), // If there are no subscriptions products, go to create one.
'url' => 'https://woocommerce.com/document/woocommerce-paypal-payments/#paypal-subscriptions',
'completeOnClick' => true,
),
'priority' => 5,

View file

@ -150,7 +150,17 @@ class PaymentRestEndpoint extends RestEndpoint {
$gateway_settings = array();
$all_methods = $this->gateways();
// First extract __meta if present.
if ( isset( $all_methods['__meta'] ) ) {
$gateway_settings['__meta'] = $all_methods['__meta'];
}
foreach ( $all_methods as $key => $method ) {
// Skip the __meta key as we've already handled it.
if ( $key === '__meta' ) {
continue;
}
$gateway_settings[ $key ] = array(
'id' => $method['id'],
'title' => $method['title'],
@ -164,6 +174,11 @@ class PaymentRestEndpoint extends RestEndpoint {
if ( isset( $method['fields'] ) ) {
$gateway_settings[ $key ]['fields'] = $method['fields'];
}
// Preserve dependency information.
if ( isset( $method['depends_on'] ) ) {
$gateway_settings[ $key ]['depends_on'] = $method['depends_on'];
}
}
$gateway_settings['paypalShowLogo'] = $this->settings->get_paypal_show_logo();

View file

@ -225,10 +225,11 @@ class SettingsDataManager {
if ( $flags->use_card_payments ) {
// Enable ACDC for business sellers.
$this->payment_methods->toggle_method_state( CreditCardGateway::ID, true );
}
$this->payment_methods->toggle_method_state( ApplePayGateway::ID, true );
$this->payment_methods->toggle_method_state( GooglePayGateway::ID, true );
// Apple Pay and Google Pay depend on the ACDC gateway.
$this->payment_methods->toggle_method_state( ApplePayGateway::ID, true );
$this->payment_methods->toggle_method_state( GooglePayGateway::ID, true );
}
// Enable all APM methods.
foreach ( $methods_apm as $method ) {

View file

@ -24,13 +24,6 @@ class TodosEligibilityService {
*/
private bool $is_fastlane_eligible;
/**
* Whether card payments are eligible.
*
* @var bool
*/
private bool $is_card_payment_eligible;
/**
* Whether Pay Later messaging is eligible.
*
@ -133,7 +126,6 @@ class TodosEligibilityService {
* Constructor.
*
* @param bool $is_fastlane_eligible Whether Fastlane is eligible.
* @param bool $is_card_payment_eligible Whether card payments are eligible.
* @param bool $is_pay_later_messaging_eligible Whether Pay Later messaging is eligible.
* @param bool $is_pay_later_messaging_product_eligible Whether Pay Later messaging for product page is eligible.
* @param bool $is_pay_later_messaging_cart_eligible Whether Pay Later messaging for cart is eligible.
@ -151,7 +143,6 @@ class TodosEligibilityService {
*/
public function __construct(
bool $is_fastlane_eligible,
bool $is_card_payment_eligible,
bool $is_pay_later_messaging_eligible,
bool $is_pay_later_messaging_product_eligible,
bool $is_pay_later_messaging_cart_eligible,
@ -168,7 +159,6 @@ class TodosEligibilityService {
bool $is_enable_google_pay_eligible
) {
$this->is_fastlane_eligible = $is_fastlane_eligible;
$this->is_card_payment_eligible = $is_card_payment_eligible;
$this->is_pay_later_messaging_eligible = $is_pay_later_messaging_eligible;
$this->is_pay_later_messaging_product_eligible = $is_pay_later_messaging_product_eligible;
$this->is_pay_later_messaging_cart_eligible = $is_pay_later_messaging_cart_eligible;
@ -193,7 +183,6 @@ class TodosEligibilityService {
public function get_eligibility_checks(): array {
return array(
'enable_fastlane' => fn() => $this->is_fastlane_eligible,
'enable_credit_debit_cards' => fn() => $this->is_card_payment_eligible,
'enable_pay_later_messaging' => fn() => $this->is_pay_later_messaging_eligible,
'add_pay_later_messaging_product_page' => fn() => $this->is_pay_later_messaging_product_eligible,
'add_pay_later_messaging_cart' => fn() => $this->is_pay_later_messaging_cart_eligible,

View file

@ -342,6 +342,12 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
assert( $dcc_applies instanceof DCCApplies );
$general_settings = $container->get( 'settings.data.general' );
assert( $general_settings instanceof GeneralSettings );
$merchant_data = $general_settings->get_merchant_data();
$merchant_country = $merchant_data->merchant_country;
// Unset BCDC if merchant is eligible for ACDC and country is eligible for card fields.
$card_fields_eligible = $container->get( 'card-fields.eligible' );
if ( $dcc_product_status->is_active() && $card_fields_eligible ) {
@ -368,6 +374,16 @@ class SettingsModule implements ServiceModule, ExecutableModule {
unset( $payment_methods['ppcp-axo-gateway'] );
}
// Unset OXXO if merchant country is not Mexico.
if ( 'MX' !== $merchant_country ) {
unset( $payment_methods[ OXXO::ID ] );
}
// Unset Pay Unon Invoice if merchant country is not Germany.
if ( 'DE' !== $merchant_country ) {
unset( $payment_methods[ PayUponInvoiceGateway::ID ] );
}
// For non-ACDC regions unset ACDC, local APMs and set BCDC.
if ( ! $dcc_applies ) {
unset( $payment_methods[ CreditCardGateway::ID ] );

View file

@ -159,7 +159,7 @@ class Settings implements ContainerInterface {
if ( $this->settings ) {
return false;
}
$this->settings = get_option( self::KEY, array() );
$this->settings = (array) get_option( self::KEY, array() );
$defaults = array(
'title' => __( 'PayPal', 'woocommerce-paypal-payments' ),