Merge branch 'PCP-4210-features-refactor-to-use-rest-endpoints' of github.com:woocommerce/woocommerce-paypal-payments into PCP-4210-features-refactor-to-use-rest-endpoints

This commit is contained in:
Daniel Dudzic 2025-02-24 16:21:27 +01:00
commit 78d14f4858
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
132 changed files with 2957 additions and 1152 deletions

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

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

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

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

@ -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' )
);
},

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

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

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

@ -30,13 +30,17 @@ class SettingsTabMapHelper {
*/
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',
'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',
);
}
@ -57,6 +61,12 @@ class SettingsTabMapHelper {
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;
}
@ -98,4 +108,37 @@ class SettingsTabMapHelper {
: 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

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

@ -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;
@ -44,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',
/**
@ -53,14 +61,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 +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

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

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

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

@ -365,7 +365,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>
@ -476,7 +476,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>';
@ -507,7 +507,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,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

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

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

@ -70,16 +70,33 @@ $width_gap: 24px;
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 {
opacity: 0.5;
transition: opacity 0.3s;
&:hover {
opacity: 1;
&.ppcp--dimmed {
opacity: 0.5;
&:hover {
opacity: 1;
}
}
.components-button.is-tertiary:first-child {
padding-left: 0;
.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

@ -227,11 +227,6 @@
}
// 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);

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,8 +11,8 @@ import { getQuery } from '../utils/navigation';
const SettingsApp = () => {
const { isReady: onboardingIsReady, completed: onboardingCompleted } =
OnboardingHooks.useSteps();
const { isReady: merchantIsReady } = CommonHooks.useStore();
const {
isReady: merchantIsReady,
merchant: { isSendOnlyCountry },
} = CommonHooks.useMerchantInfo();

View file

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

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

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

@ -22,8 +22,13 @@ const StepBusiness = ( {} ) => {
);
useEffect( () => {
if ( ! businessChoice ) {
return;
}
setIsCasualSeller( BUSINESS_TYPES.CASUAL_SELLER === businessChoice );
}, [ businessChoice, setIsCasualSeller ] );
const { canUseSubscriptions } = OnboardingHooks.useFlags();
const businessChoices = [
{

View file

@ -76,7 +76,17 @@ const StepProducts = () => {
'woocommerce-paypal-payments'
),
isDisabled: isCasualSeller,
contents: <DetailsSubscriptions showNotice={ 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 (
@ -117,14 +127,19 @@ const DetailsPhysical = () => (
</ul>
);
const DetailsSubscriptions = ( { showNotice } ) => (
const DetailsSubscriptions = ( { showLink, showNotice } ) => (
<>
<a
target="__blank"
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq"
>
{ __( 'WooCommerce Subscriptions', 'woocommerce-paypal-payments' ) }
</a>
{ showLink && (
<a
target="__blank"
href="https://woocommerce.com/document/woocommerce-paypal-payments/#subscriptions-faq"
>
{ __(
'WooCommerce Subscriptions',
'woocommerce-paypal-payments'
) }
</a>
) }
{ showNotice && (
<p>
{ __(

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',

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,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,4 +1,5 @@
import { __ } from '@wordpress/i18n';
import { useEffect } from 'react';
import { ControlToggleButton } from '../../../../../ReusableComponents/Controls';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
@ -12,6 +13,12 @@ const OrderIntent = () => {
setCaptureVirtualOnlyOrders,
} = SettingsHooks.useSettings();
useEffect( () => {
if ( ! authorizeOnly && captureVirtualOnlyOrders ) {
setCaptureVirtualOnlyOrders( false );
}
}, [ authorizeOnly ] );
return (
<SettingsBlock
title={ __( 'Order Intent', 'woocommerce-paypal-payments' ) }
@ -34,6 +41,7 @@ const OrderIntent = () => {
) }
onChange={ setCaptureVirtualOnlyOrders }
value={ captureVirtualOnlyOrders }
disabled={ ! authorizeOnly }
/>
</SettingsBlock>
);

View file

@ -7,6 +7,7 @@ import ConnectionStatusBadge from './Parts/ConnectionStatusBadge';
import DisconnectButton from './Parts/DisconnectButton';
import SettingsBlock from '../../../../ReusableComponents/SettingsBlock';
import { ControlStaticValue } from '../../../../ReusableComponents/Controls';
import { CardActions } from '../../../../ReusableComponents/Elements';
const ConnectionStatus = () => {
const merchant = CommonHooks.useMerchant();
@ -21,18 +22,20 @@ const ConnectionStatus = () => {
title={ __( 'Connection status', 'woocommerce-paypal-payments' ) }
description={ <ConnectionDescription /> }
>
<SettingsBlock>
<SettingsBlock className="ppcp--pull-right">
<ControlStaticValue
value={
<ConnectionStatusBadge
isActive={ merchant.isConnected }
isSandbox={ merchant.isSandbox }
isBusinessSeller={ merchant.isBusinessSeller }
/>
}
/>
</SettingsBlock>
<SettingsBlock
title={ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
className="ppcp--no-gap"
>
<ControlStaticValue value={ merchant.id } />
</SettingsBlock>
@ -59,9 +62,9 @@ const ConnectionDescription = () => {
'Your PayPal account connection details.',
'woocommerce-paypal-payments'
) }
<div className="ppcp--card-actions">
<CardActions isDimmed={ true }>
<DisconnectButton />
</div>
</CardActions>
</>
);
};

View file

@ -4,7 +4,6 @@ import {
Content,
ContentWrapper,
} from '../../../../ReusableComponents/Elements';
import ConnectionDetails from './Blocks/ConnectionDetails';
import Troubleshooting from './Blocks/Troubleshooting';
import PaypalSettings from './Blocks/PaypalSettings';
import OtherSettings from './Blocks/OtherSettings';
@ -29,12 +28,12 @@ const ExpertSettings = () => {
contentContainer={ false }
>
<ContentWrapper>
<Content>
{ /*<Content>
<ConnectionDetails
updateFormValue={ updateFormValue }
settings={ settings }
/>
</Content>
</Content>*/ }
<Content>
<Troubleshooting

View file

@ -5,11 +5,19 @@ import TitleBadge, {
TITLE_BADGE_POSITIVE,
} from '../../../../../ReusableComponents/TitleBadge';
const ConnectionStatusBadge = ( { isActive, isSandbox } ) => {
const ConnectionStatusBadge = ( { isActive, isSandbox, isBusinessSeller } ) => {
if ( isActive ) {
const label = isSandbox
? __( 'Sandbox Mode', 'woocommerce-paypal-payments' )
: __( 'Active', 'woocommerce-paypal-payments' );
let label;
if ( isBusinessSeller ) {
label = isSandbox
? __( 'Business | Sandbox', 'woocommerce-paypal-payments' )
: __( 'Business | Live', 'woocommerce-paypal-payments' );
} else {
label = isSandbox
? __( 'Sandbox', 'woocommerce-paypal-payments' )
: __( 'Active', 'woocommerce-paypal-payments' );
}
return <TitleBadge type={ TITLE_BADGE_POSITIVE } text={ label } />;
}

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

@ -82,3 +82,16 @@ export function persist() {
} );
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}

View file

@ -38,10 +38,16 @@ const useStoreData = () => {
};
export const useStore = () => {
const { dispatch, useTransient } = useStoreData();
const { select, dispatch, useTransient } = useStoreData();
const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
return { persist: dispatch.persist, isReady };
// Load persistent data from REST if not done yet.
if ( ! isReady ) {
select.persistentData();
}
return { persist, refresh, isReady };
};
// TODO: Replace with real hook.

View file

@ -27,6 +27,19 @@ export function persist() {
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}
/**
* Side effect. Fetches the ISU-login URL for a sandbox account.
*

View file

@ -13,8 +13,32 @@ import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
const useHooks = () => {
/**
* Single source of truth for access Redux details.
*
* This hook returns a stable API to access actions, selectors and special hooks to generate
* getter- and setters for transient or persistent properties.
*
* @return {{select, dispatch, useTransient, usePersistent}} Store data API.
*/
const useStoreData = () => {
const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
return useMemo(
() => ( {
select,
dispatch,
useTransient,
usePersistent,
} ),
[ select, dispatch, useTransient, usePersistent ]
);
};
const useHooks = () => {
const { useTransient, usePersistent, dispatch, select } = useStoreData();
const {
persist,
sandboxOnboardingUrl,
@ -23,10 +47,9 @@ const useHooks = () => {
authenticateWithOAuth,
startWebhookSimulation,
checkWebhookSimulationState,
} = useDispatch( STORE_NAME );
} = dispatch;
// Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
const [ activeHighlight, setActiveHighlight ] =
useTransient( 'activeHighlight' );
@ -38,18 +61,9 @@ const useHooks = () => {
);
// Read-only properties.
const wooSettings = useSelect(
( select ) => select( STORE_NAME ).wooSettings(),
[]
);
const features = useSelect(
( select ) => select( STORE_NAME ).features(),
[]
);
const webhooks = useSelect(
( select ) => select( STORE_NAME ).webhooks(),
[]
);
const wooSettings = select.wooSettings();
const features = select.features();
const webhooks = select.webhooks();
const savePersistent = async ( setter, value ) => {
setter( value );
@ -57,7 +71,6 @@ const useHooks = () => {
};
return {
isReady,
activeModal,
setActiveModal,
activeHighlight,
@ -82,6 +95,19 @@ const useHooks = () => {
};
};
export const useStore = () => {
const { select, dispatch, useTransient } = useStoreData();
const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
// Load persistent data from REST if not done yet.
if ( ! isReady ) {
select.persistentData();
}
return { persist, refresh, isReady };
};
export const useSandbox = () => {
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
@ -139,7 +165,7 @@ export const useWebhooks = () => {
};
export const useMerchantInfo = () => {
const { isReady, features } = useHooks();
const { features } = useHooks();
const merchant = useMerchant();
const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME );
@ -164,7 +190,6 @@ export const useMerchantInfo = () => {
}, [ refreshMerchantData, setMerchant ] );
return {
isReady,
merchant, // Merchant details
features, // Eligible merchant features
verifyLoginStatus, // Callback
@ -204,7 +229,9 @@ export const useActiveHighlight = () => {
return { activeHighlight, setActiveHighlight };
};
// -- Not using the `useHooks()` data provider --
/*
* Busy state management hooks
*/
export const useBusyState = () => {
const { startActivity, stopActivity } = useDispatch( STORE_NAME );

View file

@ -18,10 +18,15 @@ export const addDebugTools = ( context, modules ) => {
if ( ! context.debug ) { return }
*/
const describe = ( fnName, fnInfo ) => {
// eslint-disable-next-line no-console
console.log( `\n%c${ fnName }:`, 'font-weight:bold', fnInfo, '\n\n' );
};
const debugApi = ( window.ppcpDebugger = window.ppcpDebugger || {} );
// Dump the current state of all our Redux stores.
debugApi.dumpStore = async () => {
debugApi.dumpStore = async ( cbFilter = null ) => {
/* eslint-disable no-console */
if ( ! console?.groupCollapsed ) {
console.error( 'console.groupCollapsed is not supported.' );
@ -34,11 +39,19 @@ export const addDebugTools = ( context, modules ) => {
console.group( `[STORE] ${ storeSelector }` );
const dumpStore = ( selector ) => {
const contents = wp.data.select( storeName )[ selector ]();
let contents = wp.data.select( storeName )[ selector ]();
console.groupCollapsed( `.${ selector }()` );
console.table( contents );
console.groupEnd();
if ( cbFilter ) {
contents = cbFilter( contents, selector, storeName );
if ( undefined !== contents && null !== contents ) {
console.log( `.${ selector }() [filtered]`, contents );
}
} else {
console.groupCollapsed( `.${ selector }()` );
console.table( contents );
console.groupEnd();
}
};
Object.keys( module.selectors ).forEach( dumpStore );
@ -51,46 +64,89 @@ export const addDebugTools = ( context, modules ) => {
// Reset all Redux stores to their initial state.
debugApi.resetStore = () => {
const stores = [];
const { isConnected } = wp.data.select( CommonStoreName ).merchant();
if ( isConnected ) {
// Make sure the Onboarding wizard is "completed".
const onboarding = wp.data.dispatch( OnboardingStoreName );
onboarding.setPersistent( 'completed', true );
onboarding.persist();
describe(
'resetStore',
'Reset all Redux stores to their DEFAULT state, without changing any server-side data. The default state is defined in the JS code.'
);
// Reset all stores, except for the onboarding store.
stores.push( CommonStoreName );
stores.push( PaymentStoreName );
stores.push( SettingsStoreName );
stores.push( StylingStoreName );
stores.push( TodosStoreName );
stores.push( FeaturesStoreName );
} else {
// Only reset the common & onboarding stores to restart the onboarding wizard.
stores.push( CommonStoreName );
const { completed } = wp.data
.select( OnboardingStoreName )
.persistentData();
// Reset all stores, except for the onboarding store.
stores.push( CommonStoreName );
stores.push( PaymentStoreName );
stores.push( SettingsStoreName );
stores.push( StylingStoreName );
stores.push( TodosStoreName );
// Only reset the onboarding store when the wizard is not completed.
if ( ! completed ) {
stores.push( OnboardingStoreName );
}
stores.forEach( ( storeName ) => {
const store = wp.data.dispatch( storeName );
// eslint-disable-next-line no-console
console.log( `Reset store: ${ storeName }...` );
try {
store.reset();
store.persist();
// eslint-disable-next-line no-console
console.log( `Done: Store '${ storeName }' reset` );
} catch ( error ) {
console.error( ' ... Reset failed, skipping this store' );
console.error(
`Failed: Could not reset store '${ storeName }'`
);
}
} );
// eslint-disable-next-line no-console
console.log( '---- Complete ----\n\n' );
};
debugApi.refreshStore = () => {
const stores = [];
describe(
'refreshStore',
'Refreshes all Redux details with details provided by the server. This has a similar effect as reloading the page without saving'
);
stores.push( CommonStoreName );
stores.push( PaymentStoreName );
stores.push( SettingsStoreName );
stores.push( StylingStoreName );
stores.push( TodosStoreName );
stores.push( OnboardingStoreName );
stores.forEach( ( storeName ) => {
const store = wp.data.dispatch( storeName );
try {
store.refresh();
// eslint-disable-next-line no-console
console.log(
`Done: Store '${ storeName }' refreshed from REST`
);
} catch ( error ) {
console.error(
`Failed: Could not refresh store '${ storeName }' from REST`
);
}
} );
// eslint-disable-next-line no-console
console.log( '---- Complete ----\n\n' );
};
// Disconnect the merchant and display the onboarding wizard.
debugApi.disconnect = () => {
const common = wp.data.dispatch( CommonStoreName );
describe();
common.disconnectMerchant();
// eslint-disable-next-line no-console
@ -103,6 +159,11 @@ export const addDebugTools = ( context, modules ) => {
debugApi.onboardingMode = ( state ) => {
const onboarding = wp.data.dispatch( OnboardingStoreName );
describe(
'onboardingMode',
'Toggle between onboarding wizard and the settings screen.'
);
onboarding.setPersistent( 'completed', ! state );
onboarding.persist();
};

View file

@ -87,3 +87,16 @@ export function persist() {
}
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}

View file

@ -82,3 +82,16 @@ export function persist() {
} );
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}

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

@ -94,3 +94,16 @@ export function persist() {
} );
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}

View file

@ -7,22 +7,58 @@
* @file
*/
import { useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils';
import { useMemo } from '@wordpress/element';
const useHooks = () => {
/**
* Single source of truth for access Redux details.
*
* This hook returns a stable API to access actions, selectors and special hooks to generate
* getter- and setters for transient or persistent properties.
*
* @return {{select, dispatch, useTransient, usePersistent}} Store data API.
*/
const useStoreData = () => {
const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist, setPersistent, changePaymentSettings } =
useDispatch( STORE_NAME );
// Read-only flags and derived state.
// Nothing here yet.
return useMemo(
() => ( {
select,
dispatch,
useTransient,
usePersistent,
} ),
[ select, dispatch, useTransient, usePersistent ]
);
};
// Transient accessors.
export const useStore = () => {
const { select, useTransient, dispatch } = useStoreData();
const { persist, refresh, setPersistent, changePaymentSettings } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
// Load persistent data from REST if not done yet.
if ( ! isReady ) {
select.persistentData();
}
return {
persist,
refresh,
setPersistent,
changePaymentSettings,
isReady,
};
};
export const usePaymentMethods = () => {
const { usePersistent } = useStoreData();
// PayPal checkout.
const [ paypal ] = usePersistent( 'ppcp-gateway' );
const [ venmo ] = usePersistent( 'venmo' );
@ -47,79 +83,6 @@ const useHooks = () => {
const [ pui ] = usePersistent( 'ppcp-pay-upon-invoice-gateway' );
const [ oxxo ] = usePersistent( 'ppcp-oxxo-gateway' );
// Custom modal data.
const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' );
const [ threeDSecure ] = usePersistent( 'threeDSecure' );
const [ fastlaneCardholderName ] = usePersistent(
'fastlaneCardholderName'
);
const [ fastlaneDisplayWatermark ] = usePersistent(
'fastlaneDisplayWatermark'
);
return {
persist,
isReady,
setPersistent,
changePaymentSettings,
paypal,
venmo,
payLater,
creditCard,
advancedCreditCard,
fastlane,
applePay,
googlePay,
bancontact,
blik,
eps,
ideal,
mybank,
p24,
trustly,
multibanco,
pui,
oxxo,
paypalShowLogo,
threeDSecure,
fastlaneCardholderName,
fastlaneDisplayWatermark,
};
};
export const useStore = () => {
const { persist, isReady, setPersistent, changePaymentSettings } =
useHooks();
return { persist, isReady, setPersistent, changePaymentSettings };
};
export const usePaymentMethods = () => {
const {
// PayPal Checkout.
paypal,
venmo,
payLater,
creditCard,
// Online card payments.
advancedCreditCard,
fastlane,
applePay,
googlePay,
// Local APMs.
bancontact,
blik,
eps,
ideal,
mybank,
p24,
trustly,
multibanco,
pui,
oxxo,
} = useHooks();
const payPalCheckout = [ paypal, venmo, payLater, creditCard ];
const onlineCardPayments = [
advancedCreditCard,
@ -169,12 +132,16 @@ export const usePaymentMethods = () => {
};
export const usePaymentMethodsModal = () => {
const {
paypalShowLogo,
threeDSecure,
fastlaneCardholderName,
fastlaneDisplayWatermark,
} = useHooks();
const { usePersistent } = useStoreData();
const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' );
const [ threeDSecure ] = usePersistent( 'threeDSecure' );
const [ fastlaneCardholderName ] = usePersistent(
'fastlaneCardholderName'
);
const [ fastlaneDisplayWatermark ] = usePersistent(
'fastlaneDisplayWatermark'
);
return {
paypalShowLogo,

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

@ -19,6 +19,7 @@ const defaultTransient = Object.freeze( {
// Persistent: Values that are loaded from the DB.
const defaultPersistent = Object.freeze( {
// Payment methods.
'ppcp-gateway': {},
venmo: {},
'pay-later': {},
@ -37,6 +38,8 @@ const defaultPersistent = Object.freeze( {
'ppcp-multibanco': {},
'ppcp-pay-upon-invoice-gateway': {},
'ppcp-oxxo-gateway': {},
// Custom payment method properties.
paypalShowLogo: false,
threeDSecure: 'no-3d-secure',
fastlaneCardholderName: false,
@ -85,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

@ -84,3 +84,16 @@ export function persist() {
} );
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}

View file

@ -6,17 +6,38 @@
*
* @file
*/
import { useDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils';
import { useMemo } from '@wordpress/element';
/**
* Single source of truth for access Redux details.
*
* This hook returns a stable API to access actions, selectors and special hooks to generate
* getter- and setters for transient or persistent properties.
*
* @return {{select, dispatch, useTransient, usePersistent}} Store data API.
*/
const useStoreData = () => {
const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
return useMemo(
() => ( {
select,
dispatch,
useTransient,
usePersistent,
} ),
[ select, dispatch, useTransient, usePersistent ]
);
};
const useHooks = () => {
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
const [ isReady ] = useTransient( 'isReady' );
const { usePersistent } = useStoreData();
// Persistent accessors.
const [ invoicePrefix, setInvoicePrefix ] =
@ -47,8 +68,6 @@ const useHooks = () => {
usePersistent( 'disabledCards' );
return {
persist,
isReady,
invoicePrefix,
setInvoicePrefix,
authorizeOnly,
@ -79,8 +98,16 @@ const useHooks = () => {
};
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
const { select, dispatch, useTransient } = useStoreData();
const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
// Load persistent data from REST if not done yet.
if ( ! isReady ) {
select.persistentData();
}
return { persist, refresh, isReady };
};
export const useSettings = () => {

View file

@ -82,3 +82,16 @@ export function persist() {
} );
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}

View file

@ -7,7 +7,7 @@
* @file
*/
import { useCallback } from '@wordpress/element';
import { useCallback, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { createHooksForStore } from '../utils';
@ -20,13 +20,37 @@ import {
STYLING_PAYMENT_METHODS,
STYLING_SHAPES,
} from './configuration';
import { persistentData } from './selectors';
/**
* Single source of truth for access Redux details.
*
* This hook returns a stable API to access actions, selectors and special hooks to generate
* getter- and setters for transient or persistent properties.
*
* @return {{select, dispatch, useTransient, usePersistent}} Store data API.
*/
const useStoreData = () => {
const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
return useMemo(
() => ( {
select,
dispatch,
useTransient,
usePersistent,
} ),
[ select, dispatch, useTransient, usePersistent ]
);
};
const useHooks = () => {
const { useTransient } = createHooksForStore( STORE_NAME );
const { persist, setPersistent } = useDispatch( STORE_NAME );
const { useTransient, dispatch } = useStoreData();
const { setPersistent } = dispatch;
// Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
const [ location, setLocation ] = useTransient( 'location' );
// Persistent accessors.
@ -61,8 +85,6 @@ const useHooks = () => {
);
return {
persist,
isReady,
location,
setLocation,
getLocationProp,
@ -71,8 +93,16 @@ const useHooks = () => {
};
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
const { select, dispatch, useTransient } = useStoreData();
const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
// Load persistent data from REST if not done yet.
if ( ! isReady ) {
select.persistentData();
}
return { persist, refresh, isReady };
};
export const useStylingLocation = () => {

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

@ -5,6 +5,12 @@
*/
export default {
/**
* Resets the store state to its initial values.
* Used when needing to clear all store data.
*/
RESET: 'ppcp/todos/RESET',
// Transient data
SET_TRANSIENT: 'ppcp/todos/SET_TRANSIENT',
SET_COMPLETED_TODOS: 'ppcp/todos/SET_COMPLETED_TODOS',

View file

@ -17,11 +17,47 @@ import {
REST_RESET_DISMISSED_TODOS_PATH,
} from './constants';
export const setIsReady = ( isReady ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { isReady },
/**
* Special. Resets all values in the store to initial defaults.
*
* @return {Object} The action.
*/
export const reset = () => ( {
type: ACTION_TYPES.RESET,
} );
/**
* Generic transient-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Object} The action.
*/
export const setTransient = ( prop, value ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
payload: { [ prop ]: value },
} );
/**
* Generic persistent-data updater.
*
* @param {string} prop Name of the property to update.
* @param {any} value The new value of the property.
* @return {Object} The action.
*/
export const setPersistent = ( prop, value ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { [ prop ]: value },
} );
/**
* Transient. Marks the store as "ready", i.e., fully initialized.
*
* @param {boolean} isReady Whether the store is ready
* @return {Object} The action.
*/
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
export const setTodos = ( todos ) => ( {
type: ACTION_TYPES.SET_TODOS,
payload: todos,
@ -39,6 +75,7 @@ export const setCompletedTodos = ( completedTodos ) => ( {
// Thunks
// TODO: Possibly, this should be a resolver?
export function fetchTodos() {
return async () => {
const response = await apiFetch( { path: REST_PATH } );
@ -46,9 +83,14 @@ export function fetchTodos() {
};
}
/**
* Thunk action creator. Triggers the persistence of store data to the server.
*
* @return {Function} The thunk function.
*/
export function persist() {
return async ( { select } ) => {
return await apiFetch( {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data: select.persistentData(),
@ -56,6 +98,19 @@ export function persist() {
};
}
/**
* Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values.
*
* @return {Function} The thunk function.
*/
export function refresh() {
return ( { dispatch, select } ) => {
dispatch.invalidateResolutionForStore();
select.persistentData();
};
}
export function resetDismissedTodos() {
return async ( { dispatch } ) => {
try {

View file

@ -10,31 +10,40 @@
import { useSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils';
import { useMemo } from '@wordpress/element';
const ensureArray = ( value ) => {
if ( ! value ) {
return [];
}
return Array.isArray( value ) ? value : Object.values( value );
/**
* Single source of truth for access Redux details.
*
* This hook returns a stable API to access actions, selectors and special hooks to generate
* getter- and setters for transient or persistent properties.
*
* @return {{select, dispatch, useTransient, usePersistent}} Store data API.
*/
const useStoreData = () => {
const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
return useMemo(
() => ( {
select,
dispatch,
useTransient,
usePersistent,
} ),
[ select, dispatch, useTransient, usePersistent ]
);
};
const useHooks = () => {
const { useTransient } = createHooksForStore( STORE_NAME );
const { fetchTodos, setDismissedTodos, setCompletedTodos, persist } =
useDispatch( STORE_NAME );
// Read-only flags and derived state.
const [ isReady ] = useTransient( 'isReady' );
const { dispatch, select } = useStoreData();
const { fetchTodos, setDismissedTodos, setCompletedTodos } = dispatch;
// Get todos data from store
const { todos, dismissedTodos, completedTodos } = useSelect( ( select ) => {
const store = select( STORE_NAME );
return {
todos: ensureArray( store.getTodos() ),
dismissedTodos: ensureArray( store.getDismissedTodos() ),
completedTodos: ensureArray( store.getCompletedTodos() ),
};
}, [] );
const todos = select.getTodos();
const dismissedTodos = select.getDismissedTodos();
const completedTodos = select.getCompletedTodos();
const dismissedSet = new Set( dismissedTodos );
@ -62,8 +71,6 @@ const useHooks = () => {
);
return {
persist,
isReady,
todos: filteredTodos,
dismissedTodos,
completedTodos,
@ -74,14 +81,21 @@ const useHooks = () => {
};
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
const { select, dispatch, useTransient } = useStoreData();
const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' );
// Load persistent data from REST if not done yet.
if ( ! isReady ) {
select.getTodos();
}
return { persist, refresh, isReady };
};
export const useTodos = () => {
const { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady } =
useHooks();
return { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady };
const { todos, fetchTodos, dismissTodo, setTodoCompleted } = useHooks();
return { todos, fetchTodos, dismissTodo, setTodoCompleted };
};
export const useDismissedTodos = () => {

View file

@ -52,6 +52,21 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( state, payload ),
/**
* Resets state to defaults while maintaining initialization status
*
* @param {Object} state Current state
* @return {Object} Reset state
*/
[ ACTION_TYPES.RESET ]: ( state ) => {
const cleanState = changeTransient(
changePersistent( state, defaultPersistent ),
defaultTransient
);
cleanState.isReady = true; // Keep initialization flag
return cleanState;
},
/**
* Updates todos list
*
@ -99,6 +114,7 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
},
/**
* TODO: This is not used anywhere. Remove "SET_TODOS" and use this resolver instead.
* Initializes persistent state with data from the server
*
* @param {Object} state Current state

View file

@ -11,7 +11,17 @@ const EMPTY_OBJ = Object.freeze( {} );
const EMPTY_ARR = Object.freeze( [] );
const getState = ( state ) => state || EMPTY_OBJ;
const getArray = ( value ) => {
if ( Array.isArray( value ) ) {
return value;
}
if ( value ) {
return Object.values( value );
}
return EMPTY_ARR;
};
// TODO: Implement a persistentData resolver!
export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ;
};
@ -23,15 +33,15 @@ export const transientData = ( state ) => {
export const getTodos = ( state ) => {
const todos = state?.todos || persistentData( state ).todos;
return todos || EMPTY_ARR;
return getArray( todos );
};
export const getDismissedTodos = ( state ) => {
const dismissed =
state?.dismissedTodos || persistentData( state ).dismissedTodos;
return dismissed || EMPTY_ARR;
return getArray( dismissed );
};
export const getCompletedTodos = ( state ) => {
return state?.completedTodos || EMPTY_ARR; // Only look at root state, not persistent data
return getArray( state?.completedTodos ); // Only look at root state, not persistent data
};

View file

@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks, OnboardingHooks } from '../data';
import { useStoreManager } from './useStoreManager';
const PAYPAL_PARTNER_SDK_URL =
'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js';
@ -30,7 +31,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
const { productionOnboardingUrl } = CommonHooks.useProduction();
const products = OnboardingHooks.useDetermineProducts();
const { withActivity, startActivity } = CommonHooks.useBusyState();
const { startActivity } = CommonHooks.useBusyState();
const { authenticateWithOAuth } = CommonHooks.useAuthentication();
const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
const [ scriptLoaded, setScriptLoaded ] = useState( false );
@ -134,7 +135,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
// Ensure the onComplete handler is not removed by a PayPal init script.
timerRef.current = setInterval( addHandler, 250 );
},
[ authenticateWithOAuth, withActivity ]
[ authenticateWithOAuth, startActivity ]
);
const removeCompleteHandler = useCallback( () => {
@ -161,6 +162,7 @@ const useConnectionBase = () => {
useDispatch( noticesStore );
const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
const { withActivity } = CommonHooks.useBusyState();
const { refreshAll } = useStoreManager();
return {
handleFailed: ( res, genericMessage ) => {
@ -178,6 +180,7 @@ const useConnectionBase = () => {
if ( loginSuccessful ) {
createSuccessNotice( MESSAGES.CONNECTED );
await setCompleted( true );
refreshAll();
} else {
createErrorNotice( MESSAGES.LOGIN_FAILED );
}

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

@ -1,74 +0,0 @@
import { useCallback, useMemo } from '@wordpress/element';
import {
CommonHooks,
PayLaterMessagingHooks,
PaymentHooks,
SettingsHooks,
StylingHooks,
TodosHooks,
} from '../data';
export const useSaveSettings = () => {
const { withActivity } = CommonHooks.useBusyState();
const { persist: persistPayment } = PaymentHooks.useStore();
const { persist: persistSettings } = SettingsHooks.useStore();
const { persist: persistStyling } = StylingHooks.useStore();
const { persist: persistTodos } = TodosHooks.useStore();
const { persist: persistPayLaterMessaging } =
PayLaterMessagingHooks.useStore();
const persistActions = useMemo(
() => [
{
key: 'persist-methods',
message: 'Save payment methods',
action: persistPayment,
},
{
key: 'persist-settings',
message: 'Save the settings',
action: persistSettings,
},
{
key: 'persist-styling',
message: 'Save styling details',
action: persistStyling,
},
{
key: 'persist-todos',
message: 'Save todos state',
action: persistTodos,
},
{
key: 'persist-pay-later-messaging',
message: 'Save pay later messaging details',
action: persistPayLaterMessaging,
},
],
[
persistPayLaterMessaging,
persistPayment,
persistSettings,
persistStyling,
persistTodos,
]
);
const persistAll = useCallback( () => {
/**
* Executes onSave on TabPayLaterMessaging component.
*
* Todo: find a better way for this, because it's highly unreliable
* (it only works when the user is still on the "Pay Later Messaging" tab)
*/
document.getElementById( 'configurator-publishButton' )?.click();
persistActions.forEach( ( { key, message, action } ) => {
withActivity( key, message, action );
} );
}, [ persistActions, withActivity ] );
return { persistAll };
};

View file

@ -0,0 +1,76 @@
import { useCallback, useMemo } from '@wordpress/element';
import {
CommonHooks,
PayLaterMessagingHooks,
PaymentHooks,
SettingsHooks,
StylingHooks,
TodosHooks,
} from '../data';
export const useStoreManager = () => {
const { withActivity } = CommonHooks.useBusyState();
const paymentStore = PaymentHooks.useStore();
const settingsStore = SettingsHooks.useStore();
const stylingStore = StylingHooks.useStore();
const todosStore = TodosHooks.useStore();
const payLaterStore = PayLaterMessagingHooks.useStore();
const storeActions = useMemo(
() => [
{
key: 'methods',
message: 'Process payment methods',
store: paymentStore,
},
{
key: 'settings',
message: 'Process the settings',
store: settingsStore,
},
{
key: 'styling',
message: 'Process styling details',
store: stylingStore,
},
{
key: 'todos',
message: 'Process todos state',
store: todosStore,
},
{
key: 'pay-later-messaging',
message: 'Process pay later messaging details',
store: payLaterStore,
},
],
[ payLaterStore, paymentStore, settingsStore, stylingStore, todosStore ]
);
const persistAll = useCallback( () => {
/**
* Executes onSave on TabPayLaterMessaging component.
*
* Todo: find a better way for this, because it's highly unreliable
* (it only works when the user is still on the "Pay Later Messaging" tab)
*/
document.getElementById( 'configurator-publishButton' )?.click();
storeActions.forEach( ( { key, message, store } ) => {
withActivity( `persist-${ key }`, message, store.persist );
} );
}, [ storeActions, withActivity ] );
const refreshAll = useCallback( () => {
storeActions.forEach( ( { key, message, store } ) => {
withActivity( `refresh-${ key }`, message, store.refresh );
} );
}, [ storeActions, withActivity ] );
return {
persistAll,
refreshAll,
};
};

View file

@ -24,7 +24,7 @@ export const learnMoreLinks = {
'https://www.paypal.com/uk/business/paypal-business-fees',
PayPalCheckout:
'https://www.paypal.com/uk/business/accept-payments/checkout',
PayLater:
PayInThree:
'https://www.paypal.com/uk/business/accept-payments/checkout/installments',
},
FR: {

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

@ -10,9 +10,10 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
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\FeaturesSettings;
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;
@ -22,14 +23,12 @@ use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
use WooCommerce\PayPalCommerce\Settings\Endpoint\AuthenticationRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CompleteOnClickEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\FeaturesRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PayLaterMessagingEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\PaymentRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\ResetDismissedTodosEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\WebhookSettingsEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SettingsRestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\StylingRestEndpoint;
@ -40,6 +39,7 @@ use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
use WooCommerce\PayPalCommerce\Settings\Service\SettingsDataManager;
@ -47,9 +47,11 @@ use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
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.
*
@ -60,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' );
@ -80,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' );
@ -120,69 +122,107 @@ return array(
);
},
/**
* Checks if valid merchant connection details are stored in the DB.
* Merchant connection details, which includes the connection status
* (onboarding/connected) and connection-aware environment checks.
* This is the preferred solution to check environment and connection state.
*/
'settings.flag.is-connected' => static function ( ContainerInterface $container ) : bool {
'settings.connection-state' => static function ( ContainerInterface $container ) : ConnectionState {
$data = $container->get( 'settings.data.general' );
assert( $data instanceof GeneralSettings );
return $data->is_merchant_connected();
$is_connected = $data->is_merchant_connected();
$environment = new Environment( $data->is_sandbox_merchant() );
return new ConnectionState( $is_connected, $environment );
},
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
'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 );
return $state->get_environment();
},
/**
* Checks if valid merchant connection details are stored in the DB.
*/
'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`.
*/
$state = $container->get( 'settings.connection-state' );
assert( $state instanceof ConnectionState );
return $state->is_connected();
},
/**
* Checks if the merchant is connected to a sandbox environment.
*/
'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`.
*/
$state = $container->get( 'settings.connection-state' );
assert( $state instanceof ConnectionState );
return $state->is_sandbox();
},
'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',
@ -232,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(
@ -249,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' ),
@ -266,19 +306,21 @@ 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' ),
$container->get( 'api.env.endpoint.login-seller' ),
$container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'settings.connection-state' ),
$container->get( 'api.endpoint.partners' ),
$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' ),
@ -290,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' ),
@ -298,38 +340,36 @@ 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' ),
$container->get( 'settings.rest.settings' )
$container->get( 'settings.rest.settings' ),
$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' )
);
},
'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.todos_eligibilities' => static function ( ContainerInterface $container ) : TodosEligibilityService {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
);
$payment_endpoint = $container->get( 'settings.rest.payment' );
$settings = $payment_endpoint->get_details()->get_data();
'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();
$pay_later_statuses = array(
'cart' => $pay_later_settings['data']['cart']['status'] === 'enabled',
'checkout' => $pay_later_settings['data']['checkout']['status'] === 'enabled',
@ -340,16 +380,41 @@ return array(
$pay_later_settings['data']['custom_placement'][0]['status'] === 'enabled',
);
// Settings status.
$gateways = array(
$is_pay_later_messaging_enabled_for_any_location = ! array_filter( $pay_later_statuses );
return array(
'statuses' => $pay_later_statuses,
'is_enabled_for_any_location' => $is_pay_later_messaging_enabled_for_any_location,
);
},
'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'];
return array(
'cart_enabled' => $styling_data['cart']->enabled ?? false,
'block_checkout_enabled' => $styling_data['expressCheckout']->enabled ?? false,
'product_enabled' => $styling_data['product']->enabled ?? false,
);
},
'settings.service.gateways_status' => static function ( ContainerInterface $container ) : array {
$payment_endpoint = $container->get( 'settings.rest.payment' );
$settings = $payment_endpoint->get_details()->get_data();
return array(
'apple_pay' => $settings['data']['ppcp-applepay']['enabled'] ?? false,
'google_pay' => $settings['data']['ppcp-googlepay']['enabled'] ?? false,
'axo' => $settings['data']['ppcp-axo-gateway']['enabled'] ?? false,
'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
);
},
'settings.service.merchant_capabilities' => static function ( ContainerInterface $container ) : array {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
);
// Merchant eligibility.
$capabilities = array(
return array(
'apple_pay' => $features['apple_pay']['enabled'] ?? false,
'google_pay' => $features['google_pay']['enabled'] ?? false,
'acdc' => $features['advanced_credit_and_debit_cards']['enabled'] ?? false,
@ -357,25 +422,61 @@ return array(
'apm' => $features['alternative_payment_methods']['enabled'] ?? false,
'paylater' => $features['pay_later_messaging']['enabled'] ?? false,
);
},
$is_pay_later_messaging_enabled_for_any_location = ! array_filter( $pay_later_statuses );
'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'];
$button_locations = $container->get( 'settings.service.button_locations' );
$gateways = $container->get( 'settings.service.gateways_status' );
$capabilities = $container->get( 'settings.service.merchant_capabilities' );
/**
* Initializes TodosEligibilityService with eligibility conditions for various PayPal features.
* Each parameter determines whether a specific feature should be shown in the Things To Do list.
*
* Logic relies on three main factors:
* 1. $container->get( 'x.eligible' ) - Module based eligibility check, usually whether the WooCommerce store is using a supported country/currency matrix.
* 2. $capabilities - Whether the merchant is eligible for specific features on their PayPal account.
* 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_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.
* @param bool $is_pay_later_messaging_checkout_eligible - Show if Pay Later is not enabled anywhere and specifically not on checkout.
* @param bool $is_subscription_eligible - Show if WooCommerce Subscriptions plugin is active but merchant is not eligible for PayPal Vaulting.
* @param bool $is_paypal_buttons_cart_eligible - Show if PayPal buttons are not enabled on cart page.
* @param bool $is_paypal_buttons_block_checkout_eligible - Show if PayPal buttons are not enabled on blocks checkout.
* @param bool $is_paypal_buttons_product_eligible - Show if PayPal buttons are not enabled on product page.
* @param bool $is_apple_pay_domain_eligible - Show if merchant has Apple Pay capability on PayPal account.
* @param bool $is_digital_wallet_eligible - Show if merchant is eligible (ACDC) but doesn't have both wallet types on PayPal.
* @param bool $is_apple_pay_eligible - Show if merchant is eligible (ACDC) but doesn't have Apple Pay on PayPal.
* @param bool $is_google_pay_eligible - Show if merchant is eligible (ACDC) but doesn't have Google Pay on PayPal.
* @param bool $is_enable_apple_pay_eligible - Show if merchant has Apple Pay capability but hasn't enabled the gateway.
* @param bool $is_enable_google_pay_eligible - Show if merchant has Google Pay capability but hasn't enabled the gateway.
*/
return new TodosEligibilityService(
$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).
! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['checkout'], // Add Pay Later messaging (Checkout).
true, // Configure a PayPal Subscription.
true, // Add PayPal buttons.
true, // Register Domain for Apple Pay.
$capabilities['acdc'] && ! ( $capabilities['apple_pay'] && $capabilities['google_pay'] ), // Add digital wallets to your account.
$capabilities['acdc'] && ! $capabilities['apple_pay'], // Add Apple Pay to your account.
$capabilities['acdc'] && ! $capabilities['google_pay'], // Add Google Pay to your account.
true, // Configure a PayPal Subscription.
$capabilities['apple_pay'] && ! $gateways['apple_pay'], // Enable Apple Pay.
$capabilities['google_pay'] && ! $gateways['google_pay'], // Enable Google Pay.
$container->get( 'axo.eligible' ) && $capabilities['acdc'] && ! $gateways['axo'], // Enable Fastlane.
$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).
! $is_pay_later_messaging_enabled_for_any_location && ! $pay_later_statuses['checkout'], // Add Pay Later messaging (Checkout).
$container->has( 'save-payment-methods.eligible' ) &&
! $container->get( 'save-payment-methods.eligible' ) &&
$container->has( 'wc-subscriptions.helper' ) &&
$container->get( 'wc-subscriptions.helper' )->plugin_is_active(), // Configure a PayPal Subscription.
! $button_locations['cart_enabled'], // Add PayPal buttons to cart.
! $button_locations['block_checkout_enabled'], // Add PayPal buttons to block checkout.
! $button_locations['product_enabled'], // Add PayPal buttons to product.
$capabilities['apple_pay'], // Register Domain for Apple Pay.
$capabilities['acdc'] && ! ( $capabilities['apple_pay'] && $capabilities['google_pay'] ), // Add digital wallets to your account.
$container->get( 'applepay.eligible' ) && $capabilities['acdc'] && ! $capabilities['apple_pay'], // Add Apple Pay to your account.
$container->get( 'googlepay.eligible' ) && $capabilities['acdc'] && ! $capabilities['google_pay'], // Add Google Pay to your account.
$container->get( 'applepay.eligible' ) && $capabilities['apple_pay'] && ! $gateways['apple_pay'], // Enable Apple Pay.
$container->get( 'googlepay.eligible' ) && $capabilities['google_pay'] && ! $gateways['google_pay'],
);
},
'settings.rest.features' => static function ( ContainerInterface $container ) : FeaturesRestEndpoint {
@ -385,12 +486,6 @@ return array(
);
},
'settings.data.definition.features' => static function ( ContainerInterface $container ) : FeaturesDefinition {
return new FeaturesDefinition(
$container->get( 'settings.service.features_eligibilities' ),
$container->get( 'settings.data.general' )
);
},
'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
$features = apply_filters(
'woocommerce_paypal_payments_rest_common_merchant_features',
array()
@ -403,8 +498,7 @@ return array(
$gateways = array(
'card-button' => $settings['data']['ppcp-card-button-gateway']['enabled'] ?? false,
);
// Merchant eligibility.
// Merchant capabilities, serve to show active or inactive badge and buttons.
$capabilities = array(
'apple_pay' => $features['apple_pay']['enabled'] ?? false,
'google_pay' => $features['google_pay']['enabled'] ?? false,
@ -413,14 +507,37 @@ return array(
'apm' => $features['alternative_payment_methods']['enabled'] ?? false,
'paylater' => $features['pay_later_messaging']['enabled'] ?? false,
);
$merchant_capabilities = array(
'save_paypal' => $capabilities['save_paypal'], // Save PayPal and Venmo eligibility.
'acdc' => $capabilities['acdc'] && ! $gateways['card-button'], // Advanced credit and debit cards eligibility.
'apm' => $capabilities['apm'], // Alternative payment methods eligibility.
'google_pay' => $capabilities['acdc'] && ! $capabilities['google_pay'], // Google Pay eligibility.
'apple_pay' => $capabilities['acdc'] && ! $capabilities['apple_pay'], // Apple Pay eligibility.
'pay_later' => $capabilities['paylater'],
);
return new FeaturesDefinition(
$container->get( 'settings.service.features_eligibilities' ),
$container->get( 'settings.data.general' ),
$merchant_capabilities
);
},
'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
$messages_apply = $container->get( 'button.helper.messages-apply' );
assert( $messages_apply instanceof MessagesApply );
$pay_later_eligible = $messages_apply->for_country();
$merchant_country = $container->get( 'api.shop.country' );
$ineligible_countries = array( 'RU', 'BR', 'JP' );
$apm_eligible = ! in_array( $merchant_country, $ineligible_countries, true );
return new FeaturesEligibilityService(
$capabilities['save_paypal'], // Save PayPal and Venmo eligibility.
$capabilities['acdc'] && ! $gateways['card-button'], // Advanced credit and debit cards eligibility.
$capabilities['apm'], // Alternative payment methods eligibility.
$capabilities['acdc'] && $capabilities['google_pay'], // Google Pay eligibility.
$capabilities['acdc'] && $capabilities['apple_pay'], // Apple Pay eligibility.
$capabilities['paylater'], // Pay Later eligibility.
$container->get( 'save-payment-methods.eligible' ), // Save PayPal and Venmo eligibility.
$container->get( 'card-fields.eligible' ), // Advanced credit and debit cards eligibility.
$apm_eligible, // Alternative payment methods eligibility.
$container->get( 'googlepay.eligible' ), // Google Pay eligibility.
$container->get( 'applepay.eligible' ), // Apple Pay eligibility.
$pay_later_eligible, // Pay Later eligibility.
);
},
);

View file

@ -52,6 +52,13 @@ class MerchantConnectionDTO {
*/
public string $merchant_email = '';
/**
* Merchant's country.
*
* @var string
*/
public string $merchant_country = '';
/**
* Whether the merchant is a business or personal account.
* Possible values: ['business'|'personal'|'unknown']
@ -68,6 +75,7 @@ class MerchantConnectionDTO {
* @param string $client_secret API client secret.
* @param string $merchant_id PayPal's 13-character merchant ID.
* @param string $merchant_email Email address of the merchant account.
* @param string $merchant_country Merchant's country.
* @param string $seller_type Whether the merchant is a business or personal account.
*/
public function __construct(
@ -76,13 +84,15 @@ class MerchantConnectionDTO {
string $client_secret,
string $merchant_id,
string $merchant_email,
string $merchant_country,
string $seller_type = SellerTypeEnum::UNKNOWN
) {
$this->is_sandbox = $is_sandbox;
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->merchant_id = $merchant_id;
$this->merchant_email = $merchant_email;
$this->seller_type = $seller_type;
$this->is_sandbox = $is_sandbox;
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->merchant_id = $merchant_id;
$this->merchant_email = $merchant_email;
$this->merchant_country = $merchant_country;
$this->seller_type = $seller_type;
}
}

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

@ -35,18 +35,28 @@ class FeaturesDefinition {
*/
protected GeneralSettings $settings;
/**
* The merchant capabilities.
*
* @var array
*/
protected array $merchant_capabilities;
/**
* Constructor.
*
* @param FeaturesEligibilityService $eligibilities The features eligibility service.
* @param GeneralSettings $settings The general settings service.
* @param array $merchant_capabilities The merchant capabilities.
*/
public function __construct(
FeaturesEligibilityService $eligibilities,
GeneralSettings $settings
GeneralSettings $settings,
array $merchant_capabilities
) {
$this->eligibilities = $eligibilities;
$this->settings = $settings;
$this->eligibilities = $eligibilities;
$this->settings = $settings;
$this->merchant_capabilities = $merchant_capabilities;
}
/**
@ -55,7 +65,23 @@ class FeaturesDefinition {
* @return array The array of feature definitions.
*/
public function get(): array {
$all_features = $this->all_available_features();
$eligible_features = array();
$eligibility_checks = $this->eligibilities->get_eligibility_checks();
foreach ( $all_features as $feature_key => $feature ) {
if ( $eligibility_checks[ $feature_key ]() ) {
$eligible_features[ $feature_key ] = $feature;
}
}
return $eligible_features;
}
/**
* Returns all available features.
*
* @return array[] The array of all available features.
*/
public function all_available_features(): array {
$paylater_countries = array(
'UK',
'ES',
@ -72,15 +98,14 @@ class FeaturesDefinition {
'save_paypal_and_venmo' => array(
'title' => __( 'Save PayPal and Venmo', 'woocommerce-paypal-payments' ),
'description' => __( 'Securely save PayPal and Venmo payment methods for subscriptions or return buyers.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['save_paypal_and_venmo'],
'enabled' => $this->merchant_capabilities['save_paypal'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'settings',
'section' => 'ppcp--save-payment-methods',
'type' => 'tab',
'tab' => 'settings',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -106,16 +131,17 @@ class FeaturesDefinition {
'advanced_credit_and_debit_cards' => array(
'title' => __( 'Advanced Credit and Debit Cards', 'woocommerce-paypal-payments' ),
'description' => __( 'Process major credit and debit cards including Visa, Mastercard, American Express and Discover.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['advanced_credit_and_debit_cards'],
'enabled' => $this->merchant_capabilities['acdc'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'modal' => 'ppcp-credit-card-gateway',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-credit-card-gateway',
'highlight' => 'ppcp-credit-card-gateway',
'modal' => 'ppcp-credit-card-gateway',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -141,15 +167,16 @@ class FeaturesDefinition {
'alternative_payment_methods' => array(
'title' => __( 'Alternative Payment Methods', 'woocommerce-paypal-payments' ),
'description' => __( 'Offer global, country-specific payment options for your customers.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['alternative_payment_methods'],
'enabled' => $this->merchant_capabilities['apm'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-alternative-payments-card',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-alternative-payments-card',
'highlight' => 'ppcp-alternative-payments-card',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -172,16 +199,17 @@ class FeaturesDefinition {
'google_pay' => array(
'title' => __( 'Google Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Let customers pay using their Google Pay wallet.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['google_pay'],
'enabled' => $this->merchant_capabilities['google_pay'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'modal' => 'ppcp-googlepay',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'highlight' => 'ppcp-googlepay',
'modal' => 'ppcp-googlepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -210,16 +238,17 @@ class FeaturesDefinition {
'apple_pay' => array(
'title' => __( 'Apple Pay', 'woocommerce-paypal-payments' ),
'description' => __( 'Let customers pay using their Apple Pay wallet.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['apple_pay'],
'enabled' => $this->merchant_capabilities['apple_pay'],
'buttons' => array(
array(
'type' => 'secondary',
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
'action' => array(
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'modal' => 'ppcp-applepay',
'type' => 'tab',
'tab' => 'payment_methods',
'section' => 'ppcp-card-payments-card',
'highlight' => 'ppcp-applepay',
'modal' => 'ppcp-applepay',
),
'showWhen' => 'enabled',
'class' => 'small-button',
@ -258,7 +287,7 @@ class FeaturesDefinition {
'Let customers know they can buy now and pay later with PayPal. Adding this messaging can boost conversion rates and increase cart sizes by 39%¹, with no extra cost to you—plus, you get paid up front.',
'woocommerce-paypal-payments'
),
'isEligible' => $eligibility_checks['pay_later'],
'enabled' => $this->merchant_capabilities['pay_later'],
'buttons' => array(
array(
'type' => 'secondary',
@ -273,7 +302,7 @@ class FeaturesDefinition {
array(
'type' => 'tertiary',
'text' => __( 'Learn more', 'woocommerce-paypal-payments' ),
'url' => "https://www.paypal.com/{$country_location}/business/accept-payments/checkout/installments",
'url' => "https://www.paypal.com/$country_location/business/accept-payments/checkout/installments",
'class' => 'small-button',
),
),

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

@ -1,6 +1,6 @@
<?php
/**
* PayPal Commerce Todos Definitions
* Todos Definitions
*
* @package WooCommerce\PayPalCommerce\Settings\Data\Definition
*/
@ -69,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' ),
@ -126,15 +114,36 @@ class TodosDefinition {
'description' => __( 'Connect a subscriptions-type product from WooCommerce with PayPal', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['configure_paypal_subscription'],
'action' => array(
'type' => 'external',
'url' => admin_url( 'edit.php?post_type=product&product_type=subscription' ),
'type' => 'external',
'url' => 'https://woocommerce.com/document/woocommerce-paypal-payments/#paypal-subscriptions',
'completeOnClick' => true,
),
'priority' => 5,
),
'add_paypal_buttons' => array(
'title' => __( 'Add PayPal buttons', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow customers to check out quickly and securely from the <x> page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_paypal_buttons'],
'add_paypal_buttons_cart' => array(
'title' => __( 'Add PayPal buttons to the Cart page', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow customers to check out quickly and securely from the Cart page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_paypal_buttons_cart'],
'action' => array(
'type' => 'tab',
'tab' => 'styling',
),
'priority' => 6,
),
'add_paypal_buttons_block_checkout' => array(
'title' => __( 'Add PayPal buttons to the Express Checkout page', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow customers to check out quickly and securely from the Express Checkout page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_paypal_buttons_block_checkout'],
'action' => array(
'type' => 'tab',
'tab' => 'styling',
),
'priority' => 6,
),
'add_paypal_buttons_product' => array(
'title' => __( 'Add PayPal buttons to the Product page', 'woocommerce-paypal-payments' ),
'description' => __( 'Allow customers to check out quickly and securely from the Product page. Customers save time and get through checkout in fewer clicks.', 'woocommerce-paypal-payments' ),
'isEligible' => $eligibility_checks['add_paypal_buttons_product'],
'action' => array(
'type' => 'tab',
'tab' => 'styling',

View file

@ -74,6 +74,7 @@ class GeneralSettings extends AbstractDataModel {
'sandbox_merchant' => false,
'merchant_id' => '',
'merchant_email' => '',
'merchant_country' => '',
'client_id' => '',
'client_secret' => '',
'seller_type' => 'unknown',
@ -138,6 +139,7 @@ class GeneralSettings extends AbstractDataModel {
$this->data['sandbox_merchant'] = $connection->is_sandbox;
$this->data['merchant_id'] = sanitize_text_field( $connection->merchant_id );
$this->data['merchant_email'] = sanitize_email( $connection->merchant_email );
$this->data['merchant_country'] = sanitize_text_field( $connection->merchant_country );
$this->data['client_id'] = sanitize_text_field( $connection->client_id );
$this->data['client_secret'] = sanitize_text_field( $connection->client_secret );
$this->data['seller_type'] = sanitize_text_field( $connection->seller_type );
@ -156,6 +158,7 @@ class GeneralSettings extends AbstractDataModel {
$this->data['client_secret'],
$this->data['merchant_id'],
$this->data['merchant_email'],
$this->data['merchant_country'],
$this->data['seller_type']
);
}
@ -171,6 +174,7 @@ class GeneralSettings extends AbstractDataModel {
$this->data['sandbox_merchant'] = $defaults['sandbox_merchant'];
$this->data['merchant_id'] = $defaults['merchant_id'];
$this->data['merchant_email'] = $defaults['merchant_email'];
$this->data['merchant_country'] = $defaults['merchant_country'];
$this->data['client_id'] = $defaults['client_id'];
$this->data['client_secret'] = $defaults['client_secret'];
$this->data['seller_type'] = $defaults['seller_type'];
@ -239,4 +243,13 @@ class GeneralSettings extends AbstractDataModel {
public function get_merchant_email() : string {
return $this->data['merchant_email'];
}
/**
* Gets the currently connected merchant's country.
*
* @return string
*/
public function get_merchant_country() : string {
return $this->data['merchant_country'];
}
}

View file

@ -84,18 +84,9 @@ class FeaturesRestEndpoint extends RestEndpoint {
public function get_features(): WP_REST_Response {
$features = array();
foreach ( $this->features_definition->get() as $id => $feature ) {
// Evaluate eligibility check.
if ( is_callable( $feature['isEligible'] ) ) {
$is_eligible = $feature['isEligible']();
} else {
$is_eligible = (bool) $feature['isEligible'];
}
// Include all features with their eligibility state.
$features[] = array_merge(
array( 'id' => $id ),
array_diff_key( $feature, array( 'isEligible' => true ) ),
array( 'isEligible' => $is_eligible )
$feature
);
}

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

@ -17,6 +17,7 @@ use WP_REST_Response;
use WP_REST_Request;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
use WooCommerce\PayPalCommerce\Settings\Data\Definition\TodosDefinition;
use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService;
/**
* REST controller for the "Things To Do" items in the Overview tab.
@ -33,17 +34,6 @@ class TodosRestEndpoint extends RestEndpoint {
*/
protected $rest_base = 'todos';
/**
* Pay Later messaging todo IDs in priority order.
*
* @var array
*/
private const PAY_LATER_IDS = array(
'add_pay_later_messaging_product_page',
'add_pay_later_messaging_cart',
'add_pay_later_messaging_checkout',
);
/**
* The todos model instance.
*
@ -65,21 +55,31 @@ class TodosRestEndpoint extends RestEndpoint {
*/
protected SettingsRestEndpoint $settings;
/**
* The todos sorting service.
*
* @var TodosSortingAndFilteringService
*/
protected TodosSortingAndFilteringService $sorting_service;
/**
* TodosRestEndpoint constructor.
*
* @param TodosModel $todos The todos model instance.
* @param TodosDefinition $todos_definition The todos definition instance.
* @param SettingsRestEndpoint $settings The settings endpoint instance.
* @param TodosModel $todos The todos model instance.
* @param TodosDefinition $todos_definition The todos definition instance.
* @param SettingsRestEndpoint $settings The settings endpoint instance.
* @param TodosSortingAndFilteringService $sorting_service The todos sorting service.
*/
public function __construct(
TodosModel $todos,
TodosDefinition $todos_definition,
SettingsRestEndpoint $settings
SettingsRestEndpoint $settings,
TodosSortingAndFilteringService $sorting_service
) {
$this->todos = $todos;
$this->todos_definition = $todos_definition;
$this->settings = $settings;
$this->sorting_service = $sorting_service;
}
/**
@ -157,8 +157,7 @@ class TodosRestEndpoint extends RestEndpoint {
}
}
$sorted_todos = $this->sort_todos_by_priority( $todos );
$filtered_todos = $this->filter_pay_later_todos( $sorted_todos );
$filtered_todos = $this->sorting_service->apply_all_priority_filters( $todos );
return $this->return_success(
array(
@ -245,67 +244,4 @@ class TodosRestEndpoint extends RestEndpoint {
);
}
}
/**
* Filters Pay Later messaging todos to show only the highest priority eligible todo.
*
* @param array $todos The array of todos to filter.
* @return array Filtered todos with only one Pay Later messaging todo.
*/
private function filter_pay_later_todos( array $todos ): array {
$pay_later_todos = array_filter(
$todos,
function( $todo ) {
return in_array( $todo['id'], self::PAY_LATER_IDS, true );
}
);
$other_todos = array_filter(
$todos,
function( $todo ) {
return ! in_array( $todo['id'], self::PAY_LATER_IDS, true );
}
);
// Find the highest priority Pay Later todo that's eligible.
$priority_pay_later_todo = null;
foreach ( self::PAY_LATER_IDS as $pay_later_id ) {
$matching_todo = current(
array_filter(
$pay_later_todos,
function( $todo ) use ( $pay_later_id ) {
return $todo['id'] === $pay_later_id;
}
)
);
if ( $matching_todo ) {
$priority_pay_later_todo = $matching_todo;
break;
}
}
return $priority_pay_later_todo
? array_merge( $other_todos, array( $priority_pay_later_todo ) )
: $other_todos;
}
/**
* Sorts todos by their priority value.
*
* @param array $todos Array of todos to sort.
* @return array Sorted array of todos.
*/
private function sort_todos_by_priority( array $todos ): array {
usort(
$todos,
function( $a, $b ) {
$priority_a = $a['priority'] ?? 999;
$priority_b = $b['priority'] ?? 999;
return $priority_a <=> $priority_b;
}
);
return $todos;
}
}

View file

@ -12,6 +12,8 @@ namespace WooCommerce\PayPalCommerce\Settings\Service;
use JsonException;
use Throwable;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
@ -24,6 +26,7 @@ use WooCommerce\WooCommerce\Logging\Logger\NullLogger;
use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum;
use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
/**
* Class that manages the connection to PayPal.
@ -64,27 +67,47 @@ class AuthenticationManager {
*/
private PartnerReferralsData $referrals_data;
/**
* The connection state manager.
*
* @var ConnectionState
*/
private ConnectionState $connection_state;
/**
* Partners endpoint.
*
* @var PartnersEndpoint
*/
private PartnersEndpoint $partners_endpoint;
/**
* Constructor.
*
* @param GeneralSettings $common_settings Data model that stores the connection details.
* @param EnvironmentConfig $connection_host API host for direct authentication.
* @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials.
* @param PartnerReferralsData $referrals_data Partner referrals data.
* @param ?LoggerInterface $logger Logging instance.
* @param GeneralSettings $common_settings Data model that stores the connection details.
* @param EnvironmentConfig $connection_host API host for direct authentication.
* @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials.
* @param PartnerReferralsData $referrals_data Partner referrals data.
* @param ConnectionState $connection_state Connection state manager.
* @param PartnersEndpoint $partners_endpoint Partners endpoint.
* @param ?LoggerInterface $logger Logging instance.
*/
public function __construct(
GeneralSettings $common_settings,
EnvironmentConfig $connection_host,
EnvironmentConfig $login_endpoint,
PartnerReferralsData $referrals_data,
ConnectionState $connection_state,
PartnersEndpoint $partners_endpoint,
?LoggerInterface $logger = null
) {
$this->common_settings = $common_settings;
$this->connection_host = $connection_host;
$this->login_endpoint = $login_endpoint;
$this->referrals_data = $referrals_data;
$this->logger = $logger ?: new NullLogger();
$this->common_settings = $common_settings;
$this->connection_host = $connection_host;
$this->login_endpoint = $login_endpoint;
$this->referrals_data = $referrals_data;
$this->connection_state = $connection_state;
$this->partners_endpoint = $partners_endpoint;
$this->logger = $logger ?: new NullLogger();
}
/**
@ -112,6 +135,9 @@ class AuthenticationManager {
$this->common_settings->reset_merchant_data();
$this->common_settings->save();
// Update the connection status and clear the environment flags.
$this->connection_state->disconnect();
/**
* Broadcast, that the plugin disconnected from PayPal. This allows other
* modules to clean up merchant-related details, such as eligibility flags.
@ -182,12 +208,19 @@ class AuthenticationManager {
$payee = $this->request_payee( $client_id, $client_secret, $use_sandbox );
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( PayPalApiException $exception ) {
$seller_status = null;
}
$connection = new MerchantConnectionDTO(
$use_sandbox,
$client_id,
$client_secret,
$payee['merchant_id'],
$payee['email_address'],
! is_null( $seller_status ) ? $seller_status->country() : '',
SellerTypeEnum::BUSINESS
);
@ -253,10 +286,17 @@ class AuthenticationManager {
*/
$connection = $this->common_settings->get_merchant_data();
$connection->is_sandbox = $use_sandbox;
$connection->client_id = $credentials['client_id'];
$connection->client_secret = $credentials['client_secret'];
$connection->merchant_id = $credentials['merchant_id'];
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( PayPalApiException $exception ) {
$seller_status = null;
}
$connection->is_sandbox = $use_sandbox;
$connection->client_id = $credentials['client_id'];
$connection->client_secret = $credentials['client_secret'];
$connection->merchant_id = $credentials['merchant_id'];
$connection->merchant_country = ! is_null( $seller_status ) ? $seller_status->country() : '';
$this->update_connection_details( $connection );
}
@ -292,6 +332,13 @@ class AuthenticationManager {
$connection->seller_type = $seller_type;
}
try {
$seller_status = $this->partners_endpoint->seller_status();
} catch ( PayPalApiException $exception ) {
$seller_status = null;
}
$connection->merchant_country = ! is_null( $seller_status ) ? $seller_status->country() : '';
$this->update_connection_details( $connection );
}
@ -420,6 +467,9 @@ class AuthenticationManager {
if ( $this->common_settings->is_merchant_connected() ) {
$this->logger->info( 'Merchant successfully connected to PayPal' );
// Update the connection status and set the environment flags.
$this->connection_state->connect( $connection->is_sandbox );
/**
* Request to flush caches before authenticating the merchant, to
* ensure the new merchant does not use stale data from previous

View file

@ -211,7 +211,8 @@ class SettingsDataManager {
$this->payment_methods->toggle_method_state( $method['id'], false );
}
// Always enable Venmo and Pay Later.
// Always enable PayPal, Venmo and Pay Later.
$this->payment_methods->toggle_method_state( PayPalGateway::ID, true );
$this->payment_methods->toggle_method_state( 'venmo', true );
$this->payment_methods->toggle_method_state( 'pay-later', true );
@ -224,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

@ -1,9 +1,9 @@
<?php
/**
* PayPal Commerce eligibility service for WooCommerce.
* Eligibility service for Todos.
*
* This file contains the TodosEligibilityService class which manages eligibility checks
* for various PayPal Commerce features including Fastlane, card payments, Pay Later messaging,
* for various features including Fastlane, card payments, Pay Later messaging,
* subscriptions, Apple Pay, Google Pay, and other digital wallet features.
*
* @package WooCommerce\PayPalCommerce\Settings\Service
@ -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.
*
@ -66,6 +59,27 @@ class TodosEligibilityService {
*/
private bool $is_subscription_eligible;
/**
* Whether PayPal buttons for cart are eligible.
*
* @var bool
*/
private bool $is_paypal_buttons_cart_eligible;
/**
* Whether PayPal buttons for block checkout are eligible.
*
* @var bool
*/
private bool $is_paypal_buttons_block_checkout_eligible;
/**
* Whether PayPal buttons for product page are eligible.
*
* @var bool
*/
private bool $is_paypal_buttons_product_eligible;
/**
* Whether Apple Pay domain registration is eligible.
*
@ -94,20 +108,6 @@ class TodosEligibilityService {
*/
private bool $is_google_pay_eligible;
/**
* Whether PayPal buttons are eligible.
*
* @var bool
*/
private bool $is_paypal_buttons_eligible;
/**
* Whether PayPal subscription configuration is eligible.
*
* @var bool
*/
private bool $is_add_subscription_eligible;
/**
* Whether enabling Apple Pay is eligible.
*
@ -126,53 +126,53 @@ 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.
* @param bool $is_pay_later_messaging_checkout_eligible Whether Pay Later messaging for checkout is eligible.
* @param bool $is_subscription_eligible Whether subscriptions are eligible.
* @param bool $is_paypal_buttons_eligible Whether PayPal buttons are eligible.
* @param bool $is_paypal_buttons_cart_eligible Whether PayPal buttons for cart are eligible.
* @param bool $is_paypal_buttons_block_checkout_eligible Whether PayPal buttons for block checkout are eligible.
* @param bool $is_paypal_buttons_product_eligible Whether PayPal buttons for product page are eligible.
* @param bool $is_apple_pay_domain_eligible Whether Apple Pay domain registration is eligible.
* @param bool $is_digital_wallet_eligible Whether digital wallet features are eligible.
* @param bool $is_apple_pay_eligible Whether Apple Pay is eligible.
* @param bool $is_google_pay_eligible Whether Google Pay is eligible.
* @param bool $is_add_subscription_eligible Whether PayPal subscription configuration is eligible.
* @param bool $is_enable_apple_pay_eligible Whether enabling Apple Pay is eligible.
* @param bool $is_enable_google_pay_eligible Whether enabling Google Pay is eligible.
*/
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,
bool $is_pay_later_messaging_checkout_eligible,
bool $is_subscription_eligible,
bool $is_paypal_buttons_eligible,
bool $is_paypal_buttons_cart_eligible,
bool $is_paypal_buttons_block_checkout_eligible,
bool $is_paypal_buttons_product_eligible,
bool $is_apple_pay_domain_eligible,
bool $is_digital_wallet_eligible,
bool $is_apple_pay_eligible,
bool $is_google_pay_eligible,
bool $is_add_subscription_eligible,
bool $is_enable_apple_pay_eligible,
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;
$this->is_pay_later_messaging_checkout_eligible = $is_pay_later_messaging_checkout_eligible;
$this->is_subscription_eligible = $is_subscription_eligible;
$this->is_paypal_buttons_eligible = $is_paypal_buttons_eligible;
$this->is_apple_pay_domain_eligible = $is_apple_pay_domain_eligible;
$this->is_digital_wallet_eligible = $is_digital_wallet_eligible;
$this->is_apple_pay_eligible = $is_apple_pay_eligible;
$this->is_google_pay_eligible = $is_google_pay_eligible;
$this->is_add_subscription_eligible = $is_add_subscription_eligible;
$this->is_enable_apple_pay_eligible = $is_enable_apple_pay_eligible;
$this->is_enable_google_pay_eligible = $is_enable_google_pay_eligible;
$this->is_fastlane_eligible = $is_fastlane_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;
$this->is_pay_later_messaging_checkout_eligible = $is_pay_later_messaging_checkout_eligible;
$this->is_subscription_eligible = $is_subscription_eligible;
$this->is_paypal_buttons_cart_eligible = $is_paypal_buttons_cart_eligible;
$this->is_paypal_buttons_block_checkout_eligible = $is_paypal_buttons_block_checkout_eligible;
$this->is_paypal_buttons_product_eligible = $is_paypal_buttons_product_eligible;
$this->is_apple_pay_domain_eligible = $is_apple_pay_domain_eligible;
$this->is_digital_wallet_eligible = $is_digital_wallet_eligible;
$this->is_apple_pay_eligible = $is_apple_pay_eligible;
$this->is_google_pay_eligible = $is_google_pay_eligible;
$this->is_enable_apple_pay_eligible = $is_enable_apple_pay_eligible;
$this->is_enable_google_pay_eligible = $is_enable_google_pay_eligible;
}
/**
@ -183,13 +183,14 @@ 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,
'add_pay_later_messaging_checkout' => fn() => $this->is_pay_later_messaging_checkout_eligible,
'configure_paypal_subscription' => fn() => $this->is_subscription_eligible,
'add_paypal_buttons' => fn() => $this->is_paypal_buttons_eligible,
'add_paypal_buttons_cart' => fn() => $this->is_paypal_buttons_cart_eligible,
'add_paypal_buttons_block_checkout' => fn() => $this->is_paypal_buttons_block_checkout_eligible,
'add_paypal_buttons_product' => fn() => $this->is_paypal_buttons_product_eligible,
'register_domain_apple_pay' => fn() => $this->is_apple_pay_domain_eligible,
'add_digital_wallets' => fn() => $this->is_digital_wallet_eligible,
'add_apple_pay' => fn() => $this->is_apple_pay_eligible,

View file

@ -0,0 +1,182 @@
<?php
/**
* Service for sorting and filtering todo items.
*
* @package WooCommerce\PayPalCommerce\Settings\Service
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Service;
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
/**
* Service class that provides todo sorting and filtering functionality.
*/
class TodosSortingAndFilteringService {
/**
* Pay Later messaging todo IDs in priority order.
*
* @var array
*/
private const PAY_LATER_IDS = array(
'add_pay_later_messaging_product_page',
'add_pay_later_messaging_cart',
'add_pay_later_messaging_checkout',
);
/**
* Button placement todo IDs in priority order.
*
* @var array
*/
private const BUTTON_PLACEMENT_IDS = array(
'add_paypal_buttons_cart',
'add_paypal_buttons_block_checkout',
'add_paypal_buttons_product',
);
/**
* The TodosModel instance.
*
* @var TodosModel
*/
private TodosModel $todos_model;
/**
* Constructor.
*
* @param TodosModel $todos_model The TodosModel instance.
*/
public function __construct( TodosModel $todos_model ) {
$this->todos_model = $todos_model;
}
/**
* Returns Pay Later messaging todo IDs in priority order.
*
* @return array Pay Later messaging todo IDs.
*/
public function get_pay_later_ids(): array {
return self::PAY_LATER_IDS;
}
/**
* Returns Button Placement todo IDs in priority order.
*
* @return array Button Placement todo IDs.
*/
public function get_button_placement_ids(): array {
return self::BUTTON_PLACEMENT_IDS;
}
/**
* Sorts todos by their priority value.
*
* @param array $todos Array of todos to sort.
* @return array Sorted array of todos.
*/
public function sort_todos_by_priority( array $todos ): array {
usort(
$todos,
function( $a, $b ) {
$priority_a = $a['priority'] ?? 999;
$priority_b = $b['priority'] ?? 999;
return $priority_a <=> $priority_b;
}
);
return $todos;
}
/**
* Filters a group of todos to show only the highest priority one.
* Takes into account dismissed todos.
*
* @param array $todos The array of todos to filter.
* @param array $group_ids Array of todo IDs in priority order.
* @return array Filtered todos with only one todo from the specified group.
*/
public function filter_highest_priority_todo( array $todos, array $group_ids ): array {
$dismissed_todos = $this->todos_model->get_dismissed_todos();
$group_todos = array_filter(
$todos,
function( $todo ) use ( $group_ids ) {
return in_array( $todo['id'], $group_ids, true );
}
);
$other_todos = array_filter(
$todos,
function( $todo ) use ( $group_ids ) {
return ! in_array( $todo['id'], $group_ids, true );
}
);
// Find the highest priority todo from the group that's eligible AND not dismissed.
$priority_todo = null;
foreach ( $group_ids as $todo_id ) {
// Skip if this todo ID is dismissed.
if ( in_array( $todo_id, $dismissed_todos, true ) ) {
continue;
}
$matching_todo = current(
array_filter(
$group_todos,
function( $todo ) use ( $todo_id ) {
return $todo['id'] === $todo_id;
}
)
);
if ( $matching_todo ) {
$priority_todo = $matching_todo;
break;
}
}
return $priority_todo
? array_merge( $other_todos, array( $priority_todo ) )
: $other_todos;
}
/**
* Filter pay later todos to show only the highest priority eligible one.
*
* @param array $todos The array of todos to filter.
* @return array Filtered todos.
*/
public function filter_pay_later_todos( array $todos ): array {
return $this->filter_highest_priority_todo( $todos, self::PAY_LATER_IDS );
}
/**
* Filter button placement todos to show only the highest priority eligible one.
*
* @param array $todos The array of todos to filter.
* @return array Filtered todos.
*/
public function filter_button_placement_todos( array $todos ): array {
return $this->filter_highest_priority_todo( $todos, self::BUTTON_PLACEMENT_IDS );
}
/**
* Apply all priority filters to the todos list.
*
* This method applies sorting and all priority filtering in the correct order.
*
* @param array $todos The original todos array.
* @return array Fully filtered and sorted todos.
*/
public function apply_all_priority_filters( array $todos ): array {
$sorted_todos = $this->sort_todos_by_priority( $todos );
$filtered_todos = $this->filter_pay_later_todos( $sorted_todos );
$filtered_todos = $this->filter_button_placement_todos( $filtered_todos );
return $filtered_todos;
}
}

View file

@ -10,6 +10,8 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
@ -106,7 +108,11 @@ class SettingsModule implements ServiceModule, ExecutableModule {
)
);
wp_enqueue_script( 'ppcp-switch-settings-ui' );
wp_enqueue_script( 'ppcp-switch-settings-ui', '', array( 'wp-i18n' ), $script_asset_file['version'] );
wp_set_script_translations(
'ppcp-switch-settings-ui',
'woocommerce-paypal-payments',
);
}
);
@ -159,7 +165,11 @@ class SettingsModule implements ServiceModule, ExecutableModule {
true
);
wp_enqueue_script( 'ppcp-admin-settings' );
wp_enqueue_script( 'ppcp-admin-settings', '', array( 'wp-i18n' ), $script_asset_file['version'] );
wp_set_script_translations(
'ppcp-admin-settings',
'woocommerce-paypal-payments',
);
/**
* Require resolves.
@ -198,11 +208,14 @@ class SettingsModule implements ServiceModule, ExecutableModule {
wp_enqueue_script(
'ppcp-paylater-configurator-lib',
'https://www.paypalobjects.com/merchant-library/merchant-configurator.js',
array(),
array( 'wp-i18n' ),
$script_asset_file['version'],
true
);
wp_set_script_translations(
'ppcp-paylater-configurator-lib',
'woocommerce-paypal-payments',
);
$script_data['PcpPayLaterConfigurator'] = array(
'config' => array(),
'merchantClientId' => $settings->get( 'client_id' ),
@ -303,8 +316,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$flags = new ConfigurationFlagsDTO();
// TODO: Get the merchant country from PayPal here!
$flags->country_code = 'US';
$flags->country_code = $general_settings->get_merchant_country();
$flags->is_business_seller = $general_settings->is_business_seller();
$flags->use_card_payments = $onboarding_profile->get_accept_card_payments();
$flags->use_subscriptions = in_array( ProductChoicesEnum::SUBSCRIPTIONS, $onboarding_profile->get_products(), true );
@ -330,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 ) {
@ -341,21 +359,31 @@ class SettingsModule implements ServiceModule, ExecutableModule {
unset( $payment_methods['venmo'] );
}
// Unset if not eligible for Google Pay.
if ( ! $googlepay_product_status->is_active() ) {
// Unset if country/currency is not supported or merchant not eligible for Google Pay.
if ( ! $container->get( 'googlepay.eligible' ) || ! $googlepay_product_status->is_active() ) {
unset( $payment_methods['ppcp-googlepay'] );
}
// Unset if not eligible for Apple Pay.
if ( ! $applepay_product_status->is_active() ) {
// Unset if country/currency is not supported or merchant not eligible for Apple Pay.
if ( ! $container->get( 'applepay.eligible' ) || ! $applepay_product_status->is_active() ) {
unset( $payment_methods['ppcp-applepay'] );
}
// Unset Fastlane if store location is not United States or merchant is not eligible for ACDC.
if ( $container->get( 'api.shop.country' ) !== 'US' || ! $dcc_product_status->is_active() ) {
// Unset Fastlane if country/currency is not supported or merchant is not eligible for BCDC.
if ( ! $container->get( 'axo.eligible' ) || ! $dcc_product_status->is_active() ) {
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

@ -21,7 +21,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\WebhookEventStorage;
/**
@ -58,8 +57,8 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
$subscriptions_mode_settings = $c->get( 'wcgateway.settings.fields.subscriptions_mode' ) ?: array();
/* @var State $state The state. */
$state = $c->get( 'onboarding.state' );
/* @var bool $is_connected Whether onboarding is complete. */
$is_connected = $c->get( 'settings.flag.is-connected' );
/* @var Bearer $bearer The bearer. */
$bearer = $c->get( 'api.bearer' );
@ -92,7 +91,7 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
'exported_label' => 'Onboarded',
'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$this->onboarded( $bearer, $state )
$this->onboarded( $bearer, $is_connected )
),
),
array(
@ -230,19 +229,18 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
/**
* It returns the current onboarding status.
*
* @param Bearer $bearer The bearer.
* @param State $state The state.
* @param Bearer $bearer The bearer.
* @param bool $is_connected Whether onboarding is complete.
* @return bool
*/
private function onboarded( Bearer $bearer, State $state ): bool {
private function onboarded( Bearer $bearer, bool $is_connected ): bool {
try {
$token = $bearer->bearer();
} catch ( RuntimeException $exception ) {
return false;
}
$current_state = $state->current_state();
return $token->is_valid() && $current_state === $state::STATE_ONBOARDED;
return $is_connected && $token->is_valid();
}
/**

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