mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +08:00
🔀 Merge branch 'trunk'
This commit is contained in:
commit
b8ab931a7a
98 changed files with 2589 additions and 936 deletions
40
.env.integration
Normal file
40
.env.integration
Normal file
|
@ -0,0 +1,40 @@
|
|||
PPCP_INTEGRATION_WP_DIR=${ROOT_DIR}/.ddev/wordpress
|
||||
|
||||
BASEURL="https://woocommerce-paypal-payments.ddev.site"
|
||||
AUTHORIZATION="Bearer ABC123"
|
||||
|
||||
CHECKOUT_URL="/checkout"
|
||||
CHECKOUT_PAGE_ID=7
|
||||
CART_URL="/cart"
|
||||
BLOCK_CHECKOUT_URL="/checkout-block"
|
||||
BLOCK_CHECKOUT_PAGE_ID=22
|
||||
BLOCK_CART_URL="/cart-block"
|
||||
|
||||
PRODUCT_URL="/product/prod"
|
||||
PRODUCT_ID=123
|
||||
|
||||
SUBSCRIPTION_URL="/product/sub"
|
||||
|
||||
PAYPAL_SUBSCRIPTIONS_PRODUCT_ID=252
|
||||
|
||||
APM_ID="sofort"
|
||||
|
||||
WP_MERCHANT_USER="admin"
|
||||
WP_MERCHANT_PASSWORD="admin"
|
||||
|
||||
WP_CUSTOMER_USER="customer"
|
||||
WP_CUSTOMER_PASSWORD="password"
|
||||
|
||||
CUSTOMER_EMAIL="customer@example.com"
|
||||
CUSTOMER_PASSWORD="password"
|
||||
CUSTOMER_FIRST_NAME="John"
|
||||
CUSTOMER_LAST_NAME="Doe"
|
||||
CUSTOMER_COUNTRY="DE"
|
||||
CUSTOMER_ADDRESS="street 1"
|
||||
CUSTOMER_POSTCODE="12345"
|
||||
CUSTOMER_CITY="city"
|
||||
CUSTOMER_PHONE="1234567890"
|
||||
|
||||
CREDIT_CARD_NUMBER="1234567890"
|
||||
CREDIT_CARD_EXPIRATION="01/2042"
|
||||
CREDIT_CARD_CVV="123"
|
|
@ -1,4 +1,4 @@
|
|||
PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress
|
||||
PPCP_INTEGRATION_WP_DIR=${ROOT_DIR}/.ddev/wordpress
|
||||
|
||||
BASEURL="https://woocommerce-paypal-payments.ddev.site"
|
||||
AUTHORIZATION="Bearer ABC123"
|
|
@ -1,4 +1,4 @@
|
|||
name: e2e tests
|
||||
name: Integration tests
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
|
@ -7,8 +7,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.2']
|
||||
wc-versions: ['6.9.4', '7.7.2']
|
||||
php-versions: ['7.4']
|
||||
wc-versions: ['9.7.1']
|
||||
|
||||
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
|
||||
steps:
|
||||
|
@ -30,10 +30,10 @@ jobs:
|
|||
run: ddev orchestrate -f
|
||||
|
||||
- name: Create config
|
||||
run: cp -n .env.e2e.example .env.e2e
|
||||
run: cp -n .env.integration.example .env.integration
|
||||
|
||||
- name: Setup tests
|
||||
run: ddev php tests/e2e/PHPUnit/setup.php
|
||||
run: ddev php tests/integration/PHPUnit/setup.php
|
||||
|
||||
- name: Run PHPUnit
|
||||
run: ddev exec phpunit -c tests/e2e/phpunit.xml.dist
|
||||
run: ddev exec phpunit -c tests/integration/phpunit.xml.dist
|
|
@ -1,5 +1,21 @@
|
|||
*** Changelog ***
|
||||
|
||||
= 3.0.0 - 2025-03-17 =
|
||||
* Enhancement - Redesigned settings UI for new users #2908
|
||||
* Enhancement - Enable Fastlane by default on new store setups when eligible #3199
|
||||
* Enhancement - Enable support for advanced card payments and features for Hong Kong & Singapore #3089
|
||||
* Fix - Dependency conflict with more recent psr/log versions on PHP8+ #2993
|
||||
* Fix - PayPal Checkout Gateway subscription migration layer not renewing subscriptions #2699
|
||||
* Fix - Fatal error when gateway settings initialized too early by third-party plugin #2766
|
||||
* Fix - Next Payment date for Subscriptions not updating when processing a PayPal Subscriptions renewal order #2959
|
||||
* Fix - Changing the subscription payment method to ACDC triggers error #2891
|
||||
* Fix - Standard Card button not appearing in standalone gateway for free trial subscription products #2935
|
||||
* Fix - Validation error when using Trustly payment method #3031
|
||||
* Fix - Error in continuation mode due to wrong gateway selection on Checkout block #2996
|
||||
* Fix - Error in error in PayLaterConfigurator #2989
|
||||
* Tweak - Removed currency requirement for Vault v3 #2919
|
||||
* Tweak - Update plugin author from WooCommerce to PayPal
|
||||
|
||||
= 2.9.6 - 2025-01-06 =
|
||||
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
|
||||
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"WooCommerce\\PayPalCommerce\\": "tests/PHPUnit/",
|
||||
"WooCommerce\\PayPalCommerce\\Tests\\E2e\\": "tests/e2e/PHPUnit/"
|
||||
"WooCommerce\\PayPalCommerce\\Tests\\Integration\\": "tests/integration/PHPUnit/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
|
||||
|
||||
|
@ -18,43 +18,21 @@ class PartnerReferralsData {
|
|||
/**
|
||||
* The DCC Applies Helper object.
|
||||
*
|
||||
* @deprecated Deprecates with the new UI. In this class, the products are
|
||||
* always explicit, and should not be deducted from the
|
||||
* DccApplies state at this point.
|
||||
* Remove this with the legacy UI code.
|
||||
* @var DccApplies
|
||||
*/
|
||||
private $dcc_applies;
|
||||
|
||||
/**
|
||||
* The list of products ('PPCP', 'EXPRESS_CHECKOUT').
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $products;
|
||||
private DccApplies $dcc_applies;
|
||||
|
||||
/**
|
||||
* PartnerReferralsData constructor.
|
||||
*
|
||||
* @param DccApplies $dcc_applies The DCC Applies helper.
|
||||
*/
|
||||
public function __construct(
|
||||
DccApplies $dcc_applies
|
||||
) {
|
||||
public function __construct( DccApplies $dcc_applies ) {
|
||||
$this->dcc_applies = $dcc_applies;
|
||||
$this->products = array(
|
||||
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new copy of this object with the given value set.
|
||||
*
|
||||
* @param string[] $products The list of products ('PPCP', 'EXPRESS_CHECKOUT').
|
||||
* @return static
|
||||
*/
|
||||
public function with_products( array $products ): self {
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->products = $products;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,82 +40,120 @@ class PartnerReferralsData {
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
public function nonce(): string {
|
||||
public function nonce() : string {
|
||||
return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data.
|
||||
*
|
||||
* @param string[] $products The list of products to use ('PPCP', 'EXPRESS_CHECKOUT').
|
||||
* Default is based on DCC availability.
|
||||
* @param string $onboarding_token A security token to finalize the onboarding process.
|
||||
* @param bool $use_subscriptions If the merchant requires subscription features.
|
||||
* @param bool $use_card_payments If the merchant wants to process credit card payments.
|
||||
* @return array
|
||||
*/
|
||||
public function data(): array {
|
||||
public function data( array $products = array(), string $onboarding_token = '', bool $use_subscriptions = null, bool $use_card_payments = true ) : array {
|
||||
if ( ! $products ) {
|
||||
$products = array(
|
||||
$this->dcc_applies->for_country_currency() ? 'PPCP' : 'EXPRESS_CHECKOUT',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the partners referrals data.
|
||||
* Filter the return-URL, which is called at the end of the OAuth onboarding
|
||||
* process, when the merchant clicks the "Return to your shop" button.
|
||||
*/
|
||||
return apply_filters(
|
||||
'ppcp_partner_referrals_data',
|
||||
array(
|
||||
'partner_config_override' => array(
|
||||
/**
|
||||
* Returns the URL which will be opened at the end of onboarding.
|
||||
*/
|
||||
'return_url' => apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url',
|
||||
admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' )
|
||||
),
|
||||
/**
|
||||
* Returns the description of the URL which will be opened at the end of onboarding.
|
||||
*/
|
||||
'return_url_description' => apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url_description',
|
||||
__( 'Return to your shop.', 'woocommerce-paypal-payments' )
|
||||
),
|
||||
'show_add_credit_card' => true,
|
||||
$return_url = apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url',
|
||||
admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the label of the "Return to your shop" button.
|
||||
* It's displayed on the very last page of the onboarding popup.
|
||||
*/
|
||||
$return_url_label = apply_filters(
|
||||
'woocommerce_paypal_payments_partner_config_override_return_url_description',
|
||||
__( 'Return to your shop.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
|
||||
$capabilities = array();
|
||||
$first_party_features = array(
|
||||
'PAYMENT',
|
||||
'REFUND',
|
||||
'ADVANCED_TRANSACTIONS_SEARCH',
|
||||
'TRACKING_SHIPMENT_READWRITE',
|
||||
);
|
||||
|
||||
if ( true === $use_subscriptions ) {
|
||||
$capabilities[] = 'PAYPAL_WALLET_VAULTING_ADVANCED';
|
||||
$first_party_features[] = 'BILLING_AGREEMENT';
|
||||
}
|
||||
|
||||
// Backwards compatibility. Keep those features in the legacy UI (null-value).
|
||||
// Move this into the previous condition, once legacy code is removed.
|
||||
if ( false !== $use_subscriptions ) {
|
||||
$first_party_features[] = 'FUTURE_PAYMENT';
|
||||
$first_party_features[] = 'VAULT';
|
||||
}
|
||||
|
||||
if ( false === $use_subscriptions ) {
|
||||
// Only use "ADVANCED_VAULTING" product for onboarding with subscriptions.
|
||||
$products = array_filter(
|
||||
$products,
|
||||
static fn( $product ) => $product !== 'ADVANCED_VAULTING'
|
||||
);
|
||||
}
|
||||
|
||||
$payload = array(
|
||||
'partner_config_override' => array(
|
||||
'return_url' => $return_url,
|
||||
'return_url_description' => $return_url_label,
|
||||
'show_add_credit_card' => $use_card_payments,
|
||||
),
|
||||
'products' => $products,
|
||||
'capabilities' => $capabilities,
|
||||
'legal_consents' => array(
|
||||
array(
|
||||
'type' => 'SHARE_DATA_CONSENT',
|
||||
'granted' => true,
|
||||
),
|
||||
'products' => $this->products,
|
||||
'legal_consents' => array(
|
||||
array(
|
||||
'type' => 'SHARE_DATA_CONSENT',
|
||||
'granted' => true,
|
||||
),
|
||||
),
|
||||
'operations' => array(
|
||||
array(
|
||||
'operation' => 'API_INTEGRATION',
|
||||
'api_integration_preference' => array(
|
||||
'rest_api_integration' => array(
|
||||
'integration_method' => 'PAYPAL',
|
||||
'integration_type' => 'FIRST_PARTY',
|
||||
'first_party_details' => array(
|
||||
'features' => array(
|
||||
'PAYMENT',
|
||||
'FUTURE_PAYMENT',
|
||||
'REFUND',
|
||||
'ADVANCED_TRANSACTIONS_SEARCH',
|
||||
'VAULT',
|
||||
'TRACKING_SHIPMENT_READWRITE',
|
||||
),
|
||||
'seller_nonce' => $this->nonce(),
|
||||
),
|
||||
),
|
||||
'operations' => array(
|
||||
array(
|
||||
'operation' => 'API_INTEGRATION',
|
||||
'api_integration_preference' => array(
|
||||
'rest_api_integration' => array(
|
||||
'integration_method' => 'PAYPAL',
|
||||
'integration_type' => 'FIRST_PARTY',
|
||||
'first_party_details' => array(
|
||||
'features' => $first_party_features,
|
||||
'seller_nonce' => $this->nonce(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the validation token to the return_url
|
||||
*
|
||||
* @param array $data The referral data.
|
||||
* @param string $token The token to be appended.
|
||||
* @return array
|
||||
*/
|
||||
public function append_onboarding_token( array $data, string $token ): array {
|
||||
$data['partner_config_override']['return_url'] =
|
||||
add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] );
|
||||
return $data;
|
||||
/**
|
||||
* Filter the final partners referrals data collection.
|
||||
*/
|
||||
$payload = apply_filters( 'ppcp_partner_referrals_data', $payload );
|
||||
|
||||
// An empty array is not permitted.
|
||||
if ( isset( $payload['capabilities'] ) && ! $payload['capabilities'] ) {
|
||||
unset( $payload['capabilities'] );
|
||||
}
|
||||
|
||||
// Add the nonce in the end, to maintain backwards compatibility of filters.
|
||||
$payload['partner_config_override']['return_url'] = add_query_arg(
|
||||
array( 'ppcpToken' => $onboarding_token ),
|
||||
$payload['partner_config_override']['return_url']
|
||||
);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1018,6 +1018,10 @@ class ApplePayButton implements ButtonInterface {
|
|||
* @return void
|
||||
*/
|
||||
public function enqueue(): void {
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_register_script(
|
||||
'wc-ppcp-applepay',
|
||||
untrailingslashit( $this->module_url ) . '/assets/js/boot.js',
|
||||
|
|
|
@ -107,11 +107,9 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
'woocommerce_blocks_payment_method_type_registration',
|
||||
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
|
||||
/*
|
||||
* Only register the method if we are not in the admin
|
||||
* (to avoid two Debit & Credit Cards gateways in the
|
||||
* checkout block in the editor: one from ACDC one from Axo).
|
||||
* Only register the method if we are not in the admin or the customer is not logged in.
|
||||
*/
|
||||
if ( ! is_admin() ) {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
$payment_method_registry->register( $c->get( 'axoblock.method' ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace WooCommerce\PayPalCommerce\Axo;
|
|||
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies;
|
||||
use WooCommerce\PayPalCommerce\Axo\Helper\SettingsNoticeGenerator;
|
||||
use WooCommerce\PayPalCommerce\Axo\Helper\CompatibilityChecker;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
|
@ -38,8 +38,8 @@ return array(
|
|||
);
|
||||
},
|
||||
|
||||
'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator {
|
||||
return new SettingsNoticeGenerator( $container->get( 'axo.fastlane-incompatible-plugin-names' ) );
|
||||
'axo.helpers.compatibility-checker' => static function ( ContainerInterface $container ) : CompatibilityChecker {
|
||||
return new CompatibilityChecker( $container->get( 'axo.fastlane-incompatible-plugin-names' ) );
|
||||
},
|
||||
|
||||
// If AXO is configured and onboarded.
|
||||
|
@ -190,38 +190,38 @@ return array(
|
|||
);
|
||||
},
|
||||
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
|
||||
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
|
||||
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
|
||||
$compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
|
||||
assert( $compatibility_checker instanceof CompatibilityChecker );
|
||||
|
||||
$settings = $container->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
return $settings_notice_generator->generate_settings_conflict_notice( $settings );
|
||||
return $compatibility_checker->generate_settings_conflict_notice( $settings );
|
||||
},
|
||||
|
||||
'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string {
|
||||
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
|
||||
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
|
||||
$compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
|
||||
assert( $compatibility_checker instanceof CompatibilityChecker );
|
||||
|
||||
return $settings_notice_generator->generate_checkout_notice();
|
||||
return $compatibility_checker->generate_checkout_notice();
|
||||
},
|
||||
|
||||
'axo.checkout-config-notice.raw' => static function ( ContainerInterface $container ) : string {
|
||||
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
|
||||
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
|
||||
$compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
|
||||
assert( $compatibility_checker instanceof CompatibilityChecker );
|
||||
|
||||
return $settings_notice_generator->generate_checkout_notice( true );
|
||||
return $compatibility_checker->generate_checkout_notice( true );
|
||||
},
|
||||
|
||||
'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string {
|
||||
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
|
||||
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
|
||||
$settings_notice_generator = $container->get( 'axo.helpers.compatibility-checker' );
|
||||
assert( $settings_notice_generator instanceof CompatibilityChecker );
|
||||
|
||||
return $settings_notice_generator->generate_incompatible_plugins_notice();
|
||||
},
|
||||
|
||||
'axo.incompatible-plugins-notice.raw' => static function ( ContainerInterface $container ) : string {
|
||||
$settings_notice_generator = new SettingsNoticeGenerator(
|
||||
$settings_notice_generator = new CompatibilityChecker(
|
||||
$container->get( 'axo.fastlane-incompatible-plugin-names' )
|
||||
);
|
||||
|
||||
|
|
|
@ -475,7 +475,7 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
* @return void
|
||||
*/
|
||||
private function add_feature_detection_tag( bool $axo_enabled ) {
|
||||
$show_tag = is_checkout() || is_cart() || is_shop();
|
||||
$show_tag = is_home() || is_checkout() || is_cart() || is_shop();
|
||||
|
||||
if ( ! $show_tag ) {
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* Settings notice generator.
|
||||
* Generates the settings notices.
|
||||
* Fastlane compatibility checker.
|
||||
* Detects compatibility issues and generates relevant notices.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Axo\Helper
|
||||
*/
|
||||
|
@ -15,9 +15,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
|||
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class SettingsNoticeGenerator
|
||||
* Class CompatibilityChecker
|
||||
*/
|
||||
class SettingsNoticeGenerator {
|
||||
class CompatibilityChecker {
|
||||
/**
|
||||
* The list of Fastlane incompatible plugin names.
|
||||
*
|
||||
|
@ -26,12 +26,88 @@ class SettingsNoticeGenerator {
|
|||
protected array $incompatible_plugin_names;
|
||||
|
||||
/**
|
||||
* SettingsNoticeGenerator constructor.
|
||||
* Stores the result of checkout compatibility checks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $checkout_compatibility;
|
||||
|
||||
/**
|
||||
* Stores whether DCC is enabled.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
protected ?bool $is_dcc_enabled = null;
|
||||
|
||||
/**
|
||||
* CompatibilityChecker constructor.
|
||||
*
|
||||
* @param string[] $incompatible_plugin_names The list of Fastlane incompatible plugin names.
|
||||
*/
|
||||
public function __construct( array $incompatible_plugin_names ) {
|
||||
$this->incompatible_plugin_names = $incompatible_plugin_names;
|
||||
$this->checkout_compatibility = array(
|
||||
'has_elementor_checkout' => null,
|
||||
'has_classic_checkout' => null,
|
||||
'has_block_checkout' => null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the checkout uses Elementor.
|
||||
*
|
||||
* @return bool Whether the checkout uses Elementor.
|
||||
*/
|
||||
protected function has_elementor_checkout(): bool {
|
||||
if ( $this->checkout_compatibility['has_elementor_checkout'] === null ) {
|
||||
$this->checkout_compatibility['has_elementor_checkout'] = CartCheckoutDetector::has_elementor_checkout();
|
||||
}
|
||||
|
||||
return $this->checkout_compatibility['has_elementor_checkout'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the checkout uses classic checkout.
|
||||
*
|
||||
* @return bool Whether the checkout uses classic checkout.
|
||||
*/
|
||||
protected function has_classic_checkout(): bool {
|
||||
if ( $this->checkout_compatibility['has_classic_checkout'] === null ) {
|
||||
$this->checkout_compatibility['has_classic_checkout'] = CartCheckoutDetector::has_classic_checkout();
|
||||
}
|
||||
|
||||
return $this->checkout_compatibility['has_classic_checkout'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the checkout uses block checkout.
|
||||
*
|
||||
* @return bool Whether the checkout uses block checkout.
|
||||
*/
|
||||
protected function has_block_checkout(): bool {
|
||||
if ( $this->checkout_compatibility['has_block_checkout'] === null ) {
|
||||
$this->checkout_compatibility['has_block_checkout'] = CartCheckoutDetector::has_block_checkout();
|
||||
}
|
||||
|
||||
return $this->checkout_compatibility['has_block_checkout'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if DCC is enabled.
|
||||
*
|
||||
* @param Settings $settings The plugin settings container.
|
||||
* @return bool Whether DCC is enabled.
|
||||
*/
|
||||
protected function is_dcc_enabled( Settings $settings ): bool {
|
||||
if ( $this->is_dcc_enabled === null ) {
|
||||
try {
|
||||
$this->is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' );
|
||||
} catch ( NotFoundException $ignored ) {
|
||||
$this->is_dcc_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->is_dcc_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,6 +135,30 @@ class SettingsNoticeGenerator {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there aren't any incompatibilities that would prevent Fastlane from working properly.
|
||||
*
|
||||
* @return bool Whether the setup is compatible.
|
||||
*/
|
||||
public function is_fastlane_compatible(): bool {
|
||||
// Check for incompatible plugins.
|
||||
if ( ! empty( $this->incompatible_plugin_names ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for checkout page incompatibilities.
|
||||
if ( $this->has_elementor_checkout() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->has_classic_checkout() && ! $this->has_block_checkout() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No incompatibilities found.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the checkout notice.
|
||||
*
|
||||
|
@ -66,15 +166,23 @@ class SettingsNoticeGenerator {
|
|||
* @return string
|
||||
*/
|
||||
public function generate_checkout_notice( bool $raw_message = false ): string {
|
||||
$notice_content = '';
|
||||
|
||||
// Check for checkout incompatibilities.
|
||||
$has_checkout_incompatibility = $this->has_elementor_checkout() ||
|
||||
( ! $this->has_classic_checkout() && ! $this->has_block_checkout() );
|
||||
|
||||
if ( ! $has_checkout_incompatibility ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
|
||||
$block_checkout_docs_link = __(
|
||||
'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/#using-the-cart-and-checkout-blocks',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
|
||||
$notice_content = '';
|
||||
|
||||
if ( CartCheckoutDetector::has_elementor_checkout() ) {
|
||||
if ( $this->has_elementor_checkout() ) {
|
||||
$notice_content = sprintf(
|
||||
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
|
||||
__(
|
||||
|
@ -84,7 +192,7 @@ class SettingsNoticeGenerator {
|
|||
esc_url( $checkout_page_link ),
|
||||
esc_url( $block_checkout_docs_link )
|
||||
);
|
||||
} elseif ( ! CartCheckoutDetector::has_classic_checkout() && ! CartCheckoutDetector::has_block_checkout() ) {
|
||||
} elseif ( ! $this->has_classic_checkout() && ! $this->has_block_checkout() ) {
|
||||
$notice_content = sprintf(
|
||||
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
|
||||
__(
|
||||
|
@ -132,22 +240,14 @@ class SettingsNoticeGenerator {
|
|||
* @return string
|
||||
*/
|
||||
public function generate_settings_conflict_notice( Settings $settings, bool $raw_message = false ) : string {
|
||||
$notice_content = '';
|
||||
$is_dcc_enabled = false;
|
||||
|
||||
try {
|
||||
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' );
|
||||
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
} catch ( NotFoundException $ignored ) {
|
||||
// Never happens.
|
||||
if ( $this->is_dcc_enabled( $settings ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! $is_dcc_enabled ) {
|
||||
$notice_content = __(
|
||||
'<span class="highlight">Warning:</span> To enable Fastlane and accelerate payments, the <strong>Advanced Card Processing</strong> payment method must also be enabled.',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
}
|
||||
$notice_content = __(
|
||||
'<span class="highlight">Warning:</span> To enable Fastlane and accelerate payments, the <strong>Advanced Card Processing</strong> payment method must also be enabled.',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
|
||||
return $this->render_notice( $notice_content, true, $raw_message );
|
||||
}
|
|
@ -67,27 +67,6 @@ export const PayPalComponent = ( {
|
|||
? `${ config.id }-${ fundingSource }`
|
||||
: config.id;
|
||||
|
||||
/**
|
||||
* The block cart displays express checkout buttons. Those buttons are handled by the
|
||||
* PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
|
||||
*
|
||||
* A possible bug in WooCommerce does not use the correct payment method ID for the express
|
||||
* payment buttons inside the cart, but sends the ID of the _first_ active payment method.
|
||||
*
|
||||
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
|
||||
*/
|
||||
const enforcePaymentMethodForCart = () => {
|
||||
// Do nothing, unless we're handling block cart express payment buttons.
|
||||
if ( 'cart-block' !== config.scriptData.context ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the active payment method to PAYPAL_GATEWAY_ID.
|
||||
wp.data
|
||||
.dispatch( 'wc/store/payment' )
|
||||
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
// fill the form if in continuation (for product or mini-cart buttons)
|
||||
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
|
||||
|
@ -339,7 +318,6 @@ export const PayPalComponent = ( {
|
|||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
|
@ -439,7 +417,6 @@ export const PayPalComponent = ( {
|
|||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
|
@ -476,7 +453,6 @@ export const PayPalComponent = ( {
|
|||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
|
|
|
@ -81,12 +81,14 @@ export const paypalShippingToWc = ( shipping ) => {
|
|||
export const paypalPayerToWc = ( payer ) => {
|
||||
const firstName = payer?.name?.given_name ?? '';
|
||||
const lastName = payer?.name?.surname ?? '';
|
||||
const phone = payer?.phone?.phone_number?.national_number ?? '';
|
||||
const address = payer.address ? paypalAddressToWc( payer.address ) : {};
|
||||
return {
|
||||
...address,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
email: payer.email_address,
|
||||
phone: phone
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -61,7 +61,6 @@ export const handleApprove = async (
|
|||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
|
@ -132,7 +131,6 @@ export const handleApprove = async (
|
|||
location.href = getCheckoutRedirectUrl();
|
||||
} else {
|
||||
setGotoContinuationOnError( true );
|
||||
enforcePaymentMethodForCart();
|
||||
onSubmit();
|
||||
}
|
||||
} catch ( err ) {
|
||||
|
@ -171,7 +169,6 @@ export const handleApproveSubscription = async (
|
|||
shouldskipFinalConfirmation,
|
||||
getCheckoutRedirectUrl,
|
||||
setGotoContinuationOnError,
|
||||
enforcePaymentMethodForCart,
|
||||
onSubmit,
|
||||
onError,
|
||||
onClose
|
||||
|
@ -242,7 +239,6 @@ export const handleApproveSubscription = async (
|
|||
location.href = getCheckoutRedirectUrl();
|
||||
} else {
|
||||
setGotoContinuationOnError( true );
|
||||
enforcePaymentMethodForCart();
|
||||
onSubmit();
|
||||
}
|
||||
} catch ( err ) {
|
||||
|
|
|
@ -28,13 +28,6 @@ return array(
|
|||
);
|
||||
},
|
||||
'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod {
|
||||
/**
|
||||
* Cart instance; might be null, esp. in customizer or in Block Editor.
|
||||
*
|
||||
* @var null|WC_Cart $cart
|
||||
*/
|
||||
$cart = WC()->cart;
|
||||
|
||||
return new PayPalPaymentMethod(
|
||||
$container->get( 'blocks.url' ),
|
||||
$container->get( 'ppcp.asset-version' ),
|
||||
|
@ -53,7 +46,6 @@ return array(
|
|||
$container->get( 'wcgateway.place-order-button-text' ),
|
||||
$container->get( 'wcgateway.place-order-button-description' ),
|
||||
$container->get( 'wcgateway.all-funding-sources' ),
|
||||
$cart && $cart->needs_shipping()
|
||||
);
|
||||
},
|
||||
'blocks.advanced-card-method' => static function( ContainerInterface $container ): AdvancedCardPaymentMethod {
|
||||
|
|
|
@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
|
|||
*/
|
||||
class UpdateShippingEndpoint implements EndpointInterface {
|
||||
const ENDPOINT = 'ppc-update-shipping';
|
||||
const WC_STORE_API_ENDPOINT = '/wp-json/wc/store/cart/';
|
||||
const WC_STORE_API_ENDPOINT = '/wp-json/wc/store/v1/cart/';
|
||||
|
||||
/**
|
||||
* The Request Data Helper.
|
||||
|
|
|
@ -130,13 +130,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
*/
|
||||
private $all_funding_sources;
|
||||
|
||||
/**
|
||||
* Whether shipping details must be collected during checkout; i.e. paying for physical goods?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $need_shipping;
|
||||
|
||||
/**
|
||||
* Assets constructor.
|
||||
*
|
||||
|
@ -155,7 +148,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
* @param string $place_order_button_text The text for the standard "Place order" button.
|
||||
* @param string $place_order_button_description The text for additional "Place order" description.
|
||||
* @param array $all_funding_sources All existing funding sources for PayPal buttons.
|
||||
* @param bool $need_shipping Whether shipping details are required for the purchase.
|
||||
*/
|
||||
public function __construct(
|
||||
string $module_url,
|
||||
|
@ -172,8 +164,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
bool $use_place_order,
|
||||
string $place_order_button_text,
|
||||
string $place_order_button_description,
|
||||
array $all_funding_sources,
|
||||
bool $need_shipping
|
||||
array $all_funding_sources
|
||||
) {
|
||||
$this->name = PayPalGateway::ID;
|
||||
$this->module_url = $module_url;
|
||||
|
@ -191,7 +182,6 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
$this->place_order_button_text = $place_order_button_text;
|
||||
$this->place_order_button_description = $place_order_button_description;
|
||||
$this->all_funding_sources = $all_funding_sources;
|
||||
$this->need_shipping = $need_shipping;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -258,6 +248,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
&& $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ?? 'block-checkout' );
|
||||
$place_order_enabled = ( $this->use_place_order || $this->add_place_order_method )
|
||||
&& ! $this->subscription_helper->cart_contains_subscription();
|
||||
$cart = WC()->cart;
|
||||
|
||||
return array(
|
||||
'id' => $this->gateway->id,
|
||||
|
@ -284,7 +275,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
),
|
||||
),
|
||||
'scriptData' => $script_data,
|
||||
'needShipping' => $this->need_shipping,
|
||||
'needShipping' => $cart && $cart->needs_shipping(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@ export const handleShippingOptionsChange = async ( data, actions, config ) => {
|
|||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WC-Store-API-Nonce':
|
||||
config.ajax.update_customer_shipping.wp_rest_nonce,
|
||||
'Nonce':
|
||||
config.ajax.update_customer_shipping.wp_rest_nonce,
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
rate_id: shippingOptionId,
|
||||
|
@ -106,9 +106,9 @@ export const handleShippingAddressChange = async ( data, actions, config ) => {
|
|||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WC-Store-API-Nonce':
|
||||
config.ajax.update_customer_shipping
|
||||
.wp_rest_nonce,
|
||||
'Nonce':
|
||||
config.ajax.update_customer_shipping
|
||||
.wp_rest_nonce,
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
shipping_address: cartData.shipping_address,
|
||||
|
|
|
@ -104,6 +104,6 @@ class DisabledFundingSources {
|
|||
$disable_funding = $all_sources;
|
||||
}
|
||||
|
||||
return $disable_funding;
|
||||
return apply_filters( 'woocommerce_paypal_payments_disabled_funding_sources', $disable_funding );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,8 +199,9 @@ class WooCommerceOrderCreator {
|
|||
$shipping_options = null;
|
||||
|
||||
if ( $payer ) {
|
||||
$address = $payer->address();
|
||||
$payer_name = $payer->name();
|
||||
$address = $payer->address();
|
||||
$payer_name = $payer->name();
|
||||
$payer_phone = $payer->phone();
|
||||
|
||||
$wc_email = null;
|
||||
$wc_customer = WC()->customer;
|
||||
|
@ -220,6 +221,7 @@ class WooCommerceOrderCreator {
|
|||
'state' => $address ? $address->admin_area_1() : '',
|
||||
'postcode' => $address ? $address->postal_code() : '',
|
||||
'country' => $address ? $address->country_code() : '',
|
||||
'phone' => $payer_phone ? $payer_phone->phone()->national_number() : '',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Compat;
|
|||
|
||||
use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\GeneralSettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\PaymentMethodSettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMap;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsTabMapHelper;
|
||||
|
@ -145,6 +146,9 @@ return array(
|
|||
$general_map_helper = $container->get( 'compat.settings.general_map_helper' );
|
||||
assert( $general_map_helper instanceof GeneralSettingsMapHelper );
|
||||
|
||||
$payment_methods_map_helper = $container->get( 'compat.settings.payment_methods_map_helper' );
|
||||
assert( $payment_methods_map_helper instanceof PaymentMethodSettingsMapHelper );
|
||||
|
||||
return array(
|
||||
new SettingsMap(
|
||||
$container->get( 'settings.data.general' ),
|
||||
|
@ -182,6 +186,10 @@ return array(
|
|||
$container->get( 'settings.data.payment' ),
|
||||
array()
|
||||
),
|
||||
new SettingsMap(
|
||||
$container->get( 'settings.data.payment' ),
|
||||
$payment_methods_map_helper->map()
|
||||
),
|
||||
);
|
||||
},
|
||||
'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper {
|
||||
|
@ -191,6 +199,7 @@ return array(
|
|||
$container->get( 'compat.settings.settings_tab_map_helper' ),
|
||||
$container->get( 'compat.settings.subscription_map_helper' ),
|
||||
$container->get( 'compat.settings.general_map_helper' ),
|
||||
$container->get( 'compat.settings.payment_methods_map_helper' ),
|
||||
$container->get( 'wcgateway.settings.admin-settings-enabled' )
|
||||
);
|
||||
},
|
||||
|
@ -206,4 +215,7 @@ return array(
|
|||
'compat.settings.general_map_helper' => static function() : GeneralSettingsMapHelper {
|
||||
return new GeneralSettingsMapHelper();
|
||||
},
|
||||
'compat.settings.payment_methods_map_helper' => static function() : PaymentMethodSettingsMapHelper {
|
||||
return new PaymentMethodSettingsMapHelper();
|
||||
},
|
||||
);
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
/**
|
||||
* A helper for mapping the old/new payment method settings.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Compat\Settings
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Compat\Settings;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
|
||||
/**
|
||||
* A map of old to new payment method settings.
|
||||
*
|
||||
* @psalm-import-type newSettingsKey from SettingsMap
|
||||
* @psalm-import-type oldSettingsKey from SettingsMap
|
||||
*/
|
||||
class PaymentMethodSettingsMapHelper {
|
||||
|
||||
/**
|
||||
* Maps old setting keys to new payment method settings names.
|
||||
*
|
||||
* @psalm-return array<oldSettingsKey, newSettingsKey>
|
||||
*/
|
||||
public function map(): array {
|
||||
return array(
|
||||
'dcc_enabled' => CreditCardGateway::ID,
|
||||
'axo_enabled' => AxoGateway::ID,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of a mapped key from the new settings.
|
||||
*
|
||||
* @param string $old_key The key from the legacy settings.
|
||||
* @return mixed The value of the mapped setting, (null if not found).
|
||||
*/
|
||||
public function mapped_value( string $old_key ): ?bool {
|
||||
|
||||
$payment_method = $this->map()[ $old_key ] ?? false;
|
||||
|
||||
if ( ! $payment_method ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->is_gateway_enabled( $payment_method );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the payment gateway with the given name is enabled.
|
||||
*
|
||||
* @param string $gateway_name The gateway name.
|
||||
* @return bool True if the payment gateway with the given name is enabled, otherwise false.
|
||||
*/
|
||||
protected function is_gateway_enabled( string $gateway_name ): bool {
|
||||
$gateway_settings = get_option( "woocommerce_{$gateway_name}_settings", array() );
|
||||
$gateway_enabled = $gateway_settings['enabled'] ?? false;
|
||||
|
||||
return $gateway_enabled === 'yes';
|
||||
}
|
||||
}
|
|
@ -74,6 +74,13 @@ class SettingsMapHelper {
|
|||
*/
|
||||
protected GeneralSettingsMapHelper $general_settings_map_helper;
|
||||
|
||||
/**
|
||||
* A helper for mapping old and new payment method settings.
|
||||
*
|
||||
* @var PaymentMethodSettingsMapHelper
|
||||
*/
|
||||
protected PaymentMethodSettingsMapHelper $payment_method_settings_map_helper;
|
||||
|
||||
/**
|
||||
* Whether the new settings module is enabled.
|
||||
*
|
||||
|
@ -84,12 +91,13 @@ class SettingsMapHelper {
|
|||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param SettingsMap[] $settings_map A list of settings maps containing key definitions.
|
||||
* @param StylingSettingsMapHelper $styling_settings_map_helper A helper for mapping the old/new styling settings.
|
||||
* @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings.
|
||||
* @param SubscriptionSettingsMapHelper $subscription_map_helper A helper for mapping old and new subscription settings.
|
||||
* @param GeneralSettingsMapHelper $general_settings_map_helper A helper for mapping old and new general settings.
|
||||
* @param bool $new_settings_module_enabled Whether the new settings module is enabled.
|
||||
* @param SettingsMap[] $settings_map A list of settings maps containing key definitions.
|
||||
* @param StylingSettingsMapHelper $styling_settings_map_helper A helper for mapping the old/new styling settings.
|
||||
* @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings.
|
||||
* @param SubscriptionSettingsMapHelper $subscription_map_helper A helper for mapping old and new subscription settings.
|
||||
* @param GeneralSettingsMapHelper $general_settings_map_helper A helper for mapping old and new general settings.
|
||||
* @param PaymentMethodSettingsMapHelper $payment_method_settings_map_helper A helper for mapping old and new payment method settings.
|
||||
* @param bool $new_settings_module_enabled Whether the new settings module is enabled.
|
||||
* @throws RuntimeException When an old key has multiple mappings.
|
||||
*/
|
||||
public function __construct(
|
||||
|
@ -98,15 +106,17 @@ class SettingsMapHelper {
|
|||
SettingsTabMapHelper $settings_tab_map_helper,
|
||||
SubscriptionSettingsMapHelper $subscription_map_helper,
|
||||
GeneralSettingsMapHelper $general_settings_map_helper,
|
||||
PaymentMethodSettingsMapHelper $payment_method_settings_map_helper,
|
||||
bool $new_settings_module_enabled
|
||||
) {
|
||||
$this->validate_settings_map( $settings_map );
|
||||
$this->settings_map = $settings_map;
|
||||
$this->styling_settings_map_helper = $styling_settings_map_helper;
|
||||
$this->settings_tab_map_helper = $settings_tab_map_helper;
|
||||
$this->subscription_map_helper = $subscription_map_helper;
|
||||
$this->general_settings_map_helper = $general_settings_map_helper;
|
||||
$this->new_settings_module_enabled = $new_settings_module_enabled;
|
||||
$this->settings_map = $settings_map;
|
||||
$this->styling_settings_map_helper = $styling_settings_map_helper;
|
||||
$this->settings_tab_map_helper = $settings_tab_map_helper;
|
||||
$this->subscription_map_helper = $subscription_map_helper;
|
||||
$this->general_settings_map_helper = $general_settings_map_helper;
|
||||
$this->payment_method_settings_map_helper = $payment_method_settings_map_helper;
|
||||
$this->new_settings_module_enabled = $new_settings_module_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,6 +213,9 @@ class SettingsMapHelper {
|
|||
? $this->subscription_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] )
|
||||
: $this->settings_tab_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] );
|
||||
|
||||
case $model instanceof PaymentSettings:
|
||||
return $this->payment_method_settings_map_helper->mapped_value( $old_key );
|
||||
|
||||
default:
|
||||
return $this->model_cache[ $model_id ][ $new_key ] ?? null;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ class SettingsTabMapHelper {
|
|||
'intent' => '',
|
||||
'vault_enabled_dcc' => 'save_card_details',
|
||||
'blocks_final_review_enabled' => 'enable_pay_now',
|
||||
'logging_enabled' => 'enable_logging',
|
||||
'vault_enabled' => 'save_paypal_and_venmo',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class StylingSettingsMapHelper {
|
|||
|
||||
use ContextTrait;
|
||||
|
||||
protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID, 'pay-later' );
|
||||
protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID );
|
||||
|
||||
/**
|
||||
* Maps old setting keys to new setting style names.
|
||||
|
@ -89,13 +89,13 @@ class StylingSettingsMapHelper {
|
|||
return $this->mapped_disabled_funding_value( $styling_models, $payment_settings );
|
||||
|
||||
case 'googlepay_button_enabled':
|
||||
return $this->mapped_button_enabled_value( $styling_models, GooglePayGateway::ID, $payment_settings );
|
||||
return $this->mapped_button_enabled_value( $styling_models, GooglePayGateway::ID );
|
||||
|
||||
case 'applepay_button_enabled':
|
||||
return $this->mapped_button_enabled_value( $styling_models, ApplePayGateway::ID, $payment_settings );
|
||||
return $this->mapped_button_enabled_value( $styling_models, ApplePayGateway::ID );
|
||||
|
||||
case 'pay_later_button_enabled':
|
||||
return $this->mapped_button_enabled_value( $styling_models, 'pay-later', $payment_settings );
|
||||
return $this->mapped_pay_later_button_enabled_value( $styling_models, $payment_settings );
|
||||
|
||||
default:
|
||||
foreach ( $this->locations_map() as $old_location_name => $new_location_name ) {
|
||||
|
@ -255,30 +255,51 @@ class StylingSettingsMapHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mapped enabled or disabled button value from the new settings.
|
||||
* Retrieves the mapped enabled or disabled PayLater button value from the new settings.
|
||||
*
|
||||
* @param LocationStylingDTO[] $styling_models The list of location styling models.
|
||||
* @param string $button_name The button name (see {@link self::BUTTON_NAMES}).
|
||||
* @param AbstractDataModel|null $payment_settings The payment settings model.
|
||||
* @return int The enabled (1) or disabled (0) state.
|
||||
* @throws RuntimeException If an invalid button name is provided.
|
||||
* @return int|null The enabled (1) or disabled (0) state or null if it should fall back to old settings value.
|
||||
*/
|
||||
protected function mapped_button_enabled_value( array $styling_models, string $button_name, ?AbstractDataModel $payment_settings ): ?int {
|
||||
if ( is_null( $payment_settings ) ) {
|
||||
protected function mapped_pay_later_button_enabled_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?int {
|
||||
|
||||
if ( ! $payment_settings instanceof PaymentSettings ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$locations_to_context_map = $this->current_context_to_new_button_location_map();
|
||||
$current_context = $locations_to_context_map[ $this->context() ] ?? '';
|
||||
|
||||
foreach ( $styling_models as $model ) {
|
||||
if ( $model->enabled && $model->location === $current_context ) {
|
||||
if ( in_array( 'pay-later', $model->methods, true ) && $payment_settings->get_paylater_enabled() ) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mapped enabled or disabled button value from the new settings.
|
||||
*
|
||||
* @param LocationStylingDTO[] $styling_models The list of location styling models.
|
||||
* @param string $button_name The button name (see {@link self::BUTTON_NAMES}).
|
||||
* @return int The enabled (1) or disabled (0) state.
|
||||
* @throws RuntimeException If an invalid button name is provided.
|
||||
*/
|
||||
protected function mapped_button_enabled_value( array $styling_models, string $button_name ): ?int {
|
||||
if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) {
|
||||
throw new RuntimeException( 'Wrong button name is provided.' );
|
||||
}
|
||||
|
||||
$locations_to_context_map = $this->current_context_to_new_button_location_map();
|
||||
$current_context = $locations_to_context_map[ $this->context() ] ?? '';
|
||||
assert( $payment_settings instanceof PaymentSettings );
|
||||
|
||||
foreach ( $styling_models as $model ) {
|
||||
if ( $model->enabled && $model->location === $current_context ) {
|
||||
if ( in_array( $button_name, $model->methods, true ) && $payment_settings->is_method_enabled( $button_name ) ) {
|
||||
if ( in_array( $button_name, $model->methods, true ) && $this->is_gateway_enabled( $button_name ) ) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
@ -307,4 +328,17 @@ class StylingSettingsMapHelper {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the payment gateway with the given name is enabled.
|
||||
*
|
||||
* @param string $gateway_name The gateway name.
|
||||
* @return bool True if the payment gateway with the given name is enabled, otherwise false.
|
||||
*/
|
||||
protected function is_gateway_enabled( string $gateway_name ): bool {
|
||||
$gateway_settings = get_option( "woocommerce_{$gateway_name}_settings", array() );
|
||||
$gateway_enabled = $gateway_settings['enabled'] ?? false;
|
||||
|
||||
return $gateway_enabled === 'yes';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -326,9 +326,9 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
|
|||
|
||||
isDisconnecting = true;
|
||||
|
||||
const saveButton = document.querySelector( '.woocommerce-save-button' );
|
||||
saveButton.removeAttribute( 'disabled' );
|
||||
saveButton.click();
|
||||
const saveButton = document.querySelector( '.woocommerce-save-button' );
|
||||
saveButton.removeAttribute( 'disabled' );
|
||||
saveButton.click();
|
||||
};
|
||||
|
||||
// Prevent the message about unsaved checkbox/radiobutton when reloading the page.
|
||||
|
@ -345,9 +345,11 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
|
|||
|
||||
const sandboxSwitchElement = document.querySelector( '#ppcp-sandbox_on' );
|
||||
|
||||
sandboxSwitchElement?.addEventListener( 'click', () => {
|
||||
document.querySelector( '.woocommerce-save-button' )?.removeAttribute( 'disabled' );
|
||||
});
|
||||
sandboxSwitchElement?.addEventListener( 'click', () => {
|
||||
document
|
||||
.querySelector( '.woocommerce-save-button' )
|
||||
?.removeAttribute( 'disabled' );
|
||||
} );
|
||||
|
||||
const validate = () => {
|
||||
const selectors = sandboxSwitchElement.checked
|
||||
|
@ -389,7 +391,8 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
|
|||
|
||||
const isSandboxInBackend =
|
||||
PayPalCommerceGatewayOnboarding.current_env === 'sandbox';
|
||||
if ( sandboxSwitchElement.checked !== isSandboxInBackend ) {
|
||||
|
||||
if ( sandboxSwitchElement?.checked !== isSandboxInBackend ) {
|
||||
sandboxSwitchElement.checked = isSandboxInBackend;
|
||||
}
|
||||
|
||||
|
|
|
@ -103,12 +103,8 @@ class OnboardingRenderer {
|
|||
'displayMode' => 'minibrowser',
|
||||
);
|
||||
|
||||
$data = $this->partner_referrals_data
|
||||
->with_products( $products )
|
||||
->data();
|
||||
|
||||
$environment = $is_production ? 'production' : 'sandbox';
|
||||
$product = 'PPCP' === $data['products'][0] ? 'ppcp' : 'express_checkout';
|
||||
$product = strtolower( $products[0] ?? 'express_checkout' );
|
||||
$cache_key = $environment . '-' . $product;
|
||||
|
||||
$onboarding_url = new OnboardingUrl( $this->cache, $cache_key, get_current_user_id() );
|
||||
|
@ -122,8 +118,7 @@ class OnboardingRenderer {
|
|||
|
||||
$onboarding_url->init();
|
||||
|
||||
$data = $this->partner_referrals_data
|
||||
->append_onboarding_token( $data, $onboarding_url->token() ?: '' );
|
||||
$data = $this->partner_referrals_data->data( $products, $onboarding_url->token() ?: '' );
|
||||
|
||||
$url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data );
|
||||
$url = add_query_arg( $args, $url );
|
||||
|
|
|
@ -93,6 +93,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ppcp-r-payment-methods {
|
||||
.ppcp-highlight {
|
||||
animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid $color-blueberry;
|
||||
border-radius: var(--container-border-radius);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes ppcp-highlight-fade {
|
||||
0%, 20% {
|
||||
background-color: rgba($color-blueberry, 0.08);
|
||||
border-color: $color-blueberry;
|
||||
border-width: 1px;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
border-color: $color-gray-300;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled state styling.
|
||||
.ppcp--method-item--disabled {
|
||||
position: relative;
|
||||
|
|
|
@ -226,25 +226,91 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Payment Methods
|
||||
.ppcp-r-settings {
|
||||
.ppcp-highlight {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.ppcp-highlight {
|
||||
animation: ppcp-highlight-fade 2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid $color-blueberry;
|
||||
border-radius: var(--container-border-radius);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -12px;
|
||||
right: -12px;
|
||||
bottom: -8px;
|
||||
border: 1px solid $color-blueberry;
|
||||
border-radius: 4px;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
animation: ppcp-setting-highlight-bg 2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes ppcp-highlight-fade {
|
||||
0%, 20% {
|
||||
background-color: rgba($color-blueberry, 0.08);
|
||||
border-color: $color-blueberry;
|
||||
border-width: 1px;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -12px;
|
||||
width: 4px;
|
||||
bottom: -8px;
|
||||
background-color: $color-blueberry;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
animation: ppcp-setting-highlight-accent 2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
border-color: $color-gray-300;
|
||||
border-width: 1px;
|
||||
|
||||
@keyframes ppcp-setting-highlight-bg {
|
||||
0%, 15% {
|
||||
background-color: rgba($color-blueberry, 0.08);
|
||||
border-color: $color-blueberry;
|
||||
}
|
||||
70% {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ppcp-setting-highlight-accent {
|
||||
0%, 15% {
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-r-settings-section {
|
||||
.ppcp--setting-row {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
margin: 0 -12px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-gray-100, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RTL support
|
||||
html[dir="rtl"] {
|
||||
.ppcp-highlight {
|
||||
&::after {
|
||||
left: auto;
|
||||
right: -12px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ import { ToggleControl } from '@wordpress/components';
|
|||
import { Action, Description } from '../Elements';
|
||||
|
||||
const ControlToggleButton = ( {
|
||||
id = '',
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
} ) => (
|
||||
<Action>
|
||||
<Action id={ id }>
|
||||
<ToggleControl
|
||||
className="ppcp--control-toggle"
|
||||
__nextHasNoMarginBottom
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const Action = ( { children } ) => (
|
||||
<div className="ppcp--action">{ children }</div>
|
||||
const Action = ( { id, children } ) => (
|
||||
<div className="ppcp--action" { ...( id ? { id } : {} ) }>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Action;
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import { Icon } from '@wordpress/components';
|
||||
|
||||
import data from '../../utils/data';
|
||||
|
||||
const PaymentMethodIcon = ( { icons, type } ) => {
|
||||
const validIcon = Array.isArray( icons ) && icons.includes( type );
|
||||
|
||||
if ( validIcon || icons === 'all' ) {
|
||||
return (
|
||||
<Icon
|
||||
icon={ data().getImage( 'icon-button-' + type + '.svg' ) }
|
||||
className="ppcp--method-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const PaymentMethodIcon = ( { type } ) => (
|
||||
<Icon
|
||||
icon={ data().getImage( `icon-button-${ type }.svg` ) }
|
||||
className="ppcp--method-icon"
|
||||
/>
|
||||
);
|
||||
|
||||
export default PaymentMethodIcon;
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import PaymentMethodIcon from './PaymentMethodIcon';
|
||||
|
||||
const PaymentMethodIcons = ( props ) => {
|
||||
return (
|
||||
<div className="ppcp-r-payment-method-icons">
|
||||
<PaymentMethodIcon type="paypal" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="venmo" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="visa" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="mastercard" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="amex" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="discover" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="apple-pay" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="google-pay" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="ideal" icons={ props.icons } />
|
||||
<PaymentMethodIcon type="bancontact" icons={ props.icons } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const PaymentMethodIcons = ( { icons = [] } ) => (
|
||||
<div className="ppcp-r-payment-method-icons">
|
||||
{ icons.map( ( type ) => (
|
||||
<PaymentMethodIcon key={ type } type={ type } />
|
||||
) ) }
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PaymentMethodIcons;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { ToggleControl, Icon, Button } from '@wordpress/components';
|
||||
import { cog } from '@wordpress/icons';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useActiveHighlight } from '../../../data/common/hooks';
|
||||
|
||||
import SettingsBlock from '../SettingsBlock';
|
||||
import PaymentMethodIcon from '../PaymentMethodIcon';
|
||||
|
@ -16,26 +14,12 @@ const PaymentMethodItemBlock = ( {
|
|||
disabledMessage,
|
||||
warningMessages,
|
||||
} ) => {
|
||||
const { activeHighlight, setActiveHighlight } = useActiveHighlight();
|
||||
const isHighlighted = activeHighlight === paymentMethod.id;
|
||||
const hasWarning =
|
||||
warningMessages && Object.keys( warningMessages ).length > 0;
|
||||
|
||||
// Reset the active highlight after 2 seconds
|
||||
useEffect( () => {
|
||||
if ( isHighlighted ) {
|
||||
const timer = setTimeout( () => {
|
||||
setActiveHighlight( null );
|
||||
}, 2000 );
|
||||
|
||||
return () => clearTimeout( timer );
|
||||
}
|
||||
}, [ isHighlighted, setActiveHighlight ] );
|
||||
|
||||
// Determine class names based on states
|
||||
const methodItemClasses = [
|
||||
'ppcp--method-item',
|
||||
isHighlighted ? 'ppcp-highlight' : '',
|
||||
isDisabled ? 'ppcp--method-item--disabled' : '',
|
||||
hasWarning && ! isDisabled ? 'ppcp--method-item--warning' : '',
|
||||
]
|
||||
|
|
|
@ -7,7 +7,6 @@ const TodoSettingsBlock = ( {
|
|||
todosData,
|
||||
className = '',
|
||||
setActiveModal,
|
||||
setActiveHighlight,
|
||||
onDismissTodo,
|
||||
} ) => {
|
||||
const [ dismissingIds, setDismissingIds ] = useState( new Set() );
|
||||
|
@ -44,22 +43,23 @@ const TodoSettingsBlock = ( {
|
|||
};
|
||||
|
||||
const handleClick = async ( todo ) => {
|
||||
if ( todo.action.type === 'tab' ) {
|
||||
const tabId = TAB_IDS[ todo.action.tab.toUpperCase() ];
|
||||
await selectTab( tabId, todo.action.section );
|
||||
} else if ( todo.action.type === 'external' ) {
|
||||
window.open( todo.action.url, '_blank' );
|
||||
const { action } = todo;
|
||||
const highlight = Boolean( action.highlight );
|
||||
|
||||
// Handle different action types.
|
||||
if ( action.type === 'tab' ) {
|
||||
const tabId = TAB_IDS[ action.tab.toUpperCase() ];
|
||||
await selectTab( tabId, action.section, highlight );
|
||||
} else if ( action.type === 'external' ) {
|
||||
window.open( action.url, '_blank' );
|
||||
}
|
||||
|
||||
if ( todo.action.completeOnClick === true ) {
|
||||
if ( action.completeOnClick ) {
|
||||
await completeOnClick( todo.id );
|
||||
}
|
||||
|
||||
if ( todo.action.modal ) {
|
||||
setActiveModal( todo.action.modal );
|
||||
}
|
||||
if ( todo.action.highlight ) {
|
||||
setActiveHighlight( todo.action.highlight );
|
||||
if ( action.modal ) {
|
||||
setActiveModal( action.modal );
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { usePaymentConfig } from '../hooks/usePaymentConfig';
|
|||
const PaymentFlow = ( {
|
||||
useAcdc,
|
||||
isFastlane,
|
||||
isPayLater,
|
||||
storeCountry,
|
||||
onlyOptional = false,
|
||||
} ) => {
|
||||
|
@ -18,7 +17,8 @@ const PaymentFlow = ( {
|
|||
optionalTitle,
|
||||
optionalDescription,
|
||||
learnMoreConfig,
|
||||
} = usePaymentConfig( storeCountry, isPayLater, useAcdc, isFastlane );
|
||||
paypalCheckoutDescription,
|
||||
} = usePaymentConfig( storeCountry, useAcdc, isFastlane );
|
||||
|
||||
if ( onlyOptional ) {
|
||||
return (
|
||||
|
@ -34,6 +34,7 @@ const PaymentFlow = ( {
|
|||
<DefaultMethodsSection
|
||||
methods={ includedMethods }
|
||||
learnMoreConfig={ learnMoreConfig }
|
||||
paypalCheckoutDescription={ paypalCheckoutDescription }
|
||||
/>
|
||||
|
||||
<OptionalMethodsSection
|
||||
|
@ -48,10 +49,17 @@ const PaymentFlow = ( {
|
|||
|
||||
export default PaymentFlow;
|
||||
|
||||
const DefaultMethodsSection = ( { methods, learnMoreConfig } ) => {
|
||||
const DefaultMethodsSection = ( {
|
||||
methods,
|
||||
learnMoreConfig,
|
||||
paypalCheckoutDescription,
|
||||
} ) => {
|
||||
return (
|
||||
<div className="ppcp-r-welcome-docs__col">
|
||||
<PayPalCheckout learnMore={ learnMoreConfig.PayPalCheckout } />
|
||||
<PayPalCheckout
|
||||
learnMore={ learnMoreConfig.PayPalCheckout }
|
||||
description={ paypalCheckoutDescription }
|
||||
/>
|
||||
<BadgeBox
|
||||
title={ __(
|
||||
'Included in PayPal Checkout',
|
||||
|
|
|
@ -5,15 +5,15 @@ import BadgeBox from '../../../../ReusableComponents/BadgeBox';
|
|||
|
||||
const PayPalCheckout = ( {
|
||||
learnMore = 'https://www.paypal.com/us/business/accept-payments/checkout',
|
||||
description,
|
||||
} ) => {
|
||||
const title = __( 'PayPal Checkout', 'woocommerce-paypal-payments' );
|
||||
|
||||
return (
|
||||
<BadgeBox
|
||||
title={ __( 'PayPal Checkout', 'woocommerce-paypal-payments' ) }
|
||||
title={ title }
|
||||
textBadge={ <PricingTitleBadge item="checkout" /> }
|
||||
description={ __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ description }
|
||||
learnMoreLink={ learnMore }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
|
|||
import PricingDescription from './PricingDescription';
|
||||
import PaymentFlow from './PaymentFlow';
|
||||
|
||||
const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => {
|
||||
const WelcomeDocs = ( { useAcdc, isFastlane, storeCountry } ) => {
|
||||
return (
|
||||
<div className="ppcp-r-welcome-docs">
|
||||
<h2 className="ppcp-r-welcome-docs__title">
|
||||
|
@ -15,7 +15,6 @@ const WelcomeDocs = ( { useAcdc, isFastlane, isPayLater, storeCountry } ) => {
|
|||
<PaymentFlow
|
||||
useAcdc={ useAcdc }
|
||||
isFastlane={ isFastlane }
|
||||
isPayLater={ isPayLater }
|
||||
storeCountry={ storeCountry }
|
||||
/>
|
||||
<PricingDescription />
|
||||
|
|
|
@ -59,29 +59,18 @@ const StepPaymentMethods = () => {
|
|||
export default StepPaymentMethods;
|
||||
|
||||
const PaymentStepTitle = () => {
|
||||
const { storeCountry } = CommonHooks.useWooSettings();
|
||||
|
||||
if ( 'US' === storeCountry ) {
|
||||
return __(
|
||||
'Add Expanded Checkout for More Ways to Pay',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
}
|
||||
|
||||
return __(
|
||||
'Add optional payment methods to your Checkout',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
return __( 'Add Credit and Debit Cards', 'woocommerce-paypal-payments' );
|
||||
};
|
||||
|
||||
const OptionalMethodDescription = () => {
|
||||
const { isCasualSeller } = OnboardingHooks.useBusiness();
|
||||
const { storeCountry, storeCurrency } = CommonHooks.useWooSettings();
|
||||
const { canUseCardPayments } = OnboardingHooks.useFlags();
|
||||
|
||||
return (
|
||||
<PaymentFlow
|
||||
onlyOptional={ true }
|
||||
useAcdc={ canUseCardPayments }
|
||||
useAcdc={ ! isCasualSeller && canUseCardPayments }
|
||||
isFastlane={ true }
|
||||
isPayLater={ true }
|
||||
storeCountry={ storeCountry }
|
||||
|
|
|
@ -9,12 +9,27 @@ import BusyStateWrapper from '../../../ReusableComponents/BusyStateWrapper';
|
|||
import OnboardingHeader from '../Components/OnboardingHeader';
|
||||
import WelcomeDocs from '../Components/WelcomeDocs';
|
||||
import AdvancedOptionsForm from '../Components/AdvancedOptionsForm';
|
||||
import { usePaymentConfig } from '../hooks/usePaymentConfig';
|
||||
|
||||
const StepWelcome = ( { setStep, currentStep } ) => {
|
||||
const { storeCountry } = CommonHooks.useWooSettings();
|
||||
const { canUseCardPayments, canUseFastlane, canUsePayLater } =
|
||||
OnboardingHooks.useFlags();
|
||||
const nonAcdcIcons = [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ];
|
||||
const { canUseCardPayments, canUseFastlane } = OnboardingHooks.useFlags();
|
||||
|
||||
const { acdcIcons, bcdcIcons } = usePaymentConfig(
|
||||
storeCountry,
|
||||
canUseCardPayments,
|
||||
canUseFastlane
|
||||
);
|
||||
|
||||
const onboardingHeaderDescription = canUseCardPayments
|
||||
? __(
|
||||
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
|
||||
'woocommerce-paypal-payments'
|
||||
)
|
||||
: __(
|
||||
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, and more.',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ppcp-r-page-welcome">
|
||||
|
@ -23,19 +38,16 @@ const StepWelcome = ( { setStep, currentStep } ) => {
|
|||
'Welcome to PayPal Payments',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ __(
|
||||
'Your all-in-one integration for PayPal checkout solutions that enable buyers to pay via PayPal, Pay Later, all major credit/debit cards, Apple Pay, Google Pay, and more.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ onboardingHeaderDescription }
|
||||
/>
|
||||
<div className="ppcp-r-inner-container">
|
||||
<WelcomeFeatures />
|
||||
<PaymentMethodIcons
|
||||
icons={ canUseCardPayments ? 'all' : nonAcdcIcons }
|
||||
icons={ canUseCardPayments ? acdcIcons : bcdcIcons }
|
||||
/>
|
||||
<p className="ppcp-r-button__description">
|
||||
{ __(
|
||||
`Click the button below to be guided through connecting your existing PayPal account or creating a new one.You will be able to choose the payment options that are right for your store.`,
|
||||
'Click the button below to be guided through connecting your existing PayPal account or creating a new one. You will be able to choose the payment options that are right for your store.',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
</p>
|
||||
|
@ -56,7 +68,6 @@ const StepWelcome = ( { setStep, currentStep } ) => {
|
|||
<WelcomeDocs
|
||||
useAcdc={ canUseCardPayments }
|
||||
isFastlane={ canUseFastlane }
|
||||
isPayLater={ canUsePayLater }
|
||||
storeCountry={ storeCountry }
|
||||
/>
|
||||
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />
|
||||
|
|
|
@ -28,6 +28,7 @@ const defaultConfig = {
|
|||
|
||||
// Extended: Items on right side for ACDC-flow.
|
||||
extendedMethods: [
|
||||
{ name: 'CardFields', Component: CardFields },
|
||||
{ name: 'DigitalWallets', Component: DigitalWallets },
|
||||
{ name: 'APMs', Component: AlternativePaymentMethods },
|
||||
],
|
||||
|
@ -41,6 +42,26 @@ const defaultConfig = {
|
|||
'with additional application',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
|
||||
// PayPal Checkout description.
|
||||
paypalCheckoutDescription: __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
|
||||
// Icon groups.
|
||||
bcdcIcons: [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ],
|
||||
acdcIcons: [
|
||||
'paypal',
|
||||
'visa',
|
||||
'mastercard',
|
||||
'amex',
|
||||
'discover',
|
||||
'apple-pay',
|
||||
'google-pay',
|
||||
'ideal',
|
||||
'bancontact',
|
||||
],
|
||||
};
|
||||
|
||||
const countrySpecificConfigs = {
|
||||
|
@ -60,11 +81,35 @@ const countrySpecificConfigs = {
|
|||
{ name: 'APMs', Component: AlternativePaymentMethods },
|
||||
{ name: 'Fastlane', Component: Fastlane },
|
||||
],
|
||||
paypalCheckoutDescription: __(
|
||||
'Our all-in-one checkout solution lets you offer PayPal, Venmo, Pay Later options, and more to help maximise conversion',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
optionalTitle: __( 'Expanded Checkout', 'woocommerce-paypal-payments' ),
|
||||
optionalDescription: __(
|
||||
'Accept debit/credit cards, PayPal, Apple Pay, Google Pay, and more. Note: Additional application required for more methods',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
bcdcIcons: [
|
||||
'paypal',
|
||||
'venmo',
|
||||
'visa',
|
||||
'mastercard',
|
||||
'amex',
|
||||
'discover',
|
||||
],
|
||||
acdcIcons: [
|
||||
'paypal',
|
||||
'venmo',
|
||||
'visa',
|
||||
'mastercard',
|
||||
'amex',
|
||||
'discover',
|
||||
'apple-pay',
|
||||
'google-pay',
|
||||
'ideal',
|
||||
'bancontact',
|
||||
],
|
||||
},
|
||||
GB: {
|
||||
includedMethods: [
|
||||
|
@ -72,6 +117,12 @@ const countrySpecificConfigs = {
|
|||
{ name: 'PayInThree', Component: PayInThree },
|
||||
],
|
||||
},
|
||||
MX: {
|
||||
extendedMethods: [
|
||||
{ name: 'CardFields', Component: CardFields },
|
||||
{ name: 'APMs', Component: AlternativePaymentMethods },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const filterMethods = ( methods, conditions ) => {
|
||||
|
@ -80,22 +131,12 @@ const filterMethods = ( methods, conditions ) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const usePaymentConfig = (
|
||||
country,
|
||||
isPayLater,
|
||||
useAcdc,
|
||||
isFastlane
|
||||
) => {
|
||||
export const usePaymentConfig = ( country, useAcdc, isFastlane ) => {
|
||||
return useMemo( () => {
|
||||
const countryConfig = countrySpecificConfigs[ country ] || {};
|
||||
const config = { ...defaultConfig, ...countryConfig };
|
||||
const learnMoreConfig = learnMoreLinks[ country ] || {};
|
||||
|
||||
// Filter the "left side" list. PayLater is conditional.
|
||||
const includedMethods = filterMethods( config.includedMethods, [
|
||||
( method ) => isPayLater || method.name !== 'PayLater',
|
||||
] );
|
||||
|
||||
// Determine the "right side" items: Either BCDC or ACDC items.
|
||||
const optionalMethods = useAcdc
|
||||
? config.extendedMethods
|
||||
|
@ -108,9 +149,10 @@ export const usePaymentConfig = (
|
|||
|
||||
return {
|
||||
...config,
|
||||
includedMethods,
|
||||
optionalMethods: availableOptionalMethods,
|
||||
learnMoreConfig,
|
||||
acdcIcons: config.acdcIcons,
|
||||
bcdcIcons: config.bcdcIcons,
|
||||
};
|
||||
}, [ country, isPayLater, useAcdc, isFastlane ] );
|
||||
}, [ country, useAcdc, isFastlane ] );
|
||||
};
|
||||
|
|
|
@ -3,7 +3,8 @@ import { FeatureSettingsBlock } from '../../../../../ReusableComponents/Settings
|
|||
import { Content } from '../../../../../ReusableComponents/Elements';
|
||||
import { TITLE_BADGE_POSITIVE } from '../../../../../ReusableComponents/TitleBadge';
|
||||
import { selectTab, TAB_IDS } from '../../../../../../utils/tabSelector';
|
||||
import { setActiveModal } from '../../../../../../data/common/actions';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../../../data/common';
|
||||
|
||||
const FeatureItem = ( {
|
||||
isBusy,
|
||||
|
@ -14,6 +15,7 @@ const FeatureItem = ( {
|
|||
enabled,
|
||||
notes,
|
||||
} ) => {
|
||||
const { setActiveModal } = useDispatch( COMMON_STORE_NAME );
|
||||
const getButtonUrl = ( button ) => {
|
||||
if ( button.urls ) {
|
||||
return isSandbox ? button.urls.sandbox : button.urls.live;
|
||||
|
@ -30,9 +32,11 @@ const FeatureItem = ( {
|
|||
);
|
||||
const handleClick = async ( feature ) => {
|
||||
if ( feature.action?.type === 'tab' ) {
|
||||
const highlight = Boolean( feature.action?.highlight );
|
||||
const tabId = TAB_IDS[ feature.action.tab.toUpperCase() ];
|
||||
await selectTab( tabId, feature.action.section );
|
||||
await selectTab( tabId, feature.action.section, highlight );
|
||||
}
|
||||
|
||||
if ( feature.action?.modal ) {
|
||||
setActiveModal( feature.action.modal );
|
||||
}
|
||||
|
|
|
@ -15,8 +15,7 @@ const Todos = () => {
|
|||
const [ isResetting, setIsResetting ] = useState( false );
|
||||
const { todos, isReady: areTodosReady, dismissTodo } = useTodos();
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { setActiveModal, setActiveHighlight } =
|
||||
useDispatch( COMMON_STORE_NAME );
|
||||
const { setActiveModal } = useDispatch( COMMON_STORE_NAME );
|
||||
const { resetDismissedTodos, setDismissedTodos } =
|
||||
useDispatch( TODOS_STORE_NAME );
|
||||
const { createSuccessNotice } = useDispatch( noticesStore );
|
||||
|
@ -76,7 +75,6 @@ const Todos = () => {
|
|||
<TodoSettingsBlock
|
||||
todosData={ todos }
|
||||
setActiveModal={ setActiveModal }
|
||||
setActiveHighlight={ setActiveHighlight }
|
||||
onDismissTodo={ dismissTodo }
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
|
|
@ -10,8 +10,9 @@ import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight';
|
|||
* @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
|
||||
const PaymentDependencyMessage = ( { parentId, parentName } ) => {
|
||||
const displayName = parentName || parentId;
|
||||
|
||||
return createInterpolateElement(
|
||||
/* translators: %s: payment method name */
|
||||
__(
|
||||
|
@ -28,7 +29,7 @@ const DependencyMessage = ( { parentId, parentName } ) => {
|
|||
scrollAndHighlight( parentId );
|
||||
} }
|
||||
>
|
||||
{ parentName }
|
||||
{ displayName }
|
||||
</a>
|
||||
</strong>
|
||||
),
|
||||
|
@ -36,4 +37,4 @@ const DependencyMessage = ( { parentId, parentName } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default DependencyMessage;
|
||||
export default PaymentDependencyMessage;
|
|
@ -1,21 +1,26 @@
|
|||
import { useEffect } from '@wordpress/element';
|
||||
import SettingsCard from '../../../../ReusableComponents/SettingsCard';
|
||||
import { PaymentMethodsBlock } from '../../../../ReusableComponents/SettingsBlocks';
|
||||
import usePaymentDependencyState from '../../../../../hooks/usePaymentDependencyState';
|
||||
import DependencyMessage from './DependencyMessage';
|
||||
import useSettingDependencyState from '../../../../../hooks/useSettingDependencyState';
|
||||
import PaymentDependencyMessage from './PaymentDependencyMessage';
|
||||
import SettingDependencyMessage from './SettingDependencyMessage';
|
||||
import SpinnerOverlay from '../../../../ReusableComponents/SpinnerOverlay';
|
||||
import { PaymentHooks, SettingsHooks } from '../../../../../data';
|
||||
import { useNavigation } from '../../../../../hooks/useNavigation';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
* @return {JSX.Element} The rendered component
|
||||
*/
|
||||
const PaymentMethodCard = ( {
|
||||
|
@ -27,9 +32,27 @@ const PaymentMethodCard = ( {
|
|||
methodsMap = {},
|
||||
onTriggerModal,
|
||||
isDisabled = false,
|
||||
disabledMessage,
|
||||
} ) => {
|
||||
const dependencyState = usePaymentDependencyState( methods, methodsMap );
|
||||
const { isReady: isPaymentStoreReady } = PaymentHooks.useStore();
|
||||
const { isReady: isSettingsStoreReady } = SettingsHooks.useStore();
|
||||
const { handleHighlightFromUrl } = useNavigation();
|
||||
|
||||
const paymentDependencies = usePaymentDependencyState(
|
||||
methods,
|
||||
methodsMap
|
||||
);
|
||||
|
||||
const settingDependencies = useSettingDependencyState( methods );
|
||||
|
||||
useEffect( () => {
|
||||
if ( isPaymentStoreReady && isSettingsStoreReady ) {
|
||||
handleHighlightFromUrl();
|
||||
}
|
||||
}, [ handleHighlightFromUrl, isPaymentStoreReady, isSettingsStoreReady ] );
|
||||
|
||||
if ( ! isPaymentStoreReady || ! isSettingsStoreReady ) {
|
||||
return <SpinnerOverlay asModal={ true } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
|
@ -41,25 +64,39 @@ const PaymentMethodCard = ( {
|
|||
>
|
||||
<PaymentMethodsBlock
|
||||
paymentMethods={ methods.map( ( method ) => {
|
||||
const dependency = dependencyState[ method.id ];
|
||||
const paymentDependency =
|
||||
paymentDependencies?.[ method.id ];
|
||||
const settingDependency =
|
||||
settingDependencies?.[ method.id ];
|
||||
|
||||
const dependencyMessage = dependency ? (
|
||||
<DependencyMessage
|
||||
parentId={ dependency.parentId }
|
||||
parentName={ dependency.parentName }
|
||||
/>
|
||||
) : null;
|
||||
let dependencyMessage = null;
|
||||
let isMethodDisabled = method.isDisabled || isDisabled;
|
||||
|
||||
if ( paymentDependency ) {
|
||||
dependencyMessage = (
|
||||
<PaymentDependencyMessage
|
||||
parentId={ paymentDependency.parentId }
|
||||
parentName={ paymentDependency.parentName }
|
||||
/>
|
||||
);
|
||||
isMethodDisabled = true;
|
||||
} else if ( settingDependency?.isDisabled ) {
|
||||
dependencyMessage = (
|
||||
<SettingDependencyMessage
|
||||
settingId={ settingDependency.settingId }
|
||||
requiredValue={
|
||||
settingDependency.requiredValue
|
||||
}
|
||||
methodId={ method.id }
|
||||
/>
|
||||
);
|
||||
isMethodDisabled = true;
|
||||
}
|
||||
|
||||
return {
|
||||
...method,
|
||||
isDisabled:
|
||||
method.isDisabled ||
|
||||
isDisabled ||
|
||||
Boolean( dependency?.isDisabled ),
|
||||
disabledMessage:
|
||||
method.disabledMessage ||
|
||||
dependencyMessage ||
|
||||
disabledMessage,
|
||||
isDisabled: isMethodDisabled,
|
||||
disabledMessage: dependencyMessage,
|
||||
};
|
||||
} ) }
|
||||
onTriggerModal={ onTriggerModal }
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { selectTab, TAB_IDS } from '../../../../../utils/tabSelector';
|
||||
import { scrollAndHighlight } from '../../../../../utils/scrollAndHighlight';
|
||||
|
||||
/**
|
||||
* Transforms camelCase section IDs to kebab-case with ppcp prefix
|
||||
*
|
||||
* @param {string} sectionId - The original section ID in camelCase
|
||||
* @return {string} The transformed section ID in kebab-case with ppcp prefix
|
||||
*/
|
||||
const transformSectionId = ( sectionId ) => {
|
||||
if ( ! sectionId ) {
|
||||
return sectionId;
|
||||
}
|
||||
|
||||
// Convert camelCase to kebab-case.
|
||||
// This regex finds capital letters and replaces them with "-lowercase".
|
||||
const kebabCase = sectionId.replace( /([A-Z])/g, '-$1' ).toLowerCase();
|
||||
|
||||
// Add ppcp- prefix if it doesn't already have it.
|
||||
const prefixed = kebabCase.startsWith( 'ppcp-' )
|
||||
? kebabCase
|
||||
: `ppcp-${ kebabCase }`;
|
||||
|
||||
return prefixed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a setting link element
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.settingName - Display name for the setting
|
||||
* @param {string} props.sectionId - Section ID to scroll to
|
||||
* @return {JSX.Element} The formatted link element
|
||||
*/
|
||||
const SettingLink = ( { settingName, sectionId } ) => (
|
||||
<strong>
|
||||
<a
|
||||
href="#"
|
||||
onClick={ ( e ) => {
|
||||
e.preventDefault();
|
||||
|
||||
if ( sectionId ) {
|
||||
const tabId = TAB_IDS.SETTINGS;
|
||||
|
||||
// Transform the section ID before passing to selectTab.
|
||||
const transformedSectionId =
|
||||
transformSectionId( sectionId );
|
||||
|
||||
selectTab( tabId );
|
||||
|
||||
setTimeout( () => {
|
||||
scrollAndHighlight( transformedSectionId );
|
||||
}, 100 );
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ settingName }
|
||||
</a>
|
||||
</strong>
|
||||
);
|
||||
|
||||
/**
|
||||
* Component to display a setting dependency message
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.settingId - ID of the required setting
|
||||
* @param {*} props.requiredValue - Required value for the setting
|
||||
* @return {JSX.Element} The formatted message
|
||||
*/
|
||||
const SettingDependencyMessage = ( { settingId, requiredValue } ) => {
|
||||
// Setting names mapping.
|
||||
const settingNames = {
|
||||
savePaypalAndVenmo: 'Save PayPal and Venmo',
|
||||
};
|
||||
|
||||
// Get a human-friendly setting name.
|
||||
const settingName = settingNames[ settingId ] || settingId;
|
||||
|
||||
const settingLink = (
|
||||
<SettingLink settingName={ settingName } sectionId={ settingId } />
|
||||
);
|
||||
|
||||
const templates = {
|
||||
true: __(
|
||||
'This payment method requires <settingLink /> to be enabled.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
false: __(
|
||||
'This payment method requires <settingLink /> to be disabled.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
};
|
||||
|
||||
return typeof requiredValue === 'boolean'
|
||||
? createInterpolateElement( templates[ requiredValue ], {
|
||||
settingLink,
|
||||
} )
|
||||
: createInterpolateElement(
|
||||
sprintf(
|
||||
/* translators: %s: required setting value */
|
||||
__(
|
||||
'This payment method requires <settingLink /> to be set to "%s".',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
requiredValue
|
||||
),
|
||||
{ settingLink }
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingDependencyMessage;
|
|
@ -28,18 +28,18 @@ const SavePaymentMethods = () => {
|
|||
className="ppcp--save-payment-methods"
|
||||
>
|
||||
<ControlToggleButton
|
||||
id="ppcp-save-paypal-and-venmo"
|
||||
label={ __(
|
||||
'Save PayPal and Venmo',
|
||||
'woocommerce-paypal-payments'
|
||||
) }
|
||||
description={ sprintf(
|
||||
/* translators: 1: URL to Pay Later documentation, 2: URL to Alternative Payment Methods documentation */
|
||||
/* translators: 1: URL to Pay Later documentation */
|
||||
__(
|
||||
'Securely store your customers\' PayPal accounts for a seamless checkout experience. <br />This will disable all <a target="_blank" rel="noreferrer" href="%1$s">Pay Later</a> features and <a target="_blank" rel="noreferrer" href="%2$s">Alternative Payment Methods</a> on your site.',
|
||||
'Securely store your customers\' PayPal accounts for a seamless checkout experience. <br />This will disable the <a target="_blank" rel="noreferrer" href="%1$s">Pay Later</a> payment method on your site.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later',
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
|
||||
'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later'
|
||||
) }
|
||||
value={
|
||||
features.save_paypal_and_venmo.enabled
|
||||
|
|
|
@ -41,45 +41,27 @@ export function refresh() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Side effect. Fetches the ISU-login URL for a sandbox account.
|
||||
* Side effect. Fetches the ISU-login URL for an account.
|
||||
*
|
||||
* @param {string[]} [products=[]] Which products/features to display in the ISU popup.
|
||||
* @param {Object} [options={}] Options to customize the onboarding workflow.
|
||||
* @param isSandbox True if is sandbox, otherwise false.
|
||||
* @return {Function} The thunk function.
|
||||
*/
|
||||
export function sandboxOnboardingUrl() {
|
||||
export function onboardingUrl(
|
||||
products = [],
|
||||
options = {},
|
||||
isSandbox = false
|
||||
) {
|
||||
return async () => {
|
||||
try {
|
||||
return apiFetch( {
|
||||
path: REST_CONNECTION_URL_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
useSandbox: true,
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
return {
|
||||
success: false,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Side effect. Fetches the ISU-login URL for a production account.
|
||||
*
|
||||
* @param {string[]} products Which products/features to display in the ISU popup.
|
||||
* @return {Function} The thunk function.
|
||||
*/
|
||||
export function productionOnboardingUrl( products = [] ) {
|
||||
return async () => {
|
||||
try {
|
||||
return apiFetch( {
|
||||
path: REST_CONNECTION_URL_PATH,
|
||||
method: 'POST',
|
||||
data: {
|
||||
useSandbox: false,
|
||||
useSandbox: isSandbox,
|
||||
products,
|
||||
options,
|
||||
},
|
||||
} );
|
||||
} catch ( e ) {
|
||||
|
|
|
@ -74,15 +74,6 @@ export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
|
|||
export const setActiveModal = ( activeModal ) =>
|
||||
setTransient( 'activeModal', activeModal );
|
||||
|
||||
/**
|
||||
* Transient. Sets the active settings highlight.
|
||||
*
|
||||
* @param {string} activeHighlight
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setActiveHighlight = ( activeHighlight ) =>
|
||||
setTransient( 'activeHighlight', activeHighlight );
|
||||
|
||||
/**
|
||||
* Persistent. Sets the sandbox mode on or off.
|
||||
*
|
||||
|
|
|
@ -41,8 +41,6 @@ const useHooks = () => {
|
|||
const { useTransient, usePersistent, dispatch, select } = useStoreData();
|
||||
const {
|
||||
persist,
|
||||
sandboxOnboardingUrl,
|
||||
productionOnboardingUrl,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
startWebhookSimulation,
|
||||
|
@ -51,11 +49,8 @@ const useHooks = () => {
|
|||
|
||||
// Transient accessors.
|
||||
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
|
||||
const [ activeHighlight, setActiveHighlight ] =
|
||||
useTransient( 'activeHighlight' );
|
||||
|
||||
// Persistent accessors.
|
||||
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
|
||||
const [ isManualConnectionMode, setManualConnectionMode ] = usePersistent(
|
||||
'useManualConnection'
|
||||
);
|
||||
|
@ -73,18 +68,10 @@ const useHooks = () => {
|
|||
return {
|
||||
activeModal,
|
||||
setActiveModal,
|
||||
activeHighlight,
|
||||
setActiveHighlight,
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
return savePersistent( setSandboxMode, state );
|
||||
},
|
||||
isManualConnectionMode,
|
||||
setManualConnectionMode: ( state ) => {
|
||||
return savePersistent( setManualConnectionMode, state );
|
||||
},
|
||||
sandboxOnboardingUrl,
|
||||
productionOnboardingUrl,
|
||||
authenticateWithCredentials,
|
||||
authenticateWithOAuth,
|
||||
wooSettings,
|
||||
|
@ -109,15 +96,25 @@ export const useStore = () => {
|
|||
};
|
||||
|
||||
export const useSandbox = () => {
|
||||
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
|
||||
const { dispatch, usePersistent } = useStoreData();
|
||||
const [ isSandboxMode, setSandboxMode ] = usePersistent( 'useSandbox' );
|
||||
const { onboardingUrl } = dispatch;
|
||||
|
||||
return { isSandboxMode, setSandboxMode, sandboxOnboardingUrl };
|
||||
return {
|
||||
isSandboxMode,
|
||||
setSandboxMode: ( state ) => {
|
||||
setSandboxMode( state );
|
||||
return dispatch.persist();
|
||||
},
|
||||
onboardingUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProduction = () => {
|
||||
const { productionOnboardingUrl } = useHooks();
|
||||
const { dispatch } = useStoreData();
|
||||
const { onboardingUrl } = dispatch;
|
||||
|
||||
return { productionOnboardingUrl };
|
||||
return { onboardingUrl };
|
||||
};
|
||||
|
||||
export const useAuthentication = () => {
|
||||
|
@ -226,11 +223,6 @@ export const useActiveModal = () => {
|
|||
return { activeModal, setActiveModal };
|
||||
};
|
||||
|
||||
export const useActiveHighlight = () => {
|
||||
const { activeHighlight, setActiveHighlight } = useHooks();
|
||||
return { activeHighlight, setActiveHighlight };
|
||||
};
|
||||
|
||||
/*
|
||||
* Busy state management hooks
|
||||
*/
|
||||
|
|
|
@ -24,3 +24,9 @@ export const PRODUCT_TYPES = {
|
|||
PHYSICAL: 'physical',
|
||||
SUBSCRIPTIONS: 'subscriptions',
|
||||
};
|
||||
|
||||
export const PAYPAL_PRODUCTS = {
|
||||
ACDC: 'PPCP',
|
||||
BCDC: 'EXPRESS_CHECKOUT',
|
||||
VAULTING: 'ADVANCED_VAULTING',
|
||||
};
|
||||
|
|
|
@ -19,10 +19,6 @@ const useHooks = () => {
|
|||
|
||||
// Read-only flags and derived state.
|
||||
const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
|
||||
const determineProducts = useSelect(
|
||||
( select ) => select( STORE_NAME ).determineProducts(),
|
||||
[]
|
||||
);
|
||||
|
||||
// Transient accessors.
|
||||
const [ isReady ] = useTransient( 'isReady' );
|
||||
|
@ -80,7 +76,6 @@ const useHooks = () => {
|
|||
);
|
||||
return savePersistent( setProducts, validProducts );
|
||||
},
|
||||
determineProducts,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -141,9 +136,9 @@ export const useNavigationState = () => {
|
|||
};
|
||||
|
||||
export const useDetermineProducts = () => {
|
||||
const { determineProducts } = useHooks();
|
||||
|
||||
return determineProducts;
|
||||
return useSelect( ( select ) => {
|
||||
return select( STORE_NAME ).determineProductsAndCaps();
|
||||
}, [] );
|
||||
};
|
||||
|
||||
export const useFlags = () => {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
import { PAYPAL_PRODUCTS, PRODUCT_TYPES } from './configuration';
|
||||
|
||||
const EMPTY_OBJ = Object.freeze( {} );
|
||||
|
||||
const getState = ( state ) => state || EMPTY_OBJ;
|
||||
|
@ -25,44 +27,79 @@ export const flags = ( state ) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Returns the products that we use for the production login link in the last onboarding step.
|
||||
* Returns details about products and capabilities to use for the production login link in
|
||||
* the last onboarding step.
|
||||
*
|
||||
* This selector does not return state-values, but uses the state to derive the products-array
|
||||
* that should be returned.
|
||||
*
|
||||
* @param {{}} state
|
||||
* @return {string[]} The ISU products, based on choices made in the onboarding wizard.
|
||||
* @return {{products:string[], options:{}}} The ISU products, based on choices made in the onboarding wizard.
|
||||
*/
|
||||
export const determineProducts = ( state ) => {
|
||||
const derivedProducts = [];
|
||||
export const determineProductsAndCaps = ( state ) => {
|
||||
/**
|
||||
* An array of product-names that are used to build an onboarding URL via the
|
||||
* PartnerReferrals API. To avoid confusion with the "products" property from the
|
||||
* Redux store, this collection has a distinct name.
|
||||
*
|
||||
* On server-side, this value is referred to as "products" again.
|
||||
*/
|
||||
const apiModules = [];
|
||||
|
||||
const { isCasualSeller, areOptionalPaymentMethodsEnabled } =
|
||||
/**
|
||||
* Internal options that are parsed by the PartnerReferrals class to customize
|
||||
* the API payload.
|
||||
*/
|
||||
const options = {
|
||||
useSubscriptions: false,
|
||||
useCardPayments: false,
|
||||
};
|
||||
|
||||
const { isCasualSeller, areOptionalPaymentMethodsEnabled, products } =
|
||||
persistentData( state );
|
||||
const { canUseVaulting, canUseCardPayments } = flags( state );
|
||||
const cardPaymentsEligibleAndSelected =
|
||||
canUseCardPayments && areOptionalPaymentMethodsEnabled;
|
||||
|
||||
if ( ! canUseCardPayments || ! areOptionalPaymentMethodsEnabled ) {
|
||||
if ( ! cardPaymentsEligibleAndSelected ) {
|
||||
/**
|
||||
* Branch 1: Credit Card Payments not available.
|
||||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
apiModules.push( PAYPAL_PRODUCTS.BCDC );
|
||||
|
||||
if ( products?.includes( PRODUCT_TYPES.SUBSCRIPTIONS ) ) {
|
||||
options.useSubscriptions = true;
|
||||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
apiModules.push( PAYPAL_PRODUCTS.VAULTING );
|
||||
}
|
||||
} else if ( isCasualSeller ) {
|
||||
/**
|
||||
* Branch 2: Merchant has no business.
|
||||
* The store uses the Express-checkout product.
|
||||
*/
|
||||
derivedProducts.push( 'EXPRESS_CHECKOUT' );
|
||||
apiModules.push( PAYPAL_PRODUCTS.BCDC );
|
||||
} else {
|
||||
/**
|
||||
* Branch 3: Merchant is business, and can use CC payments.
|
||||
* The store uses the advanced PPCP product.
|
||||
*
|
||||
* This is the only branch that can use subscriptions.
|
||||
*/
|
||||
derivedProducts.push( 'PPCP' );
|
||||
apiModules.push( PAYPAL_PRODUCTS.ACDC );
|
||||
|
||||
if ( products?.includes( PRODUCT_TYPES.SUBSCRIPTIONS ) ) {
|
||||
options.useSubscriptions = true;
|
||||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
apiModules.push( PAYPAL_PRODUCTS.VAULTING );
|
||||
}
|
||||
}
|
||||
|
||||
if ( canUseVaulting ) {
|
||||
derivedProducts.push( 'ADVANCED_VAULTING' );
|
||||
}
|
||||
options.useCardPayments = cardPaymentsEligibleAndSelected;
|
||||
|
||||
return derivedProducts;
|
||||
return { products: apiModules, options };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
import '@testing-library/jest-dom';
|
||||
|
||||
import { PRODUCT_TYPES } from './configuration';
|
||||
import { determineProductsAndCaps } from './selectors';
|
||||
|
||||
describe( 'determineProductsAndCaps selector [casual seller]', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when card payments are not available',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when optional payment methods are disabled',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: false,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT for casual sellers with card payments',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT and ADVANCED_VAULTING when card payments are not available but vaulting is',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should ignore SUBSCRIPTION product for casual sellers',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: true,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [ PRODUCT_TYPES.SUBSCRIPTIONS ],
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each( testCases )( '$name', ( { state, expected } ) => {
|
||||
const result = determineProductsAndCaps( state );
|
||||
expect( result ).toEqual( expected );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'determineProductsAndCaps selector [business seller]', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when card payments are not available',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT when optional payment methods are disabled',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: false,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return PPCP for business merchants with card payments',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: false },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'PPCP' ],
|
||||
options: { useSubscriptions: false, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should include ADVANCED_VAULTING when vaulting is available',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'PPCP', 'ADVANCED_VAULTING' ],
|
||||
options: { useSubscriptions: false, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should return EXPRESS_CHECKOUT and ADVANCED_VAULTING when card payments are not available but vaulting is',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [ PRODUCT_TYPES.VIRTUAL ],
|
||||
},
|
||||
flags: { canUseCardPayments: false, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'EXPRESS_CHECKOUT' ],
|
||||
options: { useSubscriptions: false, useCardPayments: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should enable the SUBSCRIPTIONS option when a business seller selects the subscriptions-product',
|
||||
state: {
|
||||
data: {
|
||||
isCasualSeller: false,
|
||||
areOptionalPaymentMethodsEnabled: true,
|
||||
products: [ PRODUCT_TYPES.SUBSCRIPTIONS ],
|
||||
},
|
||||
flags: { canUseCardPayments: true, canUseVaulting: true },
|
||||
},
|
||||
expected: {
|
||||
products: [ 'PPCP', 'ADVANCED_VAULTING' ],
|
||||
options: { useSubscriptions: true, useCardPayments: true },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each( testCases )( '$name', ( { state, expected } ) => {
|
||||
const result = determineProductsAndCaps( state );
|
||||
expect( result ).toEqual( expected );
|
||||
} );
|
||||
} );
|
|
@ -8,6 +8,7 @@ import * as hooks from './hooks';
|
|||
import * as resolvers from './resolvers';
|
||||
import { initTodoSync } from '../sync/todo-state-sync';
|
||||
import { initPaymentDependencySync } from '../sync/payment-methods-sync';
|
||||
import { initSettingBasedPaymentMethodsSync } from '../sync/setting-based-payment-methods-sync';
|
||||
|
||||
/**
|
||||
* Initializes and registers the settings store with WordPress data layer.
|
||||
|
@ -30,6 +31,7 @@ export const initStore = () => {
|
|||
|
||||
// Initialize payment method dependency sync.
|
||||
initPaymentDependencySync();
|
||||
initSettingBasedPaymentMethodsSync();
|
||||
|
||||
return Boolean( wp.data.select( STORE_NAME ) );
|
||||
};
|
||||
|
|
|
@ -44,6 +44,7 @@ const defaultPersistent = Object.freeze( {
|
|||
threeDSecure: 'no-3d-secure',
|
||||
fastlaneCardholderName: false,
|
||||
fastlaneDisplayWatermark: false,
|
||||
__meta: false,
|
||||
} );
|
||||
|
||||
// Reducer logic.
|
||||
|
|
|
@ -61,8 +61,10 @@ export const initPaymentDependencySync = () => {
|
|||
( [ key, method ] ) =>
|
||||
key !== '__meta' &&
|
||||
method &&
|
||||
method.depends_on &&
|
||||
method.depends_on.includes( changedId )
|
||||
method.depends_on_payment_methods &&
|
||||
method.depends_on_payment_methods.includes(
|
||||
changedId
|
||||
)
|
||||
)
|
||||
.map( ( [ key ] ) => key );
|
||||
|
||||
|
@ -115,11 +117,11 @@ const handleRestoreDependents = ( dependentIds, methods ) => {
|
|||
|
||||
const checkAllDependenciesSatisfied = ( methodId, methods ) => {
|
||||
const method = methods[ methodId ];
|
||||
if ( ! method || ! method.depends_on ) {
|
||||
if ( ! method || ! method.depends_on_payment_methods ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! method.depends_on.some( ( parentId ) => {
|
||||
return ! method.depends_on_payment_methods.some( ( parentId ) => {
|
||||
const parent = methods[ parentId ];
|
||||
return ! parent || parent.enabled === false;
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
import { subscribe, select } from '@wordpress/data';
|
||||
|
||||
// Store names
|
||||
const PAYMENT_STORE = 'wc/paypal/payment';
|
||||
const SETTINGS_STORE = 'wc/paypal/settings';
|
||||
|
||||
// Track original states of methods affected by settings
|
||||
const settingDependentStates = {};
|
||||
|
||||
/**
|
||||
* Initialize setting dependency synchronization
|
||||
*/
|
||||
export const initSettingBasedPaymentMethodsSync = () => {
|
||||
let previousSettingsState = null;
|
||||
let isProcessing = false;
|
||||
|
||||
const unsubscribe = subscribe( () => {
|
||||
if ( isProcessing ) {
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
// Get both settings and payment stores
|
||||
const settingsHooks = select( SETTINGS_STORE );
|
||||
const paymentHooks = select( PAYMENT_STORE );
|
||||
|
||||
if ( ! settingsHooks || ! paymentHooks ) {
|
||||
isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = settingsHooks.persistentData();
|
||||
const methods = paymentHooks.persistentData();
|
||||
|
||||
if ( ! settings || ! methods ) {
|
||||
isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! previousSettingsState ) {
|
||||
previousSettingsState = { ...settings };
|
||||
isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find which settings changed
|
||||
const changedSettings = Object.keys( settings ).filter(
|
||||
( key ) =>
|
||||
previousSettingsState[ key ] !== undefined &&
|
||||
settings[ key ] !== previousSettingsState[ key ]
|
||||
);
|
||||
|
||||
if ( changedSettings.length > 0 ) {
|
||||
// Process affected payment methods for each changed setting
|
||||
for ( const methodId in methods ) {
|
||||
if ( methodId === '__meta' || ! methods[ methodId ] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const method = methods[ methodId ];
|
||||
|
||||
// Skip methods without setting dependencies
|
||||
if ( ! method.depends_on_settings?.settings ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { settings: dependencySettings } =
|
||||
method.depends_on_settings;
|
||||
|
||||
// Check if any of the changed settings affects this method
|
||||
const relevantSettings = Object.values(
|
||||
dependencySettings
|
||||
).filter( ( setting ) =>
|
||||
changedSettings.includes( setting.id )
|
||||
);
|
||||
|
||||
if ( relevantSettings.length > 0 ) {
|
||||
// Determine if method should be disabled based on new setting values
|
||||
const shouldBeDisabled = relevantSettings.some(
|
||||
( setting ) =>
|
||||
settings[ setting.id ] !== setting.value
|
||||
);
|
||||
|
||||
if ( shouldBeDisabled ) {
|
||||
// Store original state before disabling
|
||||
if ( ! ( methodId in settingDependentStates ) ) {
|
||||
settingDependentStates[ methodId ] =
|
||||
method.enabled;
|
||||
}
|
||||
|
||||
// Disable the method
|
||||
methods[ methodId ].enabled = false;
|
||||
methods[ methodId ].isDisabled = true;
|
||||
} else {
|
||||
// Check if all setting dependencies are now satisfied
|
||||
const allSettingsSatisfied = Object.values(
|
||||
dependencySettings
|
||||
).every(
|
||||
( setting ) =>
|
||||
settings[ setting.id ] === setting.value
|
||||
);
|
||||
|
||||
// Also check payment method dependencies
|
||||
const paymentDependenciesSatisfied =
|
||||
checkPaymentDependenciesSatisfied(
|
||||
methodId,
|
||||
methods
|
||||
);
|
||||
|
||||
// If all dependencies are satisfied, restore the original state
|
||||
if (
|
||||
allSettingsSatisfied &&
|
||||
paymentDependenciesSatisfied &&
|
||||
methodId in settingDependentStates
|
||||
) {
|
||||
methods[ methodId ].enabled =
|
||||
settingDependentStates[ methodId ];
|
||||
methods[ methodId ].isDisabled = false;
|
||||
delete settingDependentStates[ methodId ];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousSettingsState = { ...settings };
|
||||
} catch ( error ) {
|
||||
// Silent error handling
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
} );
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if all payment method dependencies are satisfied for a method
|
||||
*
|
||||
* @param {string} methodId - ID of the method to check
|
||||
* @param {Object} methods - All payment methods
|
||||
* @return {boolean} True if all dependencies are satisfied
|
||||
*/
|
||||
const checkPaymentDependenciesSatisfied = ( methodId, methods ) => {
|
||||
const method = methods[ methodId ];
|
||||
if ( ! method || ! method.depends_on_payment_methods ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! method.depends_on_payment_methods.some( ( parentId ) => {
|
||||
const parent = methods[ parentId ];
|
||||
return ! parent || parent.enabled === false;
|
||||
} );
|
||||
};
|
||||
|
||||
export default initSettingBasedPaymentMethodsSync;
|
|
@ -28,23 +28,19 @@ const ACTIVITIES = {
|
|||
};
|
||||
|
||||
export const useHandleOnboardingButton = ( isSandbox ) => {
|
||||
const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
|
||||
const { productionOnboardingUrl } = CommonHooks.useProduction();
|
||||
const products = OnboardingHooks.useDetermineProducts();
|
||||
const { onboardingUrl } = isSandbox
|
||||
? CommonHooks.useSandbox()
|
||||
: CommonHooks.useProduction();
|
||||
const { products, options } = OnboardingHooks.useDetermineProducts();
|
||||
const { startActivity } = CommonHooks.useBusyState();
|
||||
const { authenticateWithOAuth } = CommonHooks.useAuthentication();
|
||||
const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
|
||||
const [ onboardingUrlState, setOnboardingUrl ] = useState( '' );
|
||||
const [ scriptLoaded, setScriptLoaded ] = useState( false );
|
||||
const timerRef = useRef( null );
|
||||
|
||||
useEffect( () => {
|
||||
const fetchOnboardingUrl = async () => {
|
||||
let res;
|
||||
if ( isSandbox ) {
|
||||
res = await sandboxOnboardingUrl();
|
||||
} else {
|
||||
res = await productionOnboardingUrl( products );
|
||||
}
|
||||
const res = await onboardingUrl( products, options, isSandbox );
|
||||
|
||||
if ( res.success && res.data ) {
|
||||
setOnboardingUrl( res.data );
|
||||
|
@ -54,7 +50,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
|
|||
};
|
||||
|
||||
fetchOnboardingUrl();
|
||||
}, [ isSandbox, productionOnboardingUrl, products, sandboxOnboardingUrl ] );
|
||||
}, [ isSandbox, products, options, onboardingUrl ] );
|
||||
|
||||
useEffect( () => {
|
||||
/**
|
||||
|
@ -62,7 +58,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
|
|||
* When no buttons are present, a JS error is displayed; i.e. we should load this script
|
||||
* only when the button is ready (with a valid href and data-attributes).
|
||||
*/
|
||||
if ( ! onboardingUrl ) {
|
||||
if ( ! onboardingUrlState ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -97,7 +93,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
|
|||
}
|
||||
} );
|
||||
};
|
||||
}, [ onboardingUrl ] );
|
||||
}, [ onboardingUrlState ] );
|
||||
|
||||
const setCompleteHandler = useCallback(
|
||||
( environment ) => {
|
||||
|
@ -119,7 +115,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
|
|||
await authenticateWithOAuth(
|
||||
sharedId,
|
||||
authCode,
|
||||
'sandbox' === environment
|
||||
environment === 'sandbox'
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -148,7 +144,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
|
|||
}, [] );
|
||||
|
||||
return {
|
||||
onboardingUrl,
|
||||
onboardingUrl: onboardingUrlState,
|
||||
scriptLoaded,
|
||||
setCompleteHandler,
|
||||
removeCompleteHandler,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { scrollAndHighlight } from '../utils/scrollAndHighlight';
|
||||
|
||||
/**
|
||||
* Navigate to the WooCommerce "Payments" settings tab, i.e. exit the settings app.
|
||||
*/
|
||||
|
@ -22,9 +24,38 @@ const goToPluginSettings = ( panel = null ) => {
|
|||
window.location.href = url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check URL for highlight parameter and scroll to the element if present.
|
||||
*
|
||||
* @return {boolean} Whether a highlight parameter was found and processed
|
||||
*/
|
||||
const handleHighlightFromUrl = () => {
|
||||
const urlParams = new URLSearchParams( window.location.search );
|
||||
const elementId = urlParams.get( 'highlight' );
|
||||
|
||||
if ( elementId ) {
|
||||
setTimeout( () => {
|
||||
scrollAndHighlight( elementId );
|
||||
|
||||
// Clean up the URL by removing the highlight parameter.
|
||||
urlParams.delete( 'highlight' );
|
||||
const newUrl =
|
||||
window.location.pathname +
|
||||
( urlParams.toString() ? '?' + urlParams.toString() : '' ) +
|
||||
window.location.hash;
|
||||
|
||||
window.history.replaceState( {}, document.title, newUrl );
|
||||
}, 100 );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useNavigation = () => {
|
||||
return {
|
||||
goToWooCommercePaymentsTab,
|
||||
goToPluginSettings,
|
||||
handleHighlightFromUrl,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* Custom hook to handle payment-method-based dependencies
|
||||
*/
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
|
@ -22,60 +25,54 @@ const getParentMethodName = ( parentId, methodsMap ) => {
|
|||
* @return {Array} List of disabled parent IDs, empty if none
|
||||
*/
|
||||
const findDisabledParents = ( method, methodsMap ) => {
|
||||
if ( ! method.depends_on?.length && ! method._disabledByDependency ) {
|
||||
const dependencies = method.depends_on_payment_methods;
|
||||
|
||||
if ( ! dependencies || ! Array.isArray( dependencies ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parents = method.depends_on || [];
|
||||
|
||||
return parents.filter( ( parentId ) => {
|
||||
return dependencies.filter( ( parentId ) => {
|
||||
const parent = methodsMap[ parentId ];
|
||||
return parent && ! parent.enabled;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to handle payment method dependencies
|
||||
* Hook to evaluate 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
|
||||
* @return {Object} Dependency state object keyed by method ID
|
||||
*/
|
||||
const usePaymentDependencyState = ( methods, methodsMap ) => {
|
||||
return useSelect(
|
||||
( select ) => {
|
||||
const paymentStore = select( 'wc/paypal/payment' );
|
||||
if ( ! paymentStore ) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = {};
|
||||
return useSelect( () => {
|
||||
const result = {};
|
||||
|
||||
if ( methods && methodsMap && Object.keys( methodsMap ).length > 0 ) {
|
||||
methods.forEach( ( method ) => {
|
||||
const disabledParents = findDisabledParents(
|
||||
method,
|
||||
methodsMap
|
||||
);
|
||||
|
||||
if ( disabledParents.length > 0 ) {
|
||||
const parentId = disabledParents[ 0 ];
|
||||
const parentName = getParentMethodName(
|
||||
parentId,
|
||||
if ( method && method.id ) {
|
||||
const disabledParents = findDisabledParents(
|
||||
method,
|
||||
methodsMap
|
||||
);
|
||||
|
||||
result[ method.id ] = {
|
||||
isDisabled: true,
|
||||
parentId,
|
||||
parentName,
|
||||
};
|
||||
if ( disabledParents.length > 0 ) {
|
||||
const parentId = disabledParents[ 0 ];
|
||||
result[ method.id ] = {
|
||||
isDisabled: true,
|
||||
parentId,
|
||||
parentName: getParentMethodName(
|
||||
parentId,
|
||||
methodsMap
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[ methods, methodsMap ]
|
||||
);
|
||||
return result;
|
||||
}, [ methods, methodsMap ] );
|
||||
};
|
||||
|
||||
export default usePaymentDependencyState;
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Custom hook to handle setting-based payment method dependencies
|
||||
*/
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Check setting dependencies for methods
|
||||
*
|
||||
* @param {Array} methods - Array of methods to check
|
||||
* @return {Object} Setting dependency states mapped by method ID
|
||||
*/
|
||||
const useSettingDependencyState = ( methods ) => {
|
||||
const dependencyState = useSelect(
|
||||
( select ) => {
|
||||
const settingsStore = select( 'wc/paypal/settings' );
|
||||
|
||||
if ( ! settingsStore || ! methods?.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get settings data
|
||||
const persistentData = settingsStore.persistentData();
|
||||
const result = {};
|
||||
|
||||
// Process each method
|
||||
methods.forEach( ( method ) => {
|
||||
if ( ! method?.id || ! method.depends_on_settings ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the settings object structure
|
||||
if ( method.depends_on_settings.settings ) {
|
||||
const settingsObj = method.depends_on_settings.settings;
|
||||
|
||||
for ( const [ settingId, settingData ] of Object.entries(
|
||||
settingsObj
|
||||
) ) {
|
||||
const requiredId = settingData.id;
|
||||
const requiredValue = settingData.value;
|
||||
|
||||
const actualValue = persistentData[ requiredId ];
|
||||
|
||||
// Check if dependency is satisfied
|
||||
if ( actualValue !== requiredValue ) {
|
||||
result[ method.id ] = {
|
||||
isDisabled: true,
|
||||
settingId: requiredId,
|
||||
requiredValue,
|
||||
};
|
||||
break; // Stop checking once we find a failed dependency
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
return result;
|
||||
},
|
||||
[ methods ]
|
||||
);
|
||||
|
||||
return dependencyState;
|
||||
};
|
||||
|
||||
export default useSettingDependencyState;
|
|
@ -15,18 +15,19 @@ import { scrollAndHighlight } from './scrollAndHighlight';
|
|||
*
|
||||
* TODO: Once the TabPanel gets migrated to Tabs (TabPanel v2) we need to remove this in favor of programmatic tab switching: https://github.com/WordPress/gutenberg/issues/52997
|
||||
*
|
||||
* @param {string} tabId - The ID of the tab to select
|
||||
* @param {string} [scrollToId] - Optional ID of the element to scroll to
|
||||
* @param {string} tabId - The ID of the tab to select
|
||||
* @param {string} [scrollToId] - Optional ID of the element to scroll to
|
||||
* @param {boolean} highlight - Whether to highlight the element after scrolling to it
|
||||
* @return {Promise} - Resolves when tab switch and scroll are complete
|
||||
*/
|
||||
export const selectTab = ( tabId, scrollToId ) => {
|
||||
export const selectTab = ( tabId, scrollToId, highlight = false ) => {
|
||||
return new Promise( ( resolve ) => {
|
||||
const tab = document.getElementById( tabId );
|
||||
if ( tab ) {
|
||||
tab.click();
|
||||
setTimeout( () => {
|
||||
const targetId = scrollToId || 'ppcp-settings-container';
|
||||
scrollAndHighlight( targetId, false ).then( resolve );
|
||||
scrollAndHighlight( targetId, highlight ).then( resolve );
|
||||
}, 100 );
|
||||
} else {
|
||||
console.error(
|
||||
|
|
|
@ -10,7 +10,18 @@ declare( strict_types = 1 );
|
|||
namespace WooCommerce\PayPalCommerce\Settings;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
|
||||
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\Settings\Ajax\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\FeaturesDefinition;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
|
||||
|
@ -37,6 +48,7 @@ use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
|
|||
use WooCommerce\PayPalCommerce\Settings\Service\AuthenticationManager;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\TodosSortingAndFilteringService;
|
||||
|
@ -45,6 +57,11 @@ use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
|
|||
use WooCommerce\PayPalCommerce\Settings\Service\SettingsDataManager;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
|
||||
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Factory\ConfigFactory;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
|
@ -181,7 +198,8 @@ return array(
|
|||
'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
|
||||
return new PaymentRestEndpoint(
|
||||
$container->get( 'settings.data.payment' ),
|
||||
$container->get( 'settings.data.definition.methods' )
|
||||
$container->get( 'settings.data.definition.methods' ),
|
||||
$container->get( 'settings.data.definition.method_dependencies' )
|
||||
);
|
||||
},
|
||||
'settings.rest.styling' => static function ( ContainerInterface $container ) : StylingRestEndpoint {
|
||||
|
@ -380,12 +398,11 @@ return array(
|
|||
|
||||
return new PaymentMethodsDefinition(
|
||||
$container->get( 'settings.data.payment' ),
|
||||
$container->get( 'settings.data.definition.method_dependencies' ),
|
||||
$axo_notices
|
||||
);
|
||||
},
|
||||
'settings.data.definition.method_dependencies' => static function ( ContainerInterface $container ) : PaymentMethodsDependenciesDefinition {
|
||||
return new PaymentMethodsDependenciesDefinition();
|
||||
return new PaymentMethodsDependenciesDefinition( $container->get( 'wcgateway.settings' ) );
|
||||
},
|
||||
'settings.service.pay_later_status' => static function ( ContainerInterface $container ) : array {
|
||||
$pay_later_endpoint = $container->get( 'settings.rest.pay_later_messaging' );
|
||||
|
@ -539,7 +556,8 @@ return array(
|
|||
return new FeaturesDefinition(
|
||||
$container->get( 'settings.service.features_eligibilities' ),
|
||||
$container->get( 'settings.data.general' ),
|
||||
$merchant_capabilities
|
||||
$merchant_capabilities,
|
||||
$container->get( 'settings.data.settings' )
|
||||
);
|
||||
},
|
||||
'settings.service.features_eligibilities' => static function( ContainerInterface $container ): FeaturesEligibilityService {
|
||||
|
@ -566,4 +584,32 @@ return array(
|
|||
$container->get( 'settings.data.todos' )
|
||||
);
|
||||
},
|
||||
'settings.service.gateway-redirect' => static function (): GatewayRedirectService {
|
||||
return new GatewayRedirectService();
|
||||
},
|
||||
/**
|
||||
* Returns a list of all payment gateway IDs created by this plugin.
|
||||
*
|
||||
* @returns string[] The list of all gateway IDs.
|
||||
*/
|
||||
'settings.config.all-gateway-ids' => static function (): array {
|
||||
return array(
|
||||
PayPalGateway::ID,
|
||||
CardButtonGateway::ID,
|
||||
CreditCardGateway::ID,
|
||||
AxoGateway::ID,
|
||||
ApplePayGateway::ID,
|
||||
GooglePayGateway::ID,
|
||||
BancontactGateway::ID,
|
||||
BlikGateway::ID,
|
||||
EPSGateway::ID,
|
||||
IDealGateway::ID,
|
||||
MyBankGateway::ID,
|
||||
P24Gateway::ID,
|
||||
TrustlyGateway::ID,
|
||||
MultibancoGateway::ID,
|
||||
PayUponInvoiceGateway::ID,
|
||||
OXXO::ID,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\FeaturesEligibilityService;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
|
||||
|
@ -42,21 +43,31 @@ class FeaturesDefinition {
|
|||
*/
|
||||
protected array $merchant_capabilities;
|
||||
|
||||
/**
|
||||
* The plugin settings.
|
||||
*
|
||||
* @var SettingsModel
|
||||
*/
|
||||
protected SettingsModel $plugin_settings;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param FeaturesEligibilityService $eligibilities The features eligibility service.
|
||||
* @param GeneralSettings $settings The general settings service.
|
||||
* @param array $merchant_capabilities The merchant capabilities.
|
||||
* @param SettingsModel $plugin_settings The plugin settings.
|
||||
*/
|
||||
public function __construct(
|
||||
FeaturesEligibilityService $eligibilities,
|
||||
GeneralSettings $settings,
|
||||
array $merchant_capabilities
|
||||
array $merchant_capabilities,
|
||||
SettingsModel $plugin_settings
|
||||
) {
|
||||
$this->eligibilities = $eligibilities;
|
||||
$this->settings = $settings;
|
||||
$this->merchant_capabilities = $merchant_capabilities;
|
||||
$this->plugin_settings = $plugin_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,7 +93,7 @@ class FeaturesDefinition {
|
|||
* @return array[] The array of all available features.
|
||||
*/
|
||||
public function all_available_features(): array {
|
||||
$paylater_countries = array(
|
||||
$paylater_countries = array(
|
||||
'UK',
|
||||
'ES',
|
||||
'IT',
|
||||
|
@ -91,8 +102,9 @@ class FeaturesDefinition {
|
|||
'DE',
|
||||
'AU',
|
||||
);
|
||||
$store_country = $this->settings->get_woo_settings()['country'];
|
||||
$country_location = in_array( $store_country, $paylater_countries, true ) ? strtolower( $store_country ) : 'us';
|
||||
$store_country = $this->settings->get_woo_settings()['country'];
|
||||
$country_location = in_array( $store_country, $paylater_countries, true ) ? strtolower( $store_country ) : 'us';
|
||||
$save_paypal_and_venmo = $this->plugin_settings->get_save_paypal_and_venmo();
|
||||
|
||||
return array(
|
||||
'save_paypal_and_venmo' => array(
|
||||
|
@ -104,8 +116,9 @@ class FeaturesDefinition {
|
|||
'type' => 'secondary',
|
||||
'text' => __( 'Configure', 'woocommerce-paypal-payments' ),
|
||||
'action' => array(
|
||||
'type' => 'tab',
|
||||
'tab' => 'settings',
|
||||
'type' => 'tab',
|
||||
'tab' => 'settings',
|
||||
'section' => 'ppcp-save-paypal-and-venmo',
|
||||
),
|
||||
'showWhen' => 'enabled',
|
||||
'class' => 'small-button',
|
||||
|
@ -207,7 +220,7 @@ class FeaturesDefinition {
|
|||
'action' => array(
|
||||
'type' => 'tab',
|
||||
'tab' => 'payment_methods',
|
||||
'section' => 'ppcp-card-payments-card',
|
||||
'section' => 'ppcp-googlepay',
|
||||
'highlight' => 'ppcp-googlepay',
|
||||
'modal' => 'ppcp-googlepay',
|
||||
),
|
||||
|
@ -287,7 +300,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'
|
||||
),
|
||||
'enabled' => $this->merchant_capabilities['pay_later'],
|
||||
'enabled' => $this->merchant_capabilities['pay_later'] && ! $save_paypal_and_venmo,
|
||||
'buttons' => array(
|
||||
array(
|
||||
'type' => 'secondary',
|
||||
|
|
|
@ -40,13 +40,6 @@ class PaymentMethodsDefinition {
|
|||
*/
|
||||
private PaymentSettings $settings;
|
||||
|
||||
/**
|
||||
* Payment method dependencies definition.
|
||||
*
|
||||
* @var PaymentMethodsDependenciesDefinition
|
||||
*/
|
||||
private PaymentMethodsDependenciesDefinition $dependencies_definition;
|
||||
|
||||
/**
|
||||
* Conflict notices for Axo gateway.
|
||||
*
|
||||
|
@ -64,18 +57,15 @@ class PaymentMethodsDefinition {
|
|||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param PaymentSettings $settings Payment methods data model.
|
||||
* @param PaymentMethodsDependenciesDefinition $dependencies_definition Payment dependencies definition.
|
||||
* @param array $axo_conflicts_notices Conflicts notices for Axo.
|
||||
* @param PaymentSettings $settings Payment methods data model.
|
||||
* @param array $axo_conflicts_notices Conflicts notices for Axo.
|
||||
*/
|
||||
public function __construct(
|
||||
PaymentSettings $settings,
|
||||
PaymentMethodsDependenciesDefinition $dependencies_definition,
|
||||
array $axo_conflicts_notices = array()
|
||||
) {
|
||||
$this->settings = $settings;
|
||||
$this->dependencies_definition = $dependencies_definition;
|
||||
$this->axo_conflicts_notices = $axo_conflicts_notices;
|
||||
$this->settings = $settings;
|
||||
$this->axo_conflicts_notices = $axo_conflicts_notices;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,26 +84,16 @@ class PaymentMethodsDefinition {
|
|||
$result = array();
|
||||
foreach ( $all_methods as $method ) {
|
||||
$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(),
|
||||
$depends_on,
|
||||
$method['warningMessages'] ?? array()
|
||||
$method['warningMessages'] ?? array(),
|
||||
);
|
||||
}
|
||||
// Add dependency maps to metadata.
|
||||
$result['__meta'] = array(
|
||||
'dependencies' => $this->dependencies_definition->get_dependencies(),
|
||||
'dependents' => $this->dependencies_definition->get_dependents_map(),
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
@ -121,14 +101,13 @@ class PaymentMethodsDefinition {
|
|||
* Returns a new payment method configuration array that contains all
|
||||
* common attributes which must be present in every method definition.
|
||||
*
|
||||
* @param string $gateway_id The payment method ID.
|
||||
* @param string $title Admin-side payment method title.
|
||||
* @param string $description Admin-side info about the payment method.
|
||||
* @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.
|
||||
* @param array $warning_messages Optional. Warning messages to display in the UI.
|
||||
* @param string $gateway_id The payment method ID.
|
||||
* @param string $title Admin-side payment method title.
|
||||
* @param string $description Admin-side info about the payment method.
|
||||
* @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 $warning_messages Optional. Warning messages to display in the UI.
|
||||
* @return array Payment method definition.
|
||||
*/
|
||||
private function build_method_definition(
|
||||
|
@ -136,8 +115,7 @@ class PaymentMethodsDefinition {
|
|||
string $title,
|
||||
string $description,
|
||||
string $icon,
|
||||
$fields = array(),
|
||||
array $depends_on = array(),
|
||||
$fields = array(),
|
||||
array $warning_messages = array()
|
||||
) : array {
|
||||
$gateway = $this->wc_gateways[ $gateway_id ] ?? null;
|
||||
|
@ -156,11 +134,6 @@ class PaymentMethodsDefinition {
|
|||
'warningMessages' => $warning_messages,
|
||||
);
|
||||
|
||||
// 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(
|
||||
|
|
|
@ -9,6 +9,8 @@ declare( strict_types = 1 );
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
|
||||
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
|
||||
|
@ -29,19 +31,35 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGa
|
|||
/**
|
||||
* Class PaymentMethodsDependenciesDefinition
|
||||
*
|
||||
* Defines dependency relationships between payment methods.
|
||||
* Defines dependency relationships between payment methods and settings.
|
||||
*/
|
||||
class PaymentMethodsDependenciesDefinition {
|
||||
|
||||
/**
|
||||
* Get all payment method dependencies
|
||||
* Current settings values
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Settings $settings Settings instance.
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment method to 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 {
|
||||
public function get_payment_method_dependencies(): array {
|
||||
$dependencies = array(
|
||||
CardButtonGateway::ID => array( PayPalGateway::ID ),
|
||||
CreditCardGateway::ID => array( PayPalGateway::ID ),
|
||||
|
@ -69,43 +87,114 @@ class PaymentMethodsDependenciesDefinition {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a mapping from parent methods to their dependent methods
|
||||
* Get setting to payment method dependencies.
|
||||
*
|
||||
* @return array Parent-to-child dependency map
|
||||
* Maps method ID => array of required settings with their values.
|
||||
* A method is disabled if ANY of its required settings doesn't match the required value.
|
||||
*
|
||||
* @return array The dependency relationships between settings and payment methods
|
||||
*/
|
||||
public function get_dependents_map(): array {
|
||||
$result = array();
|
||||
$dependencies = $this->get_dependencies();
|
||||
public function get_setting_dependencies(): array {
|
||||
$dependencies = array(
|
||||
'pay-later' => array(
|
||||
'savePaypalAndVenmo' => false,
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
return apply_filters(
|
||||
'woocommerce_paypal_payments_setting_dependencies',
|
||||
$dependencies
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parent methods that a method depends on
|
||||
* Get method setting dependencies
|
||||
*
|
||||
* Returns the setting dependencies for a specific method ID.
|
||||
*
|
||||
* @param string $method_id Method ID to check.
|
||||
* @return array Setting dependencies for the method or empty array if none exist
|
||||
*/
|
||||
public function get_method_setting_dependencies( string $method_id ): array {
|
||||
$setting_dependencies = $this->get_setting_dependencies();
|
||||
return $setting_dependencies[ $method_id ] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dependency information to the payment method definitions
|
||||
*
|
||||
* @param array $methods Payment method definitions.
|
||||
* @return array Payment method definitions with dependency information
|
||||
*/
|
||||
public function add_dependency_info_to_methods( array $methods ): array {
|
||||
foreach ( $methods as $method_id => &$method ) {
|
||||
// Skip the __meta key.
|
||||
if ( $method_id === '__meta' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add payment method dependency info if applicable.
|
||||
$payment_method_dependencies = $this->get_method_payment_method_dependencies( $method_id );
|
||||
if ( ! empty( $payment_method_dependencies ) ) {
|
||||
$method['depends_on_payment_methods'] = $payment_method_dependencies;
|
||||
}
|
||||
|
||||
// Check if this method has setting dependencies.
|
||||
$method_setting_dependencies = $this->get_method_setting_dependencies( $method_id );
|
||||
if ( ! empty( $method_setting_dependencies ) ) {
|
||||
$settings = array();
|
||||
foreach ( $method_setting_dependencies as $setting_id => $required_value ) {
|
||||
$settings[ $setting_id ] = array(
|
||||
'id' => $setting_id,
|
||||
'value' => $required_value,
|
||||
);
|
||||
}
|
||||
|
||||
$method['depends_on_settings'] = array(
|
||||
'settings' => $settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add global metadata about settings that affect dependencies.
|
||||
if ( ! isset( $methods['__meta'] ) ) {
|
||||
$methods['__meta'] = array();
|
||||
}
|
||||
|
||||
$methods['__meta']['settings_affecting_methods'] = $this->get_all_dependent_settings();
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment method dependencies for a specific method
|
||||
*
|
||||
* @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();
|
||||
public function get_method_payment_method_dependencies( string $method_id ): array {
|
||||
return $this->get_payment_method_dependencies()[ $method_id ] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get methods that depend on a parent method
|
||||
* Get all settings that affect payment methods
|
||||
*
|
||||
* @param string $parent_id Parent method ID.
|
||||
* @return array Array of dependent method IDs
|
||||
* @return array Array of unique setting keys that affect payment methods
|
||||
*/
|
||||
public function get_dependent_methods( string $parent_id ): array {
|
||||
return $this->get_dependents_map()[ $parent_id ] ?? array();
|
||||
public function get_all_dependent_settings(): array {
|
||||
$settings = array();
|
||||
$dependencies = $this->get_setting_dependencies();
|
||||
|
||||
foreach ( $dependencies as $method_settings ) {
|
||||
if ( isset( $method_settings['settings'] ) ) {
|
||||
foreach ( $method_settings['settings'] as $setting_data ) {
|
||||
if ( ! in_array( $setting_data['id'], $settings, true ) ) {
|
||||
$settings[] = $setting_data['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,12 +105,6 @@ class PaymentSettings extends AbstractDataModel {
|
|||
return $this->get_paylater_enabled();
|
||||
|
||||
default:
|
||||
if (
|
||||
! did_filter( 'woocommerce_payment_gateways' )
|
||||
|| doing_filter( 'woocommerce_payment_gateways' )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
$gateway = $this->get_gateway( $method_id );
|
||||
|
||||
if ( $gateway ) {
|
||||
|
|
|
@ -71,7 +71,7 @@ class SettingsModel extends AbstractDataModel {
|
|||
'soft_descriptor' => '',
|
||||
|
||||
// Enum-type string values.
|
||||
'subtotal_adjustment' => 'skip_details', // Options: [correction|no_details].
|
||||
'subtotal_adjustment' => 'correction', // Options: [correction|no_details].
|
||||
'landing_page' => 'any', // Options: [any|login|guest_checkout].
|
||||
'button_language' => '', // empty or a language locale code.
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@ declare( strict_types = 1 );
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Request;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
|
||||
|
||||
/**
|
||||
|
@ -257,9 +257,17 @@ class CommonRestEndpoint extends RestEndpoint {
|
|||
* @return WP_REST_Response Seller details, provided by PayPal's API.
|
||||
*/
|
||||
public function get_seller_account_info() : WP_REST_Response {
|
||||
$seller_status = $this->partners_endpoint->seller_status();
|
||||
try {
|
||||
$seller_status = $this->partners_endpoint->seller_status();
|
||||
|
||||
return $this->return_success( array( 'country' => $seller_status->country() ) );
|
||||
$seller_data = array(
|
||||
'country' => $seller_status->country(),
|
||||
);
|
||||
|
||||
return $this->return_success( $seller_data );
|
||||
} catch ( Exception $ex ) {
|
||||
return $this->return_error( $ex->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -82,6 +82,16 @@ class LoginLinkRestEndpoint extends RestEndpoint {
|
|||
return array_map( 'sanitize_text_field', $products );
|
||||
},
|
||||
),
|
||||
'options' => array(
|
||||
'requires' => false,
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'bool',
|
||||
),
|
||||
'sanitize_callback' => function ( $flags ) {
|
||||
return array_map( array( $this, 'to_boolean' ), $flags );
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@ -97,9 +107,10 @@ class LoginLinkRestEndpoint extends RestEndpoint {
|
|||
public function get_login_url( WP_REST_Request $request ) : WP_REST_Response {
|
||||
$use_sandbox = $request->get_param( 'useSandbox' );
|
||||
$products = $request->get_param( 'products' );
|
||||
$flags = (array) $request->get_param( 'options' );
|
||||
|
||||
try {
|
||||
$url = $this->url_generator->generate( $products, $use_sandbox );
|
||||
$url = $this->url_generator->generate( $products, $flags, $use_sandbox );
|
||||
|
||||
return $this->return_success( $url );
|
||||
} catch ( \Exception $e ) {
|
||||
|
|
|
@ -30,6 +30,7 @@ use WP_REST_Request;
|
|||
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
|
||||
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\EPSGateway;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDependenciesDefinition;
|
||||
|
||||
/**
|
||||
* REST controller for the "Payment Methods" settings tab.
|
||||
|
@ -50,14 +51,21 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
*
|
||||
* @var PaymentSettings
|
||||
*/
|
||||
protected PaymentSettings $settings;
|
||||
protected PaymentSettings $payment_settings;
|
||||
|
||||
/**
|
||||
* The payment method details.
|
||||
*
|
||||
* @var PaymentMethodsDefinition
|
||||
*/
|
||||
protected PaymentMethodsDefinition $methods_definition;
|
||||
protected PaymentMethodsDefinition $payment_methods_definition;
|
||||
|
||||
/**
|
||||
* The payment method dependencies.
|
||||
*
|
||||
* @var PaymentMethodsDependenciesDefinition
|
||||
*/
|
||||
protected PaymentMethodsDependenciesDefinition $payment_methods_dependencies;
|
||||
|
||||
/**
|
||||
* Field mapping for request to profile transformation.
|
||||
|
@ -86,12 +94,18 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param PaymentSettings $settings The settings instance.
|
||||
* @param PaymentMethodsDefinition $methods_definition Payment Method details.
|
||||
* @param PaymentSettings $payment_settings The settings instance.
|
||||
* @param PaymentMethodsDefinition $payment_methods_definition Payment Method details.
|
||||
* @param PaymentMethodsDependenciesDefinition $payment_methods_dependencies The payment method dependencies.
|
||||
*/
|
||||
public function __construct( PaymentSettings $settings, PaymentMethodsDefinition $methods_definition ) {
|
||||
$this->settings = $settings;
|
||||
$this->methods_definition = $methods_definition;
|
||||
public function __construct(
|
||||
PaymentSettings $payment_settings,
|
||||
PaymentMethodsDefinition $payment_methods_definition,
|
||||
PaymentMethodsDependenciesDefinition $payment_methods_dependencies
|
||||
) {
|
||||
$this->payment_settings = $payment_settings;
|
||||
$this->payment_methods_definition = $payment_methods_definition;
|
||||
$this->payment_methods_dependencies = $payment_methods_dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,7 +114,10 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
* @return array[]
|
||||
*/
|
||||
protected function gateways() : array {
|
||||
return $this->methods_definition->get_definitions();
|
||||
$methods = $this->payment_methods_definition->get_definitions();
|
||||
|
||||
// Add dependency information to the methods.
|
||||
return $this->payment_methods_dependencies->add_dependency_info_to_methods( $methods );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,45 +164,48 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
* @return WP_REST_Response The current payment methods details.
|
||||
*/
|
||||
public function get_details() : WP_REST_Response {
|
||||
$gateway_settings = array();
|
||||
$all_methods = $this->gateways();
|
||||
$gateway_settings = array();
|
||||
$all_payment_methods = $this->gateways();
|
||||
|
||||
// First extract __meta if present.
|
||||
if ( isset( $all_methods['__meta'] ) ) {
|
||||
$gateway_settings['__meta'] = $all_methods['__meta'];
|
||||
if ( isset( $all_payment_methods['__meta'] ) ) {
|
||||
$gateway_settings['__meta'] = $all_payment_methods['__meta'];
|
||||
}
|
||||
|
||||
foreach ( $all_methods as $key => $method ) {
|
||||
foreach ( $all_payment_methods as $key => $payment_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'],
|
||||
'description' => $method['description'],
|
||||
'enabled' => $method['enabled'],
|
||||
'icon' => $method['icon'],
|
||||
'itemTitle' => $method['itemTitle'],
|
||||
'itemDescription' => $method['itemDescription'],
|
||||
'warningMessages' => $method['warningMessages'],
|
||||
'id' => $payment_method['id'],
|
||||
'title' => $payment_method['title'],
|
||||
'description' => $payment_method['description'],
|
||||
'enabled' => $payment_method['enabled'],
|
||||
'icon' => $payment_method['icon'],
|
||||
'itemTitle' => $payment_method['itemTitle'],
|
||||
'itemDescription' => $payment_method['itemDescription'],
|
||||
'warningMessages' => $payment_method['warningMessages'],
|
||||
);
|
||||
|
||||
if ( isset( $method['fields'] ) ) {
|
||||
$gateway_settings[ $key ]['fields'] = $method['fields'];
|
||||
if ( isset( $payment_method['fields'] ) ) {
|
||||
$gateway_settings[ $key ]['fields'] = $payment_method['fields'];
|
||||
}
|
||||
|
||||
// Preserve dependency information.
|
||||
if ( isset( $method['depends_on'] ) ) {
|
||||
$gateway_settings[ $key ]['depends_on'] = $method['depends_on'];
|
||||
if ( isset( $payment_method['depends_on_payment_methods'] ) ) {
|
||||
$gateway_settings[ $key ]['depends_on_payment_methods'] = $payment_method['depends_on_payment_methods'];
|
||||
}
|
||||
|
||||
if ( isset( $payment_method['depends_on_settings'] ) ) {
|
||||
$gateway_settings[ $key ]['depends_on_settings'] = $payment_method['depends_on_settings'];
|
||||
}
|
||||
}
|
||||
|
||||
$gateway_settings['paypalShowLogo'] = $this->settings->get_paypal_show_logo();
|
||||
$gateway_settings['threeDSecure'] = $this->settings->get_three_d_secure();
|
||||
$gateway_settings['fastlaneCardholderName'] = $this->settings->get_fastlane_cardholder_name();
|
||||
$gateway_settings['fastlaneDisplayWatermark'] = $this->settings->get_fastlane_display_watermark();
|
||||
$gateway_settings['paypalShowLogo'] = $this->payment_settings->get_paypal_show_logo();
|
||||
$gateway_settings['threeDSecure'] = $this->payment_settings->get_three_d_secure();
|
||||
$gateway_settings['fastlaneCardholderName'] = $this->payment_settings->get_fastlane_cardholder_name();
|
||||
$gateway_settings['fastlaneDisplayWatermark'] = $this->payment_settings->get_fastlane_display_watermark();
|
||||
|
||||
return $this->return_success( apply_filters( 'woocommerce_paypal_payments_payment_methods', $gateway_settings ) );
|
||||
}
|
||||
|
@ -202,21 +222,21 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
$all_methods = $this->gateways();
|
||||
|
||||
foreach ( $all_methods as $key => $value ) {
|
||||
$new_data = $request_data[ $key ];
|
||||
$new_data = $request_data[ $key ] ?? null;
|
||||
if ( ! $new_data ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $new_data['enabled'] ) ) {
|
||||
$this->settings->toggle_method_state( $key, $new_data['enabled'] );
|
||||
$this->payment_settings->toggle_method_state( $key, $new_data['enabled'] );
|
||||
}
|
||||
|
||||
if ( isset( $new_data['title'] ) ) {
|
||||
$this->settings->set_method_title( $key, sanitize_text_field( $new_data['title'] ) );
|
||||
$this->payment_settings->set_method_title( $key, sanitize_text_field( $new_data['title'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $new_data['description'] ) ) {
|
||||
$this->settings->set_method_description( $key, wp_kses_post( $new_data['description'] ) );
|
||||
$this->payment_settings->set_method_description( $key, wp_kses_post( $new_data['description'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,8 +245,8 @@ class PaymentRestEndpoint extends RestEndpoint {
|
|||
$this->field_map
|
||||
);
|
||||
|
||||
$this->settings->from_array( $wp_data );
|
||||
$this->settings->save();
|
||||
$this->payment_settings->from_array( $wp_data );
|
||||
$this->payment_settings->save();
|
||||
|
||||
return $this->get_details();
|
||||
}
|
||||
|
|
|
@ -446,9 +446,24 @@ class AuthenticationManager {
|
|||
|
||||
try {
|
||||
$endpoint = CommonRestEndpoint::seller_account_route( true );
|
||||
$details = $this->rest_service->get_response( $endpoint );
|
||||
$response = $this->rest_service->get_response( $endpoint );
|
||||
|
||||
if ( ! $response['success'] ) {
|
||||
$this->enrichment_failed( 'Server failed to provide data', $response );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$details = $response['data'];
|
||||
} catch ( Throwable $exception ) {
|
||||
$this->logger->warning( 'Could not determine merchant country: ' . $exception->getMessage() );
|
||||
$this->enrichment_failed( $exception->getMessage() );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $details['country'] ) ) {
|
||||
$this->enrichment_failed( 'Missing country in merchant details' );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -463,6 +478,26 @@ class AuthenticationManager {
|
|||
$this->common_settings->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the `enrich_merchant_details()` call fails, this method might
|
||||
* set up a cron task to retry the attempt after some time.
|
||||
*
|
||||
* @param string $reason Reason for the failure, will be logged.
|
||||
* @param mixed $details Optional. Additional details to log.
|
||||
* @return void
|
||||
*/
|
||||
private function enrichment_failed( string $reason, $details = null ) : void {
|
||||
$this->logger->warning(
|
||||
'Failed to enrich merchant details: ' . $reason,
|
||||
array(
|
||||
'reason' => $reason,
|
||||
'details' => $details,
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: Schedule a cron task to retry the enrichment, e.g. with wp_schedule_single_event().
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the provided details in the data model.
|
||||
*
|
||||
|
|
|
@ -82,12 +82,13 @@ class ConnectionUrlGenerator {
|
|||
*
|
||||
* @param array $products An array of product identifiers to include in the sign-up process.
|
||||
* These determine the PayPal onboarding experience.
|
||||
* @param array $flags Onboarding choices that will customize the ISU payload.
|
||||
* @param bool $use_sandbox Whether to generate a sandbox URL.
|
||||
*
|
||||
* @return string The generated PayPal onboarding URL.
|
||||
*/
|
||||
public function generate( array $products = array(), bool $use_sandbox = false ) : string {
|
||||
$cache_key = $this->cache_key( $products, $use_sandbox );
|
||||
public function generate( array $products = array(), array $flags = array(), bool $use_sandbox = false ) : string {
|
||||
$cache_key = $this->cache_key( $products, $flags, $use_sandbox );
|
||||
$user_id = get_current_user_id();
|
||||
$onboarding_url = $this->url_manager->get( $cache_key, $user_id );
|
||||
$cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
|
||||
|
@ -100,7 +101,7 @@ class ConnectionUrlGenerator {
|
|||
|
||||
$this->logger->info( 'Generating onboarding URL for: ' . $cache_key );
|
||||
|
||||
$url = $this->generate_new_url( $use_sandbox, $products, $onboarding_url, $cache_key );
|
||||
$url = $this->generate_new_url( $use_sandbox, $products, $flags, $onboarding_url, $cache_key );
|
||||
|
||||
if ( $url ) {
|
||||
$this->persist_url( $onboarding_url, $url );
|
||||
|
@ -112,18 +113,28 @@ class ConnectionUrlGenerator {
|
|||
/**
|
||||
* Generates a cache key from the environment and sorted product array.
|
||||
*
|
||||
* Q: Why do we cache the connection URL?
|
||||
* A: The URL is generated by a partner-referrals API, i.e. it requires a
|
||||
* remote request; caching the response avoids unnecessary API calls.
|
||||
*
|
||||
* @param array $products Product identifiers that are part of the cache key.
|
||||
* @param array $flags Onboarding flags.
|
||||
* @param bool $for_sandbox Whether the cache contains a sandbox URL.
|
||||
*
|
||||
* @return string The cache key, defining the product list and environment.
|
||||
*/
|
||||
protected function cache_key( array $products, bool $for_sandbox ) : string {
|
||||
protected function cache_key( array $products, array $flags, bool $for_sandbox ) : string {
|
||||
$environment = $for_sandbox ? 'sandbox' : 'production';
|
||||
|
||||
// Sort products alphabetically, to improve cache implementation.
|
||||
sort( $products );
|
||||
|
||||
return $environment . '-' . implode( '-', $products );
|
||||
// Extract the names of active flags.
|
||||
$active_flags = array_keys( array_filter( $flags ) );
|
||||
|
||||
return strtolower(
|
||||
$environment . '-' . implode( '-', $products ) . '-' . implode( '-', $active_flags )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,12 +173,13 @@ class ConnectionUrlGenerator {
|
|||
*
|
||||
* @param bool $for_sandbox Whether to generate a sandbox URL.
|
||||
* @param array $products The products array.
|
||||
* @param array $flags Onboarding flags.
|
||||
* @param OnboardingUrl $onboarding_url The OnboardingUrl object.
|
||||
* @param string $cache_key The cache key.
|
||||
*
|
||||
* @return string The generated URL or an empty string on failure.
|
||||
*/
|
||||
protected function generate_new_url( bool $for_sandbox, array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string {
|
||||
protected function generate_new_url( bool $for_sandbox, array $products, array $flags, OnboardingUrl $onboarding_url, string $cache_key ) : string {
|
||||
$query_args = array( 'displayMode' => 'minibrowser' );
|
||||
$onboarding_url->init();
|
||||
|
||||
|
@ -179,7 +191,7 @@ class ConnectionUrlGenerator {
|
|||
return '';
|
||||
}
|
||||
|
||||
$data = $this->prepare_referral_data( $products, $onboarding_token );
|
||||
$data = $this->prepare_referral_data( $products, $flags, $onboarding_token );
|
||||
|
||||
try {
|
||||
$referral = $this->partner_referrals->get_value( $for_sandbox );
|
||||
|
@ -197,16 +209,18 @@ class ConnectionUrlGenerator {
|
|||
* Prepares the referral data.
|
||||
*
|
||||
* @param array $products The products array.
|
||||
* @param array $flags Onboarding flags.
|
||||
* @param string $onboarding_token The onboarding token.
|
||||
*
|
||||
* @return array The prepared referral data.
|
||||
*/
|
||||
protected function prepare_referral_data( array $products, string $onboarding_token ) : array {
|
||||
$data = $this->referrals_data
|
||||
->with_products( $products )
|
||||
->data();
|
||||
|
||||
return $this->referrals_data->append_onboarding_token( $data, $onboarding_token );
|
||||
protected function prepare_referral_data( array $products, array $flags, string $onboarding_token ) : array {
|
||||
return $this->referrals_data->data(
|
||||
$products,
|
||||
$onboarding_token,
|
||||
(bool) ( $flags['useSubscriptions'] ?? false ),
|
||||
(bool) ( $flags['useCardPayments'] ?? false )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
129
modules/ppcp-settings/src/Service/GatewayRedirectService.php
Normal file
129
modules/ppcp-settings/src/Service/GatewayRedirectService.php
Normal file
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
/**
|
||||
* Provides gateway redirect handling logic.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Settings\Service
|
||||
*/
|
||||
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Service;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway;
|
||||
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\CardButtonGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
|
||||
|
||||
/**
|
||||
* GatewayRedirectService class. Handles redirects from individual gateway
|
||||
* settings URLs to the new Settings UI page.
|
||||
*/
|
||||
class GatewayRedirectService {
|
||||
|
||||
/**
|
||||
* List of gateways to redirect.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $gateways;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->gateways = array(
|
||||
AxoGateway::ID,
|
||||
GooglePayGateway::ID,
|
||||
ApplePayGateway::ID,
|
||||
CreditCardGateway::ID,
|
||||
CardButtonGateway::ID,
|
||||
BancontactGateway::ID,
|
||||
BlikGateway::ID,
|
||||
EPSGateway::ID,
|
||||
IDealGateway::ID,
|
||||
MyBankGateway::ID,
|
||||
P24Gateway::ID,
|
||||
TrustlyGateway::ID,
|
||||
MultibancoGateway::ID,
|
||||
OXXO::ID,
|
||||
PayUponInvoiceGateway::ID,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
add_action(
|
||||
'admin_init',
|
||||
array( $this, 'handle_redirects' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirects for gateway settings pages.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_redirects(): void {
|
||||
if ( ! is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current URL parameters.
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
// The sanitize_get_param method handles unslashing and sanitization internally.
|
||||
$page = isset( $_GET['page'] ) ? $this->sanitize_get_param( $_GET['page'] ) : '';
|
||||
$tab = isset( $_GET['tab'] ) ? $this->sanitize_get_param( $_GET['tab'] ) : '';
|
||||
$section = isset( $_GET['section'] ) ? $this->sanitize_get_param( $_GET['section'] ) : '';
|
||||
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
// Check if we're on a WooCommerce settings page and checkout tab.
|
||||
if ( $page !== 'wc-settings' || $tab !== 'checkout' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're on one of the gateway settings pages we want to redirect.
|
||||
if ( in_array( $section, $this->gateways, true ) ) {
|
||||
$redirect_url = admin_url(
|
||||
sprintf(
|
||||
'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=payment-methods&highlight=%s',
|
||||
$section
|
||||
)
|
||||
);
|
||||
|
||||
wp_safe_redirect( $redirect_url );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a GET parameter that could be string or array.
|
||||
*
|
||||
* @param mixed $param The parameter to sanitize.
|
||||
* @return string The sanitized parameter.
|
||||
*/
|
||||
private function sanitize_get_param( $param ): string {
|
||||
if ( is_array( $param ) ) {
|
||||
return '';
|
||||
}
|
||||
return sanitize_text_field( wp_unslash( $param ) );
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ declare( strict_types = 1 );
|
|||
namespace WooCommerce\PayPalCommerce\Settings\Service;
|
||||
|
||||
use Throwable;
|
||||
use RuntimeException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WP_Http_Cookie;
|
||||
|
||||
|
@ -48,6 +49,7 @@ class InternalRestService {
|
|||
*
|
||||
* @param string $endpoint The endpoint for which the token is generated.
|
||||
* @return mixed The REST response.
|
||||
* @throws RuntimeException In case the remote request fails, an exception is thrown.
|
||||
*/
|
||||
public function get_response( string $endpoint ) {
|
||||
$rest_url = rest_url( $endpoint );
|
||||
|
@ -69,9 +71,11 @@ class InternalRestService {
|
|||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$this->logger->error( 'Internal REST error', array( 'response' => $response ) );
|
||||
// Error: The wp_remote_request() call failed (timeout or similar).
|
||||
$error = new RuntimeException( 'Internal REST error' );
|
||||
$this->logger->error( $error->getMessage(), array( 'response' => $response ) );
|
||||
|
||||
return array();
|
||||
throw $error;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
@ -79,26 +83,22 @@ class InternalRestService {
|
|||
try {
|
||||
$json = json_decode( $body, true, 512, JSON_THROW_ON_ERROR );
|
||||
} catch ( Throwable $exception ) {
|
||||
// Error: The returned body-string is not valid JSON.
|
||||
$error = new RuntimeException( 'Internal REST error: Invalid JSON response' );
|
||||
$this->logger->error(
|
||||
'Internal REST error: Invalid JSON response',
|
||||
$error->getMessage(),
|
||||
array(
|
||||
'error' => $exception->getMessage(),
|
||||
'response_body' => $body,
|
||||
)
|
||||
);
|
||||
|
||||
return array();
|
||||
throw $error;
|
||||
}
|
||||
|
||||
if ( ! $json || empty( $json['success'] ) ) {
|
||||
$this->logger->error( 'Internal REST error: Invalid response', array( 'json' => $json ) );
|
||||
$this->logger->info( 'Internal REST success!', array( 'json' => $json ) );
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
$this->logger->info( 'Internal REST success', array( 'data' => $json['data'] ) );
|
||||
|
||||
return $json['data'];
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -171,6 +171,19 @@ class SettingsDataManager {
|
|||
|
||||
$this->onboarding_profile->set_setup_done( true );
|
||||
$this->onboarding_profile->save();
|
||||
|
||||
/**
|
||||
* Fires after the core merchant configuration was applied.
|
||||
*
|
||||
* This action indicates that a merchant completed the onboarding wizard.
|
||||
* The flags contain several choices which the merchant took during the
|
||||
* onboarding wizard, and provide additional context on which defaults
|
||||
* should be applied for the new merchant.
|
||||
*
|
||||
* Other modules or integrations can use this hook to initialize
|
||||
* additional plugin settings on first merchant login.
|
||||
*/
|
||||
do_action( 'woocommerce_paypal_payments_apply_default_configuration', $flags );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -253,12 +266,7 @@ class SettingsDataManager {
|
|||
|
||||
if ( $flags->is_business_seller && $flags->use_subscriptions ) {
|
||||
$this->payment_settings->set_save_paypal_and_venmo( true );
|
||||
|
||||
if ( $flags->use_card_payments ) {
|
||||
$this->payment_settings->set_save_card_details( true );
|
||||
} else {
|
||||
$this->payment_settings->set_save_card_details( false );
|
||||
}
|
||||
$this->payment_settings->set_save_card_details( true );
|
||||
}
|
||||
|
||||
$this->payment_settings->save();
|
||||
|
|
|
@ -10,10 +10,9 @@ 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\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
|
||||
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BancontactGateway;
|
||||
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\BlikGateway;
|
||||
|
@ -24,10 +23,12 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\MyBankGateway;
|
|||
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\P24Gateway;
|
||||
use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\TrustlyGateway;
|
||||
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\Definition\PaymentMethodsDefinition;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Handler\ConnectionListener;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\GatewayRedirectService;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
|
||||
|
@ -35,6 +36,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
|||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
|
@ -42,6 +44,8 @@ use WooCommerce\PayPalCommerce\Settings\Service\SettingsDataManager;
|
|||
use WooCommerce\PayPalCommerce\Settings\DTO\ConfigurationFlagsDTO;
|
||||
use WooCommerce\PayPalCommerce\Settings\Enum\ProductChoicesEnum;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
|
||||
use WooCommerce\PayPalCommerce\Axo\Helper\CompatibilityChecker;
|
||||
|
||||
/**
|
||||
* Class SettingsModule
|
||||
|
@ -423,8 +427,9 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
*
|
||||
* @psalm-suppress MissingClosureParamType
|
||||
*/
|
||||
static function ( $methods ) use ( $container ) : array {
|
||||
if ( ! is_array( $methods ) ) {
|
||||
function ( $methods ) use ( $container ) : array {
|
||||
$is_onboarded = $container->get( 'api.merchant_id' ) !== '';
|
||||
if ( ! is_array( $methods ) || ! $is_onboarded ) {
|
||||
return $methods;
|
||||
}
|
||||
|
||||
|
@ -445,6 +450,56 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
$methods[] = $applepay_gateway;
|
||||
$methods[] = $axo_gateway;
|
||||
|
||||
$is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' );
|
||||
$all_gateway_ids = $container->get( 'settings.config.all-gateway-ids' );
|
||||
|
||||
if ( $is_payments_page ) {
|
||||
$methods = array_filter(
|
||||
$methods,
|
||||
function ( $method ) use ( $all_gateway_ids ): bool {
|
||||
if ( ! is_object( $method )
|
||||
|| $method->id === PayPalGateway::ID
|
||||
|| ! in_array( $method->id, $all_gateway_ids, true )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! $this->is_gateway_enabled( $method->id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $methods;
|
||||
},
|
||||
99
|
||||
);
|
||||
|
||||
// Remove the Fastlane gateway if the customer is logged in, ensuring that we don't interfere with the Fastlane gateway status in the settings UI.
|
||||
add_filter(
|
||||
'woocommerce_available_payment_gateways',
|
||||
/**
|
||||
* Param types removed to avoid third-party issues.
|
||||
*
|
||||
* @psalm-suppress MissingClosureParamType
|
||||
*/
|
||||
static function ( $methods ) use ( $container ) : array {
|
||||
if ( ! is_array( $methods ) ) {
|
||||
return $methods;
|
||||
}
|
||||
|
||||
if ( is_user_logged_in() && ! is_admin() ) {
|
||||
foreach ( $methods as $key => $method ) {
|
||||
if ( $method instanceof WC_Payment_Gateway && $method->id === 'ppcp-axo-gateway' ) {
|
||||
unset( $methods[ $key ] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
);
|
||||
|
@ -502,24 +557,78 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
2
|
||||
);
|
||||
|
||||
add_filter( 'woocommerce_paypal_payments_axo_gateway_should_update_enabled', '__return_false' );
|
||||
if ( is_admin() ) {
|
||||
add_filter( 'woocommerce_paypal_payments_axo_gateway_should_update_enabled', '__return_false' );
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_axo_gateway_title',
|
||||
function ( string $title, WC_Payment_Gateway $gateway ) {
|
||||
return $gateway->get_option( 'title', $title );
|
||||
},
|
||||
10,
|
||||
2
|
||||
);
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_axo_gateway_description',
|
||||
function ( string $description, WC_Payment_Gateway $gateway ) {
|
||||
return $gateway->get_option( 'description', $description );
|
||||
},
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets the BCDC black button if merchant is eligible for ACDC.
|
||||
*/
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_axo_gateway_title',
|
||||
function ( string $title, WC_Payment_Gateway $gateway ) {
|
||||
return $gateway->get_option( 'title', $title );
|
||||
},
|
||||
10,
|
||||
2
|
||||
'woocommerce_paypal_payments_disabled_funding_sources',
|
||||
/**
|
||||
* Unsets the BCDC black button if merchant is eligible for ACDC.
|
||||
*
|
||||
* @param int[]|string[]|mixed $disable_funding The disabled funding sources.
|
||||
* @return int[]|string[]|mixed The disabled funding sources.
|
||||
*
|
||||
* @psalm-suppress MissingClosureParamType
|
||||
*/
|
||||
static function ( $disable_funding ) use ( $container ) {
|
||||
if ( ! is_array( $disable_funding ) || in_array( 'card', $disable_funding, true ) ) {
|
||||
return $disable_funding;
|
||||
}
|
||||
|
||||
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
|
||||
assert( $dcc_product_status instanceof DCCProductStatus );
|
||||
|
||||
if ( $dcc_product_status->is_active() ) {
|
||||
$disable_funding[] = 'card';
|
||||
}
|
||||
|
||||
return $disable_funding;
|
||||
}
|
||||
);
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_axo_gateway_description',
|
||||
function ( string $description, WC_Payment_Gateway $gateway ) {
|
||||
return $gateway->get_option( 'description', $description );
|
||||
},
|
||||
10,
|
||||
2
|
||||
|
||||
// Enable Fastlane after onboarding if the store is compatible.
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_apply_default_configuration',
|
||||
static function () use ( $container ) {
|
||||
$compatibility_checker = $container->get( 'axo.helpers.compatibility-checker' );
|
||||
assert( $compatibility_checker instanceof CompatibilityChecker );
|
||||
|
||||
$payment_settings = $container->get( 'settings.data.payment' );
|
||||
assert( $payment_settings instanceof PaymentSettings );
|
||||
|
||||
if ( $compatibility_checker->is_fastlane_compatible() ) {
|
||||
$payment_settings->toggle_method_state( AxoGateway::ID, true );
|
||||
}
|
||||
|
||||
$payment_settings->save();
|
||||
}
|
||||
);
|
||||
|
||||
// Redirect payment method links in the WC Payment Gateway to the new UI Payment Methods tab.
|
||||
$gateway_redirect_service = $container->get( 'settings.service.gateway-redirect' );
|
||||
assert( $gateway_redirect_service instanceof GatewayRedirectService );
|
||||
$gateway_redirect_service->register();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -542,4 +651,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
protected function render_content() : void {
|
||||
echo '<div id="ppcp-settings-container"></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the payment gateway with the given name is enabled.
|
||||
*
|
||||
* @param string $gateway_name The gateway name.
|
||||
* @return bool True if the payment gateway with the given name is enabled, otherwise false.
|
||||
*/
|
||||
protected function is_gateway_enabled( string $gateway_name ): bool {
|
||||
$gateway_settings = get_option( "woocommerce_{$gateway_name}_settings", array() );
|
||||
$gateway_enabled = $gateway_settings['enabled'] ?? false;
|
||||
|
||||
return $gateway_enabled === 'yes';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,8 @@ class UninstallModule implements ServiceModule, ExtendingModule, ExecutableModul
|
|||
$clear_db->clear_scheduled_actions( $scheduled_action_names );
|
||||
$clear_db->clear_actions( $action_names );
|
||||
|
||||
update_option( 'woocommerce-ppcp-is-new-merchant', '1' );
|
||||
|
||||
wp_send_json_success();
|
||||
return true;
|
||||
} catch ( Exception $error ) {
|
||||
|
|
|
@ -458,9 +458,14 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
|
|||
);
|
||||
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
\WP_CLI::add_command(
|
||||
'pcp settings',
|
||||
$c->get( 'wcgateway.cli.settings.command' )
|
||||
add_action(
|
||||
'init',
|
||||
function() use ( $c ) {
|
||||
\WP_CLI::add_command(
|
||||
'pcp settings',
|
||||
$c->get( 'wcgateway.cli.settings.command' )
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "woocommerce-paypal-payments",
|
||||
"version": "2.9.6",
|
||||
"version": "3.0.0",
|
||||
"description": "WooCommerce PayPal Payments",
|
||||
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
|
||||
"license": "GPL-2.0",
|
||||
|
|
20
readme.txt
20
readme.txt
|
@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, credit card
|
|||
Requires at least: 6.5
|
||||
Tested up to: 6.7
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 2.9.6
|
||||
Stable tag: 3.0.0
|
||||
License: GPLv2
|
||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
|
@ -113,7 +113,7 @@ If you like this extension, please [leave a review on WordPress.org](https://wor
|
|||
To install and configure WooCommerce PayPal Payments, you will need:
|
||||
|
||||
* WordPress Version 6.3 or newer (installed)
|
||||
* WooCommerce Version 6.9 or newer (installed and activated)
|
||||
* WooCommerce Version 9.6 or newer (installed and activated)
|
||||
* PHP Version 7.4 or newer
|
||||
* PayPal business **or** personal account
|
||||
|
||||
|
@ -156,6 +156,22 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 3.0.0 - 2025-03-17 =
|
||||
* Enhancement - Redesigned settings UI for new users #2908
|
||||
* Enhancement - Enable Fastlane by default on new store setups when eligible #3199
|
||||
* Enhancement - Enable support for advanced card payments and features for Hong Kong & Singapore #3089
|
||||
* Fix - Dependency conflict with more recent psr/log versions on PHP8+ #2993
|
||||
* Fix - PayPal Checkout Gateway subscription migration layer not renewing subscriptions #2699
|
||||
* Fix - Fatal error when gateway settings initialized too early by third-party plugin #2766
|
||||
* Fix - Next Payment date for Subscriptions not updating when processing a PayPal Subscriptions renewal order #2959
|
||||
* Fix - Changing the subscription payment method to ACDC triggers error #2891
|
||||
* Fix - Standard Card button not appearing in standalone gateway for free trial subscription products #2935
|
||||
* Fix - Validation error when using Trustly payment method #3031
|
||||
* Fix - Error in continuation mode due to wrong gateway selection on Checkout block #2996
|
||||
* Fix - Error in error in PayLaterConfigurator #2989
|
||||
* Tweak - Removed currency requirement for Vault v3 #2919
|
||||
* Tweak - Update plugin author from WooCommerce to PayPal
|
||||
|
||||
= 2.9.6 - 2025-01-06 =
|
||||
* Fix - NOT_ENABLED_TO_VAULT_PAYMENT_SOURCE on PayPal transactions when using ACDC Vaulting without PayPal Vault approval #2955
|
||||
* Fix - Express buttons for Free Trial Subscription products on Block Cart/Checkout trigger CANNOT_BE_ZERO_OR_NEGATIVE error #2872
|
||||
|
|
221
tests/PHPUnit/ApiClient/Repository/PartnerReferralsDataTest.php
Normal file
221
tests/PHPUnit/ApiClient/Repository/PartnerReferralsDataTest.php
Normal file
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
declare( strict_types = 1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
|
||||
|
||||
use Mockery;
|
||||
use WooCommerce\PayPalCommerce\TestCase;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||
use function Brain\Monkey\Functions\when;
|
||||
|
||||
/**
|
||||
* @group api
|
||||
* @group onboarding
|
||||
*/
|
||||
class PartnerReferralsDataTest extends TestCase {
|
||||
/**
|
||||
* Expected nonce that should appear in the payload.
|
||||
*/
|
||||
private const DEFAULT_NONCE = 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG';
|
||||
|
||||
/**
|
||||
* Sample URL which is used to mock the `admin_url()` response, used to build the return URL.
|
||||
* Specifically, we want to verify the $path which is appended to the admin URL.
|
||||
*/
|
||||
private const ADMIN_URL = 'https://example.com/wp-admin/';
|
||||
|
||||
/**
|
||||
* A sample token that we add to the return URL.
|
||||
* We pass this const to the `->data()` method to ensure it's appended at the end of the
|
||||
* return URL as-is.
|
||||
*/
|
||||
private const TOKEN = 'SECURE_TOKEN';
|
||||
|
||||
/**
|
||||
* Expected return URL to see at in the payload, including the ppcpToken.
|
||||
*/
|
||||
private const RETURN_URL = 'https://example.com/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcpToken=SECURE_TOKEN';
|
||||
|
||||
private $testee;
|
||||
private $dccApplies;
|
||||
|
||||
public function setUp() : void {
|
||||
parent::setUp();
|
||||
|
||||
$this->dccApplies = Mockery::mock( DccApplies::class );
|
||||
$this->testee = new PartnerReferralsData( $this->dccApplies );
|
||||
|
||||
when( 'admin_url' )->alias( static fn( string $path ) => self::ADMIN_URL . $path );
|
||||
when( 'add_query_arg' )->justReturn( self::RETURN_URL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Base structure of the API payload. Each test should modify the returned
|
||||
* value of the method to meet its expectations.
|
||||
*
|
||||
* This avoids repeating the full structure, while also highlighting the
|
||||
* specific changes that different params will generate.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getBaseExpectedArray() : array {
|
||||
return [
|
||||
'partner_config_override' => [
|
||||
'return_url' => self::RETURN_URL,
|
||||
'return_url_description' => 'Return to your shop.',
|
||||
'show_add_credit_card' => true,
|
||||
],
|
||||
'legal_consents' => [
|
||||
[
|
||||
'type' => 'SHARE_DATA_CONSENT',
|
||||
'granted' => true,
|
||||
],
|
||||
],
|
||||
'operations' => [
|
||||
[
|
||||
'operation' => 'API_INTEGRATION',
|
||||
'api_integration_preference' => [
|
||||
'rest_api_integration' => [
|
||||
'integration_method' => 'PAYPAL',
|
||||
'integration_type' => 'FIRST_PARTY',
|
||||
'first_party_details' => [
|
||||
'features' => [
|
||||
'PAYMENT',
|
||||
'REFUND',
|
||||
'ADVANCED_TRANSACTIONS_SEARCH',
|
||||
'TRACKING_SHIPMENT_READWRITE',
|
||||
],
|
||||
'seller_nonce' => self::DEFAULT_NONCE,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testing flag combinations.
|
||||
*
|
||||
* @return array[] Test cases with [has_subscriptions, has_cards, expected_changes]
|
||||
*/
|
||||
public function flagCombinationsProvider() : array {
|
||||
return [
|
||||
'with subscriptions and cards' => [
|
||||
true, // With subscription?
|
||||
true, // With cards?
|
||||
[
|
||||
'capabilities' => [ 'PAYPAL_WALLET_VAULTING_ADVANCED' ],
|
||||
'show_add_credit_card' => true,
|
||||
'has_vault_features' => true,
|
||||
],
|
||||
],
|
||||
'with subscriptions, no cards' => [
|
||||
true, // With subscription?
|
||||
false, // With cards?
|
||||
[
|
||||
'capabilities' => [ 'PAYPAL_WALLET_VAULTING_ADVANCED' ],
|
||||
'show_add_credit_card' => false,
|
||||
'has_vault_features' => true,
|
||||
],
|
||||
],
|
||||
'no subscriptions, with cards' => [
|
||||
false, // With subscription?
|
||||
true, // With cards?
|
||||
[
|
||||
'show_add_credit_card' => true,
|
||||
'has_vault_features' => false,
|
||||
],
|
||||
],
|
||||
'no subscriptions, no cards' => [
|
||||
false, // With subscription?
|
||||
false, // With cards?
|
||||
[
|
||||
'show_add_credit_card' => false,
|
||||
'has_vault_features' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the default "products" are derived from the DccApplies response.
|
||||
*/
|
||||
public function testDefaultValues() : void {
|
||||
/**
|
||||
* Case 1: The data() method gets no parameters, and the DccApplies check
|
||||
* returns TRUE. Onboarding payload should indicate "PPCP".
|
||||
*/
|
||||
$this->dccApplies->expects( 'for_country_currency' )->andReturn( true );
|
||||
$result = $this->testee->data();
|
||||
$this->assertEquals( [ 'PPCP' ], $result['products'] );
|
||||
|
||||
/**
|
||||
* Case 2: The data() method gets no parameters, and the DccApplies check
|
||||
* returns FALSE. Onboarding payload should indicate "EXPRESS_CHECKOUT".
|
||||
*/
|
||||
$this->dccApplies->expects( 'for_country_currency' )->andReturn( false );
|
||||
$result = $this->testee->data();
|
||||
$this->assertEquals( [ 'EXPRESS_CHECKOUT' ], $result['products'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the generated API payload is stable and contains the expected values.
|
||||
*
|
||||
* The test only verifies the "products" and "token" arguments, as those are the
|
||||
* core params present in the legacy and new UI.
|
||||
*/
|
||||
public function testDataStructure() : void {
|
||||
/**
|
||||
* Undefined subscription: Keep vaulting in first-party, but don't add the capability.
|
||||
*/
|
||||
$result = $this->testee->data( [ 'PPCP' ], self::TOKEN );
|
||||
$this->dccApplies->shouldNotHaveReceived( 'for_country_currency' );
|
||||
|
||||
$expected = $this->getBaseExpectedArray();
|
||||
|
||||
$expected['products'] = [ 'PPCP' ];
|
||||
|
||||
$expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'FUTURE_PAYMENT';
|
||||
$expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'VAULT';
|
||||
|
||||
$this->assertArrayNotHasKey( 'capabilities', $expected );
|
||||
$this->assertEquals( $expected, $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test how different flag combinations affect the data structure.
|
||||
* Those flags are present in the new UI.
|
||||
*
|
||||
* @dataProvider flagCombinationsProvider
|
||||
*/
|
||||
public function testDataStructureWithFlags( bool $has_subscriptions, bool $has_cards, array $expected_changes ) : void {
|
||||
$result = $this->testee->data( [ 'PPCP' ], self::TOKEN, $has_subscriptions, $has_cards );
|
||||
$expected = $this->getBaseExpectedArray();
|
||||
|
||||
$expected['products'] = [ 'PPCP' ];
|
||||
|
||||
if ( isset( $expected_changes['capabilities'] ) ) {
|
||||
$expected['capabilities'] = $expected_changes['capabilities'];
|
||||
} else {
|
||||
$this->assertArrayNotHasKey( 'capabilities', $expected );
|
||||
}
|
||||
|
||||
$expected['partner_config_override']['show_add_credit_card'] = $expected_changes['show_add_credit_card'];
|
||||
|
||||
if ( $has_subscriptions ) {
|
||||
$expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'BILLING_AGREEMENT';
|
||||
}
|
||||
|
||||
if ( $expected_changes['has_vault_features'] ) {
|
||||
$expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'FUTURE_PAYMENT';
|
||||
$expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'][] = 'VAULT';
|
||||
} else {
|
||||
// Double-check that the features are not present in our expected array
|
||||
$this->assertNotContains( 'FUTURE_PAYMENT', $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'] );
|
||||
$this->assertNotContains( 'VAULT', $expected['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['features'] );
|
||||
}
|
||||
|
||||
$this->assertEquals( $expected, $result );
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e;
|
||||
|
||||
use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
|
||||
|
||||
class PayPalSubscriptionsRenewalTest extends TestCase
|
||||
{
|
||||
public function test_parent_order()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
// Simulates receiving webhook 1 minute after subscription start.
|
||||
$subscription = $this->createSubscription('-1 minute');
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
|
||||
$this->assertEquals(count($renewal), 0);
|
||||
}
|
||||
|
||||
public function test_renewal_order()
|
||||
{
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
|
||||
|
||||
// Simulates receiving webhook 9 hours after subscription start.
|
||||
$subscription = $this->createSubscription('-9 hour');
|
||||
|
||||
$handler->process([$subscription], 'TRANSACTION-ID');
|
||||
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
|
||||
$this->assertEquals(count($renewal), 1);
|
||||
}
|
||||
|
||||
private function createSubscription(string $startDate)
|
||||
{
|
||||
$args = [
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'customer_id' => 1,
|
||||
'set_paid' => true,
|
||||
'payment_method' => 'ppcp-gateway',
|
||||
'billing' => [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'address_1' => '969 Market',
|
||||
'address_2' => '',
|
||||
'city' => 'San Francisco',
|
||||
'state' => 'CA',
|
||||
'postcode' => '94103',
|
||||
'country' => 'US',
|
||||
'email' => 'john.doe@example.com',
|
||||
'phone' => '(555) 555-5555'
|
||||
],
|
||||
'line_items' => [
|
||||
[
|
||||
'product_id' => 156,
|
||||
'quantity' => 1
|
||||
]
|
||||
],
|
||||
]),
|
||||
];
|
||||
|
||||
$response = wp_remote_request(
|
||||
'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/orders',
|
||||
$args
|
||||
);
|
||||
|
||||
$body = json_decode( $response['body'] );
|
||||
|
||||
$args = [
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
|
||||
'parent_id' => $body->id,
|
||||
'customer_id' => 1,
|
||||
'status' => 'active',
|
||||
'billing_period' => 'day',
|
||||
'billing_interval' => 1,
|
||||
'payment_method' => 'ppcp-gateway',
|
||||
'line_items' => [
|
||||
[
|
||||
'product_id' => $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'],
|
||||
'quantity' => 1
|
||||
]
|
||||
],
|
||||
]),
|
||||
];
|
||||
|
||||
$response = wp_remote_request(
|
||||
'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/subscriptions?per_page=1',
|
||||
$args
|
||||
);
|
||||
|
||||
$body = json_decode( $response['body'] );
|
||||
|
||||
return wcs_get_subscription($body->id);
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMap;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
|
||||
class SettingsTest extends TestCase {
|
||||
private Settings $settings;
|
||||
|
||||
protected function setUp(): void {
|
||||
$commonSettingsModel = $this->createMock(AbstractDataModel::class);
|
||||
$commonSettingsModel->method('to_array')->willReturn([
|
||||
'use_sandbox' => 'yes',
|
||||
'client_id' => 'abc123',
|
||||
'client_secret' => 'secret123',
|
||||
]);
|
||||
|
||||
$generalSettingsModel = $this->createMock(AbstractDataModel::class);
|
||||
$generalSettingsModel->method('to_array')->willReturn([
|
||||
'is_sandbox' => 'no',
|
||||
'live_client_id' => 'live_id_123',
|
||||
'live_client_secret' => 'live_secret_123',
|
||||
]);
|
||||
|
||||
$settingsMap = [
|
||||
new SettingsMap(
|
||||
$commonSettingsModel,
|
||||
[
|
||||
'client_id' => 'client_id',
|
||||
'client_secret' => 'client_secret',
|
||||
]
|
||||
),
|
||||
new SettingsMap(
|
||||
$generalSettingsModel,
|
||||
[
|
||||
'is_sandbox' => 'sandbox_on',
|
||||
'live_client_id' => 'client_id_production',
|
||||
'live_client_secret' => 'client_secret_production',
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
$settingsMapHelper = new SettingsMapHelper($settingsMap);
|
||||
|
||||
$this->settings = new Settings(
|
||||
['cart', 'checkout'],
|
||||
'PayPal Credit Gateway',
|
||||
['checkout'],
|
||||
['cart'],
|
||||
$settingsMapHelper
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetMappedValue() {
|
||||
$value = $this->settings->get('sandbox_on');
|
||||
|
||||
$this->assertEquals('no', $value);
|
||||
}
|
||||
|
||||
public function testGetThrowsNotFoundExceptionForInvalidKey() {
|
||||
$this->expectException(NotFoundException::class);
|
||||
|
||||
$this->settings->get('invalid_key');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('E2E_TESTS_ROOT_DIR', dirname(__DIR__));
|
||||
define('ROOT_DIR', dirname(dirname(E2E_TESTS_ROOT_DIR)));
|
||||
|
||||
require_once ROOT_DIR . '/vendor/autoload.php';
|
||||
|
||||
if (file_exists(ROOT_DIR . '/.env.e2e')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(ROOT_DIR, '.env.e2e');
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
if (!isset($_ENV['PPCP_E2E_WP_DIR'])) {
|
||||
exit('Copy .env.e2e.example to .env.e2e or define the environment variables.' . PHP_EOL);
|
||||
}
|
||||
$wpRootDir = str_replace('${ROOT_DIR}', ROOT_DIR, $_ENV['PPCP_E2E_WP_DIR']);
|
||||
|
||||
define('WP_ROOT_DIR', $wpRootDir);
|
||||
|
||||
$_SERVER['HTTP_HOST'] = ''; // just to avoid a warning
|
||||
|
||||
require_once WP_ROOT_DIR . '/wp-load.php';
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e\Order;
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration\Order;
|
||||
|
||||
use Exception;
|
||||
use WC_Cart;
|
||||
|
@ -15,7 +15,7 @@ use WC_Product;
|
|||
use WC_Product_Simple;
|
||||
use WC_Session;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
|
||||
use WooCommerce\PayPalCommerce\Tests\E2e\TestCase;
|
||||
use WooCommerce\PayPalCommerce\Tests\Integration\TestCase;
|
||||
|
||||
class PurchaseUnitTest extends TestCase
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e;
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
|
||||
|
88
tests/integration/PHPUnit/PayPalSubscriptionsRenewalTest.php
Normal file
88
tests/integration/PHPUnit/PayPalSubscriptionsRenewalTest.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use WC_Product_Simple;
|
||||
use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
|
||||
|
||||
class PayPalSubscriptionsRenewalTest extends TestCase {
|
||||
public function test_renewal_order_is_not_created_just_after_receiving_webhook() {
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler( $c->get( 'woocommerce.logger.woocommerce' ) );
|
||||
|
||||
// Simulates receiving webhook 1 minute after subscription start.
|
||||
$subscription = $this->createSubscription( '-1 minute' );
|
||||
|
||||
$handler->process( [ $subscription ], 'TRANSACTION-ID' );
|
||||
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
|
||||
$this->assertEquals( count( $renewal ), 0 );
|
||||
}
|
||||
|
||||
public function test_renewal_order_is_created_when_receiving_webhook_nine_hours_later() {
|
||||
$c = $this->getContainer();
|
||||
$handler = new RenewalHandler( $c->get( 'woocommerce.logger.woocommerce' ) );
|
||||
|
||||
// Simulates receiving webhook 9 hours after subscription start.
|
||||
$subscription = $this->createSubscription( '-9 hour' );
|
||||
|
||||
$handler->process( [ $subscription ], 'TRANSACTION-ID' );
|
||||
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
|
||||
$this->assertEquals( count( $renewal ), 1 );
|
||||
}
|
||||
|
||||
private function createSubscription( string $startDate ) {
|
||||
$order = wc_create_order( [
|
||||
'customer_id' => 1,
|
||||
'set_paid' => true,
|
||||
'payment_method' => 'ppcp-gateway',
|
||||
'billing' => [
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'address_1' => '969 Market',
|
||||
'address_2' => '',
|
||||
'city' => 'San Francisco',
|
||||
'state' => 'CA',
|
||||
'postcode' => '94103',
|
||||
'country' => 'US',
|
||||
'email' => 'john.doe@example.com',
|
||||
'phone' => '(555) 555-5555'
|
||||
],
|
||||
'line_items' => [
|
||||
[
|
||||
'product_id' => 42,
|
||||
'quantity' => 1
|
||||
]
|
||||
],
|
||||
] );
|
||||
|
||||
$product = new WC_Product_Simple();
|
||||
$product->set_props([
|
||||
'name' => 'Dummy Product',
|
||||
'regular_price' => 10,
|
||||
'price' => 10,
|
||||
'sku' => 'DUMMY SKU',
|
||||
'manage_stock' => false,
|
||||
'tax_status' => 'taxable',
|
||||
'downloadable' => false,
|
||||
'virtual' => false,
|
||||
'stock_status' => 'instock',
|
||||
'weight' => '1.1',
|
||||
]);
|
||||
|
||||
return wcs_create_subscription([
|
||||
'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
|
||||
'parent_id' => $order->get_id(),
|
||||
'customer_id' => 1,
|
||||
'status' => 'active',
|
||||
'billing_period' => 'day',
|
||||
'billing_interval' => 1,
|
||||
'payment_method' => 'ppcp-gateway',
|
||||
'line_items' => [
|
||||
[
|
||||
'product_id' => $product->get_id(),
|
||||
'quantity' => 1
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e;
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use WC_Payment_Token_CC;
|
||||
use WC_Payment_Tokens;
|
84
tests/integration/PHPUnit/SettingsTest.php
Normal file
84
tests/integration/PHPUnit/SettingsTest.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use Mockery;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\GeneralSettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\PaymentMethodSettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMap;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SettingsTabMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\StylingSettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Compat\Settings\SubscriptionSettingsMapHelper;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
|
||||
class SettingsTest extends TestCase {
|
||||
private Settings $settings;
|
||||
|
||||
protected function setUp(): void {
|
||||
$commonSettingsModel = $this->createMock( AbstractDataModel::class );
|
||||
$commonSettingsModel->method( 'to_array' )->willReturn( [
|
||||
'use_sandbox' => 'yes',
|
||||
'client_id' => 'abc123',
|
||||
'client_secret' => 'secret123',
|
||||
] );
|
||||
|
||||
$generalSettingsModel = $this->createMock( AbstractDataModel::class );
|
||||
$generalSettingsModel->method( 'to_array' )->willReturn( [
|
||||
'is_sandbox' => 'no',
|
||||
'live_client_id' => 'live_id_123',
|
||||
'live_client_secret' => 'live_secret_123',
|
||||
] );
|
||||
|
||||
$settingsMap = [
|
||||
new SettingsMap(
|
||||
$commonSettingsModel,
|
||||
[
|
||||
'client_id' => 'client_id',
|
||||
'client_secret' => 'client_secret',
|
||||
]
|
||||
),
|
||||
new SettingsMap(
|
||||
$generalSettingsModel,
|
||||
[
|
||||
'is_sandbox' => 'sandbox_on',
|
||||
'live_client_id' => 'client_id_production',
|
||||
'live_client_secret' => 'client_secret_production',
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
$settingsMapHelper = new SettingsMapHelper(
|
||||
$settingsMap,
|
||||
Mockery::mock( StylingSettingsMapHelper::class ),
|
||||
Mockery::mock( SettingsTabMapHelper::class ),
|
||||
Mockery::mock( SubscriptionSettingsMapHelper::class ),
|
||||
Mockery::mock( GeneralSettingsMapHelper::class ),
|
||||
Mockery::mock( PaymentMethodSettingsMapHelper::class ),
|
||||
true
|
||||
);
|
||||
|
||||
$this->settings = new Settings(
|
||||
[ 'cart', 'checkout' ],
|
||||
'PayPal Credit Gateway',
|
||||
[ 'checkout' ],
|
||||
[ 'cart' ],
|
||||
$settingsMapHelper
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetMappedValue() {
|
||||
$value = $this->settings->get( 'pay_later_messaging_enabled' );
|
||||
|
||||
$this->assertTrue( $value );
|
||||
}
|
||||
|
||||
public function testGetThrowsNotFoundExceptionForInvalidKey() {
|
||||
$this->expectException( NotFoundException::class );
|
||||
|
||||
$this->settings->get( 'invalid_key' );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e;
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration;
|
||||
|
||||
use WooCommerce\PayPalCommerce\PPCP;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
|
@ -1,11 +1,11 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Tests\E2e\Validation;
|
||||
namespace WooCommerce\PayPalCommerce\Tests\Integration\Validation;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
|
||||
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
|
||||
use WooCommerce\PayPalCommerce\Tests\E2e\TestCase;
|
||||
use WooCommerce\PayPalCommerce\Tests\Integration\TestCase;
|
||||
|
||||
class ValidationTest extends TestCase
|
||||
{
|
23
tests/integration/PHPUnit/bootstrap.php
Normal file
23
tests/integration/PHPUnit/bootstrap.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
define('INTEGRATION_TESTS_ROOT_DIR', dirname(__DIR__));
|
||||
define('ROOT_DIR', dirname(dirname(INTEGRATION_TESTS_ROOT_DIR)));
|
||||
|
||||
require_once ROOT_DIR . '/vendor/autoload.php';
|
||||
|
||||
if (file_exists(ROOT_DIR . '/.env.integration')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(ROOT_DIR, '.env.integration');
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
if (!isset($_ENV['PPCP_INTEGRATION_WP_DIR'])) {
|
||||
exit('Copy .env.integration.example to .env.integration or define the environment variables.' . PHP_EOL);
|
||||
}
|
||||
$wpRootDir = str_replace('${ROOT_DIR}', ROOT_DIR, $_ENV['PPCP_INTEGRATION_WP_DIR']);
|
||||
|
||||
define('WP_ROOT_DIR', $wpRootDir);
|
||||
|
||||
$_SERVER['HTTP_HOST'] = ''; // just to avoid a warning
|
||||
|
||||
require_once WP_ROOT_DIR . '/wp-load.php';
|
|
@ -32,6 +32,6 @@ require WP_ROOT_DIR . '/wp-admin/includes/class-wp-importer.php';
|
|||
require WP_ROOT_DIR . '/wp-content/plugins/woocommerce/includes/admin/importers/class-wc-tax-rate-importer.php';
|
||||
|
||||
$taxImporter = new WC_Tax_Rate_Importer();
|
||||
$taxImporter->import(E2E_TESTS_ROOT_DIR . '/data/tax_rates.csv');
|
||||
$taxImporter->import(INTEGRATION_TESTS_ROOT_DIR . '/data/tax_rates.csv');
|
||||
|
||||
echo PHP_EOL;
|
|
@ -3,15 +3,15 @@
|
|||
* Plugin Name: WooCommerce PayPal Payments
|
||||
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
|
||||
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
|
||||
* Version: 2.9.6
|
||||
* Author: WooCommerce
|
||||
* Author URI: https://woocommerce.com/
|
||||
* Version: 3.0.0
|
||||
* Author: PayPal
|
||||
* Author URI: https://paypal.com/
|
||||
* License: GPL-2.0
|
||||
* Requires PHP: 7.4
|
||||
* Requires Plugins: woocommerce
|
||||
* Requires at least: 6.5
|
||||
* WC requires at least: 9.2
|
||||
* WC tested up to: 9.5
|
||||
* WC requires at least: 9.6
|
||||
* WC tested up to: 9.7
|
||||
* Text Domain: woocommerce-paypal-payments
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce
|
||||
|
@ -27,7 +27,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
|
|||
define( 'PAYPAL_URL', 'https://www.paypal.com' );
|
||||
define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' );
|
||||
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
|
||||
define( 'PAYPAL_INTEGRATION_DATE', '2024-12-31' );
|
||||
define( 'PAYPAL_INTEGRATION_DATE', '2025-03-13' );
|
||||
define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
|
||||
|
||||
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue