diff --git a/.megaignore b/.megaignore
new file mode 100644
index 000000000..6ad02d892
--- /dev/null
+++ b/.megaignore
@@ -0,0 +1 @@
+-s:*
\ No newline at end of file
diff --git a/changelog.txt b/changelog.txt
index aaf167948..8d811ad35 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,22 @@
*** Changelog ***
+= 2.8.3 - 2024-08-12 =
+* Fix - Google Pay: Prevent field validation from being triggered on checkout page load #2474
+* Fix - Do not add tax info into order meta during order creation #2471
+* Fix - PayPal declares subscription support when for Subscription mode is set Disable PayPal for subscription #2425
+* Fix - PayPal js files loaded on non PayPal pages #2411
+* Fix - Google Pay: Fix the incorrect popup triggering #2414
+* Fix - Add tax configurator when programmatically creating WC orders #2431
+* Fix - Shipping callback compatibility with WC Name Your Price plugin #2402
+* Fix - Uncaught Error: Cannot use object of type ...\Settings as array in .../AbstractPaymentMethodType.php (3253) #2334
+* Fix - Prevent displaying smart button multiple times on variable product page #2420
+* Fix - Prevent enabling Standard Card Button when ACDC is enabled #2404
+* Fix - Use client credentials for user tokens #2491
+* Fix - Apple Pay: Fix the shipping callback #2492
+* Enhancement - Separate Google Pay button for Classic Checkout #2430
+* Enhancement - Add Apple Pay and Google Pay support for China, simplify country-currency matrix #2468
+* Enhancement - Add AMEX support for Advanced Card Processing in China #2469
+
= 2.8.2 - 2024-07-22 =
* Fix - Sold individually checkbox automatically disabled after adding product to the cart more than once #2415
* Fix - All products "Sold individually" when PayPal Subscriptions selected as Subscriptions Mode #2400
diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php
index dbdf2efe3..1a4ecdc8d 100644
--- a/modules/ppcp-api-client/services.php
+++ b/modules/ppcp-api-client/services.php
@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
+use WooCommerce\PayPalCommerce\ApiClient\Authentication\ClientCredentials;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
@@ -1655,18 +1656,27 @@ return array(
return new PurchaseUnitSanitizer( $behavior, $line_name );
}
),
+ 'api.client-credentials' => static function( ContainerInterface $container ): ClientCredentials {
+ return new ClientCredentials(
+ $container->get( 'wcgateway.settings' )
+ );
+ },
+ 'api.client-credentials-cache' => static function( ContainerInterface $container ): Cache {
+ return new Cache( 'ppcp-client-credentials-cache' );
+ },
'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken {
return new UserIdToken(
$container->get( 'api.host' ),
- $container->get( 'api.bearer' ),
- $container->get( 'woocommerce.logger.woocommerce' )
+ $container->get( 'woocommerce.logger.woocommerce' ),
+ $container->get( 'api.client-credentials' )
);
},
'api.sdk-client-token' => static function( ContainerInterface $container ): SdkClientToken {
return new SdkClientToken(
$container->get( 'api.host' ),
- $container->get( 'api.bearer' ),
- $container->get( 'woocommerce.logger.woocommerce' )
+ $container->get( 'woocommerce.logger.woocommerce' ),
+ $container->get( 'api.client-credentials' ),
+ $container->get( 'api.client-credentials-cache' )
);
},
);
diff --git a/modules/ppcp-api-client/src/ApiModule.php b/modules/ppcp-api-client/src/ApiModule.php
index 42bc1f117..8b2f43911 100644
--- a/modules/ppcp-api-client/src/ApiModule.php
+++ b/modules/ppcp-api-client/src/ApiModule.php
@@ -10,6 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WC_Order;
+use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
+use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
diff --git a/modules/ppcp-api-client/src/Authentication/ClientCredentials.php b/modules/ppcp-api-client/src/Authentication/ClientCredentials.php
new file mode 100644
index 000000000..08d37bead
--- /dev/null
+++ b/modules/ppcp-api-client/src/Authentication/ClientCredentials.php
@@ -0,0 +1,49 @@
+settings = $settings;
+ }
+
+ /**
+ * Returns encoded client credentials.
+ *
+ * @return string
+ * @throws NotFoundException If setting does not found.
+ */
+ public function credentials(): string {
+ $client_id = $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : '';
+ $client_secret = $this->settings->has( 'client_secret' ) ? $this->settings->get( 'client_secret' ) : '';
+
+ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
+ return 'Basic ' . base64_encode( $client_id . ':' . $client_secret );
+ }
+}
diff --git a/modules/ppcp-api-client/src/Authentication/SdkClientToken.php b/modules/ppcp-api-client/src/Authentication/SdkClientToken.php
index ba0f3a373..5031f1291 100644
--- a/modules/ppcp-api-client/src/Authentication/SdkClientToken.php
+++ b/modules/ppcp-api-client/src/Authentication/SdkClientToken.php
@@ -11,6 +11,7 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
+use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WP_Error;
/**
@@ -20,6 +21,8 @@ class SdkClientToken {
use RequestTrait;
+ const CACHE_KEY = 'sdk-client-token-key';
+
/**
* The host.
*
@@ -27,13 +30,6 @@ class SdkClientToken {
*/
private $host;
- /**
- * The bearer.
- *
- * @var Bearer
- */
- private $bearer;
-
/**
* The logger.
*
@@ -41,35 +37,52 @@ class SdkClientToken {
*/
private $logger;
+ /**
+ * The client credentials.
+ *
+ * @var ClientCredentials
+ */
+ private $client_credentials;
+
+ /**
+ * The cache.
+ *
+ * @var Cache
+ */
+ private $cache;
+
/**
* SdkClientToken constructor.
*
- * @param string $host The host.
- * @param Bearer $bearer The bearer.
- * @param LoggerInterface $logger The logger.
+ * @param string $host The host.
+ * @param LoggerInterface $logger The logger.
+ * @param ClientCredentials $client_credentials The client credentials.
+ * @param Cache $cache The cache.
*/
public function __construct(
string $host,
- Bearer $bearer,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ ClientCredentials $client_credentials,
+ Cache $cache
) {
- $this->host = $host;
- $this->bearer = $bearer;
- $this->logger = $logger;
+ $this->host = $host;
+ $this->logger = $logger;
+ $this->client_credentials = $client_credentials;
+ $this->cache = $cache;
}
/**
- * Returns `sdk_client_token` which uniquely identifies the payer.
- *
- * @param string $target_customer_id Vaulted customer id.
+ * Returns the client token for SDK `data-sdk-client-token`.
*
* @return string
*
* @throws PayPalApiException If the request fails.
* @throws RuntimeException If something unexpected happens.
*/
- public function sdk_client_token( string $target_customer_id = '' ): string {
- $bearer = $this->bearer->bearer();
+ public function sdk_client_token(): string {
+ if ( $this->cache->has( self::CACHE_KEY ) ) {
+ return $this->cache->get( self::CACHE_KEY );
+ }
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$domain = wp_unslash( $_SERVER['HTTP_HOST'] ?? '' );
@@ -77,19 +90,10 @@ class SdkClientToken {
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=client_token&intent=sdk_init&domains[]=' . $domain;
- if ( $target_customer_id ) {
- $url = add_query_arg(
- array(
- 'target_customer_id' => $target_customer_id,
- ),
- $url
- );
- }
-
$args = array(
'method' => 'POST',
'headers' => array(
- 'Authorization' => 'Bearer ' . $bearer->token(),
+ 'Authorization' => $this->client_credentials->credentials(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
@@ -105,6 +109,11 @@ class SdkClientToken {
throw new PayPalApiException( $json, $status_code );
}
- return $json->access_token;
+ $access_token = $json->access_token;
+ $expires_in = (int) $json->expires_in;
+
+ $this->cache->set( self::CACHE_KEY, $access_token, $expires_in );
+
+ return $access_token;
}
}
diff --git a/modules/ppcp-api-client/src/Authentication/UserIdToken.php b/modules/ppcp-api-client/src/Authentication/UserIdToken.php
index cae8cb58a..dde47c223 100644
--- a/modules/ppcp-api-client/src/Authentication/UserIdToken.php
+++ b/modules/ppcp-api-client/src/Authentication/UserIdToken.php
@@ -27,13 +27,6 @@ class UserIdToken {
*/
private $host;
- /**
- * The bearer.
- *
- * @var Bearer
- */
- private $bearer;
-
/**
* The logger.
*
@@ -41,21 +34,28 @@ class UserIdToken {
*/
private $logger;
+ /**
+ * The client credentials.
+ *
+ * @var ClientCredentials
+ */
+ private $client_credentials;
+
/**
* UserIdToken constructor.
*
- * @param string $host The host.
- * @param Bearer $bearer The bearer.
- * @param LoggerInterface $logger The logger.
+ * @param string $host The host.
+ * @param LoggerInterface $logger The logger.
+ * @param ClientCredentials $client_credentials The client credentials.
*/
public function __construct(
string $host,
- Bearer $bearer,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ ClientCredentials $client_credentials
) {
- $this->host = $host;
- $this->bearer = $bearer;
- $this->logger = $logger;
+ $this->host = $host;
+ $this->logger = $logger;
+ $this->client_credentials = $client_credentials;
}
/**
@@ -69,8 +69,6 @@ class UserIdToken {
* @throws RuntimeException If something unexpected happens.
*/
public function id_token( string $target_customer_id = '' ): string {
- $bearer = $this->bearer->bearer();
-
$url = trailingslashit( $this->host ) . 'v1/oauth2/token?grant_type=client_credentials&response_type=id_token';
if ( $target_customer_id ) {
$url = add_query_arg(
@@ -84,7 +82,7 @@ class UserIdToken {
$args = array(
'method' => 'POST',
'headers' => array(
- 'Authorization' => 'Bearer ' . $bearer->token(),
+ 'Authorization' => $this->client_credentials->credentials(),
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js
index fb853182c..83b0a95b5 100644
--- a/modules/ppcp-applepay/resources/js/ApplepayButton.js
+++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js
@@ -930,10 +930,12 @@ class ApplePayButton {
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
- simplified_contact:
- this.updatedContactInfo ||
- this.initialPaymentRequest.shippingContact ||
- this.initialPaymentRequest.billingContact,
+ simplified_contact: this.hasValidContactInfo(
+ this.updatedContactInfo
+ )
+ ? this.updatedContactInfo
+ : this.initialPaymentRequest?.shippingContact ??
+ this.initialPaymentRequest?.billingContact,
product_id: productId,
products: JSON.stringify( this.products ),
caller_page: 'productDetail',
@@ -949,10 +951,12 @@ class ApplePayButton {
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
- simplified_contact:
- this.updatedContactInfo ||
- this.initialPaymentRequest.shippingContact ||
- this.initialPaymentRequest.billingContact,
+ simplified_contact: this.hasValidContactInfo(
+ this.updatedContactInfo
+ )
+ ? this.updatedContactInfo
+ : this.initialPaymentRequest?.shippingContact ??
+ this.initialPaymentRequest?.billingContact,
caller_page: 'cart',
'woocommerce-process-checkout-nonce': this.nonce,
};
@@ -1233,6 +1237,12 @@ class ApplePayButton {
return btoa( utf8Str );
}
+
+ hasValidContactInfo( value ) {
+ return Array.isArray( value )
+ ? value.length > 0
+ : Object.keys( value || {} ).length > 0;
+ }
}
export default ApplePayButton;
diff --git a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js
index cc3a3aeb2..00d7bd463 100644
--- a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js
+++ b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js
@@ -23,7 +23,7 @@ class BaseHandler {
}
shippingAllowed() {
- return this.buttonConfig.product.needsShipping;
+ return this.buttonConfig.product.needShipping;
}
transactionInfo() {
diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php
index 491e5f2aa..76de2150d 100644
--- a/modules/ppcp-applepay/services.php
+++ b/modules/ppcp-applepay/services.php
@@ -24,32 +24,33 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
- 'applepay.eligible' => static function ( ContainerInterface $container ): bool {
+ 'applepay.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'applepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
- return $apm_applies->for_country_currency();
+ return $apm_applies->for_country() && $apm_applies->for_currency();
},
- 'applepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
+ 'applepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
- $container->get( 'applepay.supported-country-currency-matrix' ),
+ $container->get( 'applepay.supported-countries' ),
+ $container->get( 'applepay.supported-currencies' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
- 'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
+ 'applepay.status-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-paypal-apple-status-cache' );
},
// We assume it's a referral if we can check product status without API request failures.
- 'applepay.is_referral' => static function ( ContainerInterface $container ): bool {
+ 'applepay.is_referral' => static function ( ContainerInterface $container ): bool {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
return ! $status->has_request_failure();
},
- 'applepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
+ 'applepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
$settings = $container->get( 'wcgateway.settings' );
return new AvailabilityNotice(
@@ -63,17 +64,17 @@ return array(
);
},
- 'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
+ 'applepay.has_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' );
},
- 'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
+ 'applepay.is_validated' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
return $settings->has( 'applepay_validated' ) ? $settings->get( 'applepay_validated' ) === true : false;
},
- 'applepay.apple-product-status' => SingletonDecorator::make(
+ 'applepay.apple-product-status' => SingletonDecorator::make(
static function( ContainerInterface $container ): AppleProductStatus {
return new AppleProductStatus(
$container->get( 'wcgateway.settings' ),
@@ -83,7 +84,7 @@ return array(
);
}
),
- 'applepay.available' => static function ( ContainerInterface $container ): bool {
+ 'applepay.available' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_applepay_validate_product_status', true ) ) {
$status = $container->get( 'applepay.apple-product-status' );
assert( $status instanceof AppleProductStatus );
@@ -94,10 +95,10 @@ return array(
}
return true;
},
- 'applepay.server_supported' => static function ( ContainerInterface $container ): bool {
+ 'applepay.server_supported' => static function ( ContainerInterface $container ): bool {
return ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off';
},
- 'applepay.is_browser_supported' => static function ( ContainerInterface $container ): bool {
+ 'applepay.is_browser_supported' => static function ( ContainerInterface $container ): bool {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$user_agent = wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' );
if ( $user_agent ) {
@@ -127,7 +128,7 @@ return array(
}
return false;
},
- 'applepay.url' => static function ( ContainerInterface $container ): string {
+ 'applepay.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
@@ -137,13 +138,13 @@ return array(
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
);
},
- 'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
+ 'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
return 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
},
- 'applepay.data_to_scripts' => static function ( ContainerInterface $container ): DataToAppleButtonScripts {
+ 'applepay.data_to_scripts' => static function ( ContainerInterface $container ): DataToAppleButtonScripts {
return new DataToAppleButtonScripts( $container->get( 'applepay.sdk_script_url' ), $container->get( 'wcgateway.settings' ) );
},
- 'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton {
+ 'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton {
return new ApplePayButton(
$container->get( 'wcgateway.settings' ),
$container->get( 'woocommerce.logger.woocommerce' ),
@@ -155,7 +156,7 @@ return array(
$container->get( 'button.helper.cart-products' )
);
},
- 'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
+ 'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
return new BlocksPaymentMethod(
'ppcp-applepay',
$container->get( 'applepay.url' ),
@@ -164,781 +165,103 @@ return array(
$container->get( 'blocks.method' )
);
},
+
/**
- * The matrix which countries and currency combinations can be used for ApplePay.
+ * The list of which countries can be used for ApplePay.
*/
- 'applepay.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
+ 'applepay.supported-countries' => static function ( ContainerInterface $container ) : array {
/**
- * Returns which countries and currency combinations can be used for ApplePay.
+ * Returns which countries can be used for ApplePay.
*/
return apply_filters(
- 'woocommerce_paypal_payments_applepay_supported_country_currency_matrix',
+ 'woocommerce_paypal_payments_applepay_supported_countries',
+ // phpcs:disable Squiz.Commenting.InlineComment
array(
- 'AU' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'AT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'BE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'BG' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'CA' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'CY' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'CZ' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'DK' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'EE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'FI' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'FR' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'DE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'GR' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'HU' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'IE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'IT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LV' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LI' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LU' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'MT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'NO' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'NL' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'PL' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'PT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'RO' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'SK' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'SI' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'ES' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'SE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'GB' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'US' => array(
- 'AUD',
- 'CAD',
- 'EUR',
- 'GBP',
- 'JPY',
- 'USD',
- ),
+ 'AU', // Australia
+ 'AT', // Austria
+ 'BE', // Belgium
+ 'BG', // Bulgaria
+ 'CA', // Canada
+ 'CN', // China
+ 'CY', // Cyprus
+ 'CZ', // Czech Republic
+ 'DK', // Denmark
+ 'EE', // Estonia
+ 'FI', // Finland
+ 'FR', // France
+ 'DE', // Germany
+ 'GR', // Greece
+ 'HU', // Hungary
+ 'IE', // Ireland
+ 'IT', // Italy
+ 'LV', // Latvia
+ 'LI', // Liechtenstein
+ 'LT', // Lithuania
+ 'LU', // Luxembourg
+ 'MT', // Malta
+ 'NL', // Netherlands
+ 'NO', // Norway
+ 'PL', // Poland
+ 'PT', // Portugal
+ 'RO', // Romania
+ 'SK', // Slovakia
+ 'SI', // Slovenia
+ 'ES', // Spain
+ 'SE', // Sweden
+ 'US', // United States
+ 'GB', // United Kingdom
)
+ // phpcs:enable Squiz.Commenting.InlineComment
);
},
- 'applepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
+ /**
+ * The list of which currencies can be used for ApplePay.
+ */
+ 'applepay.supported-currencies' => static function ( ContainerInterface $container ) : array {
+ /**
+ * Returns which currencies can be used for ApplePay.
+ */
+ return apply_filters(
+ 'woocommerce_paypal_payments_applepay_supported_currencies',
+ // phpcs:disable Squiz.Commenting.InlineComment
+ array(
+ 'AUD', // Australian Dollar
+ 'BRL', // Brazilian Real
+ 'CAD', // Canadian Dollar
+ 'CHF', // Swiss Franc
+ 'CZK', // Czech Koruna
+ 'DKK', // Danish Krone
+ 'EUR', // Euro
+ 'GBP', // British Pound Sterling
+ 'HKD', // Hong Kong Dollar
+ 'HUF', // Hungarian Forint
+ 'ILS', // Israeli New Shekel
+ 'JPY', // Japanese Yen
+ 'MXN', // Mexican Peso
+ 'NOK', // Norwegian Krone
+ 'NZD', // New Zealand Dollar
+ 'PHP', // Philippine Peso
+ 'PLN', // Polish Zloty
+ 'SEK', // Swedish Krona
+ 'SGD', // Singapore Dollar
+ 'THB', // Thai Baht
+ 'TWD', // New Taiwan Dollar
+ 'USD', // United States Dollar
+ )
+ // phpcs:enable Squiz.Commenting.InlineComment
+ );
+ },
+
+ 'applepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY';
},
- 'applepay.enable-url-live' => static function ( ContainerInterface $container ): string {
+ 'applepay.enable-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY';
},
- 'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
+ 'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
diff --git a/modules/ppcp-applepay/src/Helper/ApmApplies.php b/modules/ppcp-applepay/src/Helper/ApmApplies.php
index 0fa12420b..4deeade14 100644
--- a/modules/ppcp-applepay/src/Helper/ApmApplies.php
+++ b/modules/ppcp-applepay/src/Helper/ApmApplies.php
@@ -16,11 +16,18 @@ namespace WooCommerce\PayPalCommerce\Applepay\Helper;
class ApmApplies {
/**
- * The matrix which countries and currency combinations can be used for ApplePay.
+ * The list of which countries can be used for ApplePay.
*
* @var array
*/
- private $allowed_country_currency_matrix;
+ private $allowed_countries;
+
+ /**
+ * The list of which currencies can be used for ApplePay.
+ *
+ * @var array
+ */
+ private $allowed_currencies;
/**
* 3-letter currency code of the shop.
@@ -37,32 +44,41 @@ class ApmApplies {
private $country;
/**
- * ApmApplies constructor.
+ * DccApplies constructor.
*
- * @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for ApplePay.
+ * @param array $allowed_countries The list of which countries can be used for ApplePay.
+ * @param array $allowed_currencies The list of which currencies can be used for ApplePay.
* @param string $currency 3-letter currency code of the shop.
* @param string $country 2-letter country code of the shop.
*/
public function __construct(
- array $allowed_country_currency_matrix,
+ array $allowed_countries,
+ array $allowed_currencies,
string $currency,
string $country
) {
- $this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
- $this->currency = $currency;
- $this->country = $country;
+ $this->allowed_countries = $allowed_countries;
+ $this->allowed_currencies = $allowed_currencies;
+ $this->currency = $currency;
+ $this->country = $country;
}
/**
- * Returns whether ApplePay can be used in the current country and the current currency used.
+ * Returns whether ApplePay can be used in the current country used.
*
* @return bool
*/
- public function for_country_currency(): bool {
- if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
- return false;
- }
- return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
+ public function for_country(): bool {
+ return in_array( $this->country, $this->allowed_countries, true );
+ }
+
+ /**
+ * Returns whether ApplePay can be used in the current currency used.
+ *
+ * @return bool
+ */
+ public function for_currency(): bool {
+ return in_array( $this->currency, $this->allowed_currencies, true );
}
}
diff --git a/modules/ppcp-axo/extensions.php b/modules/ppcp-axo/extensions.php
index 8d8bf4378..636aee38e 100644
--- a/modules/ppcp-axo/extensions.php
+++ b/modules/ppcp-axo/extensions.php
@@ -83,7 +83,7 @@ return array(
->rule()
->condition_element( 'axo_enabled', '1' )
->action_visible( 'axo_gateway_title' )
- ->action_visible( 'axo_checkout_config_notice' )
+ ->action_visible( 'axo_main_notice' )
->action_visible( 'axo_privacy' )
->action_visible( 'axo_name_on_card' )
->action_visible( 'axo_style_heading' )
@@ -114,9 +114,17 @@ return array(
),
'classes' => array( 'ppcp-valign-label-middle', 'ppcp-align-label-center' ),
),
- 'axo_checkout_config_notice' => array(
+ 'axo_main_notice' => array(
'heading' => '',
- 'html' => $container->get( 'axo.checkout-config-notice' ),
+ 'html' => implode(
+ '',
+ array(
+ $container->get( 'axo.settings-conflict-notice' ),
+ $container->get( 'axo.shipping-config-notice' ),
+ $container->get( 'axo.checkout-config-notice' ),
+ $container->get( 'axo.incompatible-plugins-notice' ),
+ )
+ ),
'type' => 'ppcp-html',
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js
index b0837bcd6..34b8d24c5 100644
--- a/modules/ppcp-axo/resources/js/AxoManager.js
+++ b/modules/ppcp-axo/resources/js/AxoManager.js
@@ -709,6 +709,8 @@ class AxoManager {
}`
);
+ this.emailInput.value = this.stripSpaces( this.emailInput.value );
+
this.$( this.el.paymentContainer.selector + '-detail' ).html( '' );
this.$( this.el.paymentContainer.selector + '-form' ).html( '' );
@@ -1134,6 +1136,10 @@ class AxoManager {
return emailPattern.test( value );
}
+ stripSpaces( str ) {
+ return str.replace( /\s+/g, '' );
+ }
+
validateEmail( billingEmail ) {
const billingEmailSelector = document.querySelector( billingEmail );
const value = document.querySelector( billingEmail + ' input' ).value;
diff --git a/modules/ppcp-axo/resources/js/Helper/Debug.js b/modules/ppcp-axo/resources/js/Helper/Debug.js
index 84cda012c..6345ee935 100644
--- a/modules/ppcp-axo/resources/js/Helper/Debug.js
+++ b/modules/ppcp-axo/resources/js/Helper/Debug.js
@@ -1,7 +1,19 @@
export function log( message, level = 'info' ) {
const wpDebug = window.wc_ppcp_axo?.wp_debug;
const endpoint = window.wc_ppcp_axo?.ajax?.frontend_logger?.endpoint;
- if ( ! endpoint ) {
+ const loggingEnabled = window.wc_ppcp_axo?.logging_enabled;
+
+ if ( wpDebug ) {
+ switch ( level ) {
+ case 'error':
+ console.error( `[AXO] ${ message }` );
+ break;
+ default:
+ console.log( `[AXO] ${ message }` );
+ }
+ }
+
+ if ( ! endpoint || ! loggingEnabled ) {
return;
}
@@ -15,15 +27,5 @@ export function log( message, level = 'info' ) {
level,
},
} ),
- } ).then( () => {
- if ( wpDebug ) {
- switch ( level ) {
- case 'error':
- console.error( `[AXO] ${ message }` );
- break;
- default:
- console.log( `[AXO] ${ message }` );
- }
- }
} );
}
diff --git a/modules/ppcp-axo/services.php b/modules/ppcp-axo/services.php
index 9d4e6d5fe..5b468835f 100644
--- a/modules/ppcp-axo/services.php
+++ b/modules/ppcp-axo/services.php
@@ -12,10 +12,10 @@ 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\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
-use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
@@ -25,7 +25,7 @@ return array(
$apm_applies = $container->get( 'axo.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
- return $apm_applies->for_country_currency() && $apm_applies->for_settings();
+ return $apm_applies->for_country_currency();
},
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
@@ -36,6 +36,10 @@ return array(
);
},
+ 'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator {
+ return new SettingsNoticeGenerator();
+ },
+
// If AXO is configured and onboarded.
'axo.available' => static function ( ContainerInterface $container ): bool {
return true;
@@ -159,48 +163,35 @@ 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 );
+
+ $settings = $container->get( 'wcgateway.settings' );
+ assert( $settings instanceof Settings );
+
+ return $settings_notice_generator->generate_settings_conflict_notice( $settings );
+ },
+
'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string {
- $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
- $block_checkout_docs_link = __(
- 'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes',
- 'woocommerce-paypal-payments'
- );
+ $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
+ assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
- if ( CartCheckoutDetector::has_elementor_checkout() ) {
- $notice_content = sprintf(
- /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
- __(
- 'Warning: The Checkout page of your store currently uses the Elementor Checkout widget
. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout
or the [woocommerce_checkout]
shortcode. See this page for instructions on how to switch to the classic layout.',
- 'woocommerce-paypal-payments'
- ),
- esc_url( $checkout_page_link ),
- esc_url( $block_checkout_docs_link )
- );
- } elseif ( CartCheckoutDetector::has_block_checkout() ) {
- $notice_content = sprintf(
- /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
- __(
- 'Warning: The Checkout page of your store currently uses the WooCommerce Checkout
block. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout
or the [woocommerce_checkout]
shortcode. See this page for instructions on how to switch to the classic layout.',
- 'woocommerce-paypal-payments'
- ),
- esc_url( $checkout_page_link ),
- esc_url( $block_checkout_docs_link )
- );
- } elseif ( ! CartCheckoutDetector::has_classic_checkout() ) {
- $notice_content = sprintf(
- /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
- __(
- 'Warning: The Checkout page of your store does not seem to be properly configured or uses an incompatible third-party Checkout
solution. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout
or the [woocommerce_checkout]
shortcode. See this page for instructions on how to switch to the classic layout.',
- 'woocommerce-paypal-payments'
- ),
- esc_url( $checkout_page_link ),
- esc_url( $block_checkout_docs_link )
- );
- } else {
- return '';
- }
+ return $settings_notice_generator->generate_checkout_notice();
+ },
- return '
';
+ 'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string {
+ $settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
+ assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
+
+ return $settings_notice_generator->generate_shipping_notice();
+ },
+
+ '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 );
+
+ return $settings_notice_generator->generate_incompatible_plugins_notice();
},
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
@@ -230,6 +221,7 @@ return array(
return '';
},
+
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint {
return new FrontendLoggerEndpoint(
$container->get( 'button.request-data' ),
diff --git a/modules/ppcp-axo/src/Assets/AxoManager.php b/modules/ppcp-axo/src/Assets/AxoManager.php
index 363f0d092..2e7e60804 100644
--- a/modules/ppcp-axo/src/Assets/AxoManager.php
+++ b/modules/ppcp-axo/src/Assets/AxoManager.php
@@ -216,6 +216,7 @@ class AxoManager {
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
+ 'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
);
diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php
index 3505ff555..9b5a6985d 100644
--- a/modules/ppcp-axo/src/AxoModule.php
+++ b/modules/ppcp-axo/src/AxoModule.php
@@ -66,7 +66,7 @@ class AxoModule implements ModuleInterface {
// Add the gateway in admin area.
if ( is_admin() ) {
- $methods[] = $gateway;
+ // $methods[] = $gateway; - Temporarily remove Fastlane from the payment gateway list in admin area.
return $methods;
}
@@ -77,9 +77,10 @@ class AxoModule implements ModuleInterface {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
+ $is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
- if ( ! $is_dcc_enabled ) {
+ if ( ! $is_paypal_enabled || ! $is_dcc_enabled ) {
return $methods;
}
@@ -87,6 +88,10 @@ class AxoModule implements ModuleInterface {
return $methods;
}
+ if ( ! $this->is_compatible_shipping_config() ) {
+ return $methods;
+ }
+
$methods[] = $gateway;
return $methods;
},
@@ -144,13 +149,20 @@ class AxoModule implements ModuleInterface {
function () use ( $c ) {
$module = $this;
+ $settings = $c->get( 'wcgateway.settings' );
+ assert( $settings instanceof Settings );
+
+ $is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false;
+
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
// Check if the module is applicable, correct country, currency, ... etc.
- if ( ! $c->get( 'axo.eligible' )
+ if ( ! $is_paypal_enabled
+ || ! $c->get( 'axo.eligible' )
|| 'continuation' === $c->get( 'button.context' )
- || $subscription_helper->cart_contains_subscription() ) {
+ || $subscription_helper->cart_contains_subscription()
+ || ! $this->is_compatible_shipping_config() ) {
return;
}
@@ -194,9 +206,17 @@ class AxoModule implements ModuleInterface {
add_action(
'wp_head',
- function () {
+ function () use ( $c ) {
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
echo '';
+
+ // Add meta tag to allow feature-detection of the site's AXO payment state.
+ $settings = $c->get( 'wcgateway.settings' );
+ assert( $settings instanceof Settings );
+
+ $this->add_feature_detection_tag(
+ $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' )
+ );
}
);
@@ -280,15 +300,7 @@ class AxoModule implements ModuleInterface {
array $localized_script_data
): array {
try {
- $target_customer_id = '';
- if ( is_user_logged_in() ) {
- $target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
- if ( ! $target_customer_id ) {
- $target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
- }
- }
-
- $sdk_client_token = $api->sdk_client_token( $target_customer_id );
+ $sdk_client_token = $api->sdk_client_token();
$localized_script_data['axo'] = array(
'sdk_client_token' => $sdk_client_token,
);
@@ -341,6 +353,7 @@ class AxoModule implements ModuleInterface {
return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout()
+ && $this->is_compatible_shipping_config()
&& $is_axo_enabled
&& $is_dcc_enabled
&& ! $this->is_excluded_endpoint();
@@ -396,4 +409,32 @@ class AxoModule implements ModuleInterface {
// Exclude the Order Pay endpoint.
return is_wc_endpoint_url( 'order-pay' );
}
+
+ /**
+ * Condition to evaluate if the shipping configuration is compatible.
+ *
+ * @return bool
+ */
+ private function is_compatible_shipping_config(): bool {
+ return ! wc_shipping_enabled() || ( wc_shipping_enabled() && ! wc_ship_to_billing_address_only() );
+ }
+
+ /**
+ * Outputs a meta tag to allow feature detection on certain pages.
+ *
+ * @param bool $axo_enabled Whether the gateway is enabled.
+ * @return void
+ */
+ private function add_feature_detection_tag( bool $axo_enabled ) {
+ $show_tag = is_checkout() || is_cart() || is_shop();
+
+ if ( ! $show_tag ) {
+ return;
+ }
+
+ printf(
+ '',
+ $axo_enabled ? 'enabled' : 'disabled'
+ );
+ }
}
diff --git a/modules/ppcp-axo/src/Gateway/AxoGateway.php b/modules/ppcp-axo/src/Gateway/AxoGateway.php
index 8d50b6253..4fa80cd4a 100644
--- a/modules/ppcp-axo/src/Gateway/AxoGateway.php
+++ b/modules/ppcp-axo/src/Gateway/AxoGateway.php
@@ -168,7 +168,7 @@ class AxoGateway extends WC_Payment_Gateway {
? $this->ppcp_settings->get( 'axo_gateway_title' )
: $this->get_option( 'title', $this->method_title );
- $this->description = __( 'Enter your email address to continue.', 'woocommerce-paypal-payments' );
+ $this->description = __( 'Enter your email address above to continue.', 'woocommerce-paypal-payments' );
$this->init_form_fields();
$this->init_settings();
diff --git a/modules/ppcp-axo/src/Helper/ApmApplies.php b/modules/ppcp-axo/src/Helper/ApmApplies.php
index c5fe645a9..c8a709f55 100644
--- a/modules/ppcp-axo/src/Helper/ApmApplies.php
+++ b/modules/ppcp-axo/src/Helper/ApmApplies.php
@@ -64,19 +64,4 @@ class ApmApplies {
}
return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
}
-
- /**
- * Returns whether the settings are compatible with AXO.
- *
- * @return bool
- */
- public function for_settings(): bool {
- if ( get_option( 'woocommerce_ship_to_destination' ) === 'billing_only' ) { // Force shipping to the customer billing address.
- return false;
- }
- return true;
- }
-
-
-
}
diff --git a/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php
new file mode 100644
index 000000000..503e135cb
--- /dev/null
+++ b/modules/ppcp-axo/src/Helper/SettingsNoticeGenerator.php
@@ -0,0 +1,179 @@
+%2$s
',
+ $is_error ? 'ppcp-notice-error' : '',
+ $message
+ );
+ }
+
+ /**
+ * Generates the checkout notice.
+ *
+ * @return string
+ */
+ public function generate_checkout_notice(): string {
+ $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
+ $block_checkout_docs_link = __(
+ 'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes',
+ 'woocommerce-paypal-payments'
+ );
+
+ $notice_content = '';
+
+ if ( CartCheckoutDetector::has_elementor_checkout() ) {
+ $notice_content = sprintf(
+ /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
+ __(
+ 'Warning: The Checkout page of your store currently uses the Elementor Checkout widget
. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout
or the [woocommerce_checkout]
shortcode. See this page for instructions on how to switch to the classic layout.',
+ 'woocommerce-paypal-payments'
+ ),
+ esc_url( $checkout_page_link ),
+ esc_url( $block_checkout_docs_link )
+ );
+ } elseif ( CartCheckoutDetector::has_block_checkout() ) {
+ $notice_content = sprintf(
+ /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
+ __(
+ 'Warning: The Checkout page of your store currently uses the WooCommerce Checkout
block. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout
or the [woocommerce_checkout]
shortcode. See this page for instructions on how to switch to the classic layout.',
+ 'woocommerce-paypal-payments'
+ ),
+ esc_url( $checkout_page_link ),
+ esc_url( $block_checkout_docs_link )
+ );
+ } elseif ( ! CartCheckoutDetector::has_classic_checkout() ) {
+ $notice_content = sprintf(
+ /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
+ __(
+ 'Warning: The Checkout page of your store does not seem to be properly configured or uses an incompatible third-party Checkout
solution. To enable Fastlane and accelerate payments, the page must include either the Classic Checkout
or the [woocommerce_checkout]
shortcode. See this page for instructions on how to switch to the classic layout.',
+ 'woocommerce-paypal-payments'
+ ),
+ esc_url( $checkout_page_link ),
+ esc_url( $block_checkout_docs_link )
+ );
+ }
+
+ return $notice_content ? '' : '';
+ }
+
+ /**
+ * Generates the shipping notice.
+ *
+ * @return string
+ */
+ public function generate_shipping_notice(): string {
+ $shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' );
+
+ $notice_content = '';
+
+ if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) {
+ $notice_content = sprintf(
+ /* translators: %1$s: URL to the Shipping destination settings page. */
+ __(
+ 'Warning: The Shipping destination of your store is currently configured to Force shipping to the customer billing address
. To enable Fastlane and accelerate payments, the shipping destination must be configured either to Default to customer shipping address
or Default to customer billing address
so buyers can set separate billing and shipping details.',
+ 'woocommerce-paypal-payments'
+ ),
+ esc_url( $shipping_settings_link )
+ );
+ }
+
+ return $notice_content ? '' : '';
+ }
+
+ /**
+ * Generates the incompatible plugins notice.
+ *
+ * @return string
+ */
+ public function generate_incompatible_plugins_notice(): string {
+ $incompatible_plugins = array(
+ 'Elementor' => did_action( 'elementor/loaded' ),
+ 'CheckoutWC' => defined( 'CFW_NAME' ),
+ );
+
+ $active_plugins_list = array_filter( $incompatible_plugins );
+
+ if ( empty( $active_plugins_list ) ) {
+ return '';
+ }
+
+ $incompatible_plugin_items = array_map(
+ function ( $plugin ) {
+ return "{$plugin}";
+ },
+ array_keys( $active_plugins_list )
+ );
+
+ $plugins_settings_link = esc_url( admin_url( 'plugins.php' ) );
+ $notice_content = sprintf(
+ /* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */
+ __(
+ 'Note: The accelerated guest buyer experience provided by Fastlane may not be fully compatible with some of the following active plugins: ',
+ 'woocommerce-paypal-payments'
+ ),
+ $plugins_settings_link,
+ implode( '', $incompatible_plugin_items )
+ );
+
+ return '';
+ }
+
+ /**
+ * Generates a warning notice with instructions on conflicting plugin-internal settings.
+ *
+ * @param Settings $settings The plugin settings container, which is checked for conflicting
+ * values.
+ * @return string
+ */
+ public function generate_settings_conflict_notice( Settings $settings ) : string {
+ $notice_content = '';
+ $is_dcc_enabled = false;
+
+ try {
+ $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' );
+ // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ } catch ( NotFoundException $ignored ) {
+ // Never happens.
+ }
+
+ if ( ! $is_dcc_enabled ) {
+ $notice_content = __(
+ 'Warning: To enable Fastlane and accelerate payments, the Advanced Card Processing payment method must also be enabled.',
+ 'woocommerce-paypal-payments'
+ );
+ }
+
+ return $this->render_notice( $notice_content, true );
+ }
+}
diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js
index bf36dcb65..d1b6ad990 100644
--- a/modules/ppcp-blocks/resources/js/checkout-block.js
+++ b/modules/ppcp-blocks/resources/js/checkout-block.js
@@ -227,7 +227,7 @@ const PayPalComponent = ( {
throw new Error( config.scriptData.labels.error.generic );
}
- if ( ! shouldHandleShippingInPayPal() ) {
+ if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
@@ -318,7 +318,7 @@ const PayPalComponent = ( {
throw new Error( config.scriptData.labels.error.generic );
}
- if ( ! shouldHandleShippingInPayPal() ) {
+ if ( ! shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
} else {
setGotoContinuationOnError( true );
@@ -364,16 +364,20 @@ const PayPalComponent = ( {
};
const shouldHandleShippingInPayPal = () => {
- if ( config.finalReviewEnabled ) {
- return false;
- }
-
- return (
- window.ppcpFundingSource !== 'venmo' ||
- ! config.scriptData.vaultingEnabled
- );
+ return shouldskipFinalConfirmation() && config.needShipping
};
+ const shouldskipFinalConfirmation = () => {
+ if ( config.finalReviewEnabled ) {
+ return false;
+ }
+
+ return (
+ window.ppcpFundingSource !== 'venmo' ||
+ ! config.scriptData.vaultingEnabled
+ );
+ };
+
let handleShippingOptionsChange = null;
let handleShippingAddressChange = null;
let handleSubscriptionShippingOptionsChange = null;
@@ -544,7 +548,7 @@ const PayPalComponent = ( {
if ( config.scriptData.continuation ) {
return true;
}
- if ( shouldHandleShippingInPayPal() ) {
+ if ( shouldskipFinalConfirmation() ) {
location.href = getCheckoutRedirectUrl();
}
return true;
diff --git a/modules/ppcp-blocks/src/PayPalPaymentMethod.php b/modules/ppcp-blocks/src/PayPalPaymentMethod.php
index d94eec53d..1d0654bac 100644
--- a/modules/ppcp-blocks/src/PayPalPaymentMethod.php
+++ b/modules/ppcp-blocks/src/PayPalPaymentMethod.php
@@ -254,6 +254,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
),
),
'scriptData' => $script_data,
+ 'needShipping' => WC()->cart->needs_shipping(),
);
}
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
index ca8cba2ff..2dc75ed82 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
@@ -9,6 +9,10 @@ import {
PaymentMethods,
} from '../Helper/CheckoutMethodState';
import BootstrapHelper from '../Helper/BootstrapHelper';
+import {
+ ButtonEvents,
+ dispatchButtonEvent,
+} from '../Helper/PaymentButtonHelpers';
class CheckoutBootstap {
constructor( gateway, renderer, spinner, errorHandler ) {
@@ -70,6 +74,7 @@ class CheckoutBootstap {
jQuery( document.body ).on(
'updated_checkout payment_method_selected',
() => {
+ this.invalidatePaymentMethods();
this.updateUi();
}
);
@@ -176,6 +181,14 @@ class CheckoutBootstap {
);
}
+ invalidatePaymentMethods() {
+ /**
+ * Custom JS event to notify other modules that the payment button on the checkout page
+ * has become irrelevant or invalid.
+ */
+ dispatchButtonEvent( { event: ButtonEvents.INVALIDATE } );
+ }
+
updateUi() {
const currentPaymentMethod = getCurrentPaymentMethod();
const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL;
@@ -237,10 +250,20 @@ class CheckoutBootstap {
}
}
- setVisible( '#ppc-button-ppcp-googlepay', isGooglePayMethod );
+ /**
+ * Custom JS event that is observed by the relevant payment gateway.
+ *
+ * Dynamic part of the event name is the payment method ID, for example
+ * "ppcp-credit-card-gateway" or "ppcp-googlepay"
+ */
+ dispatchButtonEvent( {
+ event: ButtonEvents.RENDER,
+ paymentMethod: currentPaymentMethod,
+ } );
+
setVisible( '#ppc-button-ppcp-applepay', isApplePayMethod );
- jQuery( document.body ).trigger( 'ppcp_checkout_rendered' );
+ document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) );
}
shouldShowMessages() {
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js
index 3492462e7..7620547c0 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper.js
@@ -26,8 +26,11 @@ export function setupButtonEvents( refresh ) {
document.addEventListener( REFRESH_BUTTON_EVENT, debouncedRefresh );
// Listen for cart and checkout update events.
- document.body.addEventListener( 'updated_cart_totals', debouncedRefresh );
- document.body.addEventListener( 'updated_checkout', debouncedRefresh );
+ // Note: we need jQuery here, because WooCommerce uses jQuery.trigger() to dispatch the events.
+ window
+ .jQuery( 'body' )
+ .on( 'updated_cart_totals', debouncedRefresh )
+ .on( 'updated_checkout', debouncedRefresh );
// Use setTimeout for fragment events to avoid unnecessary refresh on initial render.
setTimeout( () => {
diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js
index b2dd3bbe8..4275e48d4 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js
@@ -7,6 +7,30 @@ export const PaymentMethods = {
APPLEPAY: 'ppcp-applepay',
};
+/**
+ * List of valid context values that the button can have.
+ *
+ * The "context" describes the placement or page where a payment button might be displayed.
+ *
+ * @type {Object}
+ */
+export const PaymentContext = {
+ Cart: 'cart', // Classic cart.
+ Checkout: 'checkout', // Classic checkout.
+ BlockCart: 'cart-block', // Block cart.
+ BlockCheckout: 'checkout-block', // Block checkout.
+ Product: 'product', // Single product page.
+ MiniCart: 'mini-cart', // Mini cart available on all pages except checkout & cart.
+ PayNow: 'pay-now', // Pay for order, via admin generated link.
+ Preview: 'preview', // Layout preview on settings page.
+
+ // Contexts that use blocks to render payment methods.
+ Blocks: [ 'cart-block', 'checkout-block' ],
+
+ // Contexts that display "classic" payment gateways.
+ Gateways: [ 'checkout', 'pay-now' ],
+};
+
export const ORDER_BUTTON_SELECTOR = '#place_order';
export const getCurrentPaymentMethod = () => {
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
new file mode 100644
index 000000000..f9a066a23
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
@@ -0,0 +1,117 @@
+/**
+ * Helper function used by PaymentButton instances.
+ *
+ * @file
+ */
+
+/**
+ * Collection of recognized event names for payment button events.
+ *
+ * @type {Object}
+ */
+export const ButtonEvents = Object.freeze( {
+ INVALIDATE: 'ppcp_invalidate_methods',
+ RENDER: 'ppcp_render_method',
+ REDRAW: 'ppcp_redraw_method',
+} );
+
+/**
+ *
+ * @param {string} defaultId - Default wrapper ID.
+ * @param {string} miniCartId - Wrapper inside the mini-cart.
+ * @param {string} smartButtonId - ID of the smart button wrapper.
+ * @param {string} blockId - Block wrapper ID (express checkout, block cart).
+ * @param {string} gatewayId - Gateway wrapper ID (classic checkout).
+ * @return {{MiniCart, Gateway, Block, SmartButton, Default}} List of all wrapper IDs, by context.
+ */
+export function combineWrapperIds(
+ defaultId = '',
+ miniCartId = '',
+ smartButtonId = '',
+ blockId = '',
+ gatewayId = ''
+) {
+ const sanitize = ( id ) => id.replace( /^#/, '' );
+
+ return {
+ Default: sanitize( defaultId ),
+ SmartButton: sanitize( smartButtonId ),
+ Block: sanitize( blockId ),
+ Gateway: sanitize( gatewayId ),
+ MiniCart: sanitize( miniCartId ),
+ };
+}
+
+/**
+ * Returns full payment button styles by combining the global ppcpConfig with
+ * payment-method-specific styling provided via buttonConfig.
+ *
+ * @param {Object} ppcpConfig - Global plugin configuration.
+ * @param {Object} buttonConfig - Payment method specific configuration.
+ * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
+ */
+export function combineStyles( ppcpConfig, buttonConfig ) {
+ return {
+ Default: {
+ ...ppcpConfig.style,
+ ...buttonConfig.style,
+ },
+ MiniCart: {
+ ...ppcpConfig.mini_cart_style,
+ ...buttonConfig.mini_cart_style,
+ },
+ };
+}
+
+/**
+ * Verifies if the given event name is a valid Payment Button event.
+ *
+ * @param {string} event - The event name to verify.
+ * @return {boolean} True, if the event name is valid.
+ */
+export function isValidButtonEvent( event ) {
+ const buttonEventValues = Object.values( ButtonEvents );
+
+ return buttonEventValues.includes( event );
+}
+
+/**
+ * Dispatches a payment button event.
+ *
+ * @param {Object} options - The options for dispatching the event.
+ * @param {string} options.event - Event to dispatch.
+ * @param {string} [options.paymentMethod] - Optional. Name of payment method, to target a specific button only.
+ * @throws {Error} Throws an error if the event is invalid.
+ */
+export function dispatchButtonEvent( { event, paymentMethod = '' } ) {
+ if ( ! isValidButtonEvent( event ) ) {
+ throw new Error( `Invalid event: ${ event }` );
+ }
+
+ const fullEventName = paymentMethod
+ ? `${ event }-${ paymentMethod }`
+ : event;
+
+ document.body.dispatchEvent( new Event( fullEventName ) );
+}
+
+/**
+ * Adds an event listener for the provided button event.
+ *
+ * @param {Object} options - The options for the event listener.
+ * @param {string} options.event - Event to observe.
+ * @param {string} [options.paymentMethod] - The payment method name (optional).
+ * @param {Function} options.callback - The callback function to execute when the event is triggered.
+ * @throws {Error} Throws an error if the event is invalid.
+ */
+export function observeButtonEvent( { event, paymentMethod = '', callback } ) {
+ if ( ! isValidButtonEvent( event ) ) {
+ throw new Error( `Invalid event: ${ event }` );
+ }
+
+ const fullEventName = paymentMethod
+ ? `${ event }-${ paymentMethod }`
+ : event;
+
+ document.body.addEventListener( fullEventName, callback );
+}
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js
index 0aa70f793..588e14cd6 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js
@@ -71,7 +71,10 @@ export const loadPaypalScript = ( config, onLoaded, onError = null ) => {
}
// Load PayPal script for special case with data-client-token
- if ( config.data_client_id?.set_attribute ) {
+ if (
+ config.data_client_id?.set_attribute &&
+ config.vault_v3_enabled !== '1'
+ ) {
dataClientIdAttributeHandler(
scriptOptions,
config.data_client_id,
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
new file mode 100644
index 000000000..11fe31f79
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -0,0 +1,822 @@
+import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
+import { apmButtonsInit } from '../Helper/ApmButtons';
+import {
+ getCurrentPaymentMethod,
+ PaymentContext,
+ PaymentMethods,
+} from '../Helper/CheckoutMethodState';
+import {
+ ButtonEvents,
+ dispatchButtonEvent,
+ observeButtonEvent,
+} from '../Helper/PaymentButtonHelpers';
+
+/**
+ * Collection of all available styling options for this button.
+ *
+ * @typedef {Object} StylesCollection
+ * @property {string} Default - Default button styling.
+ * @property {string} MiniCart - Styles for mini-cart button.
+ */
+
+/**
+ * Collection of all available wrapper IDs that are possible for the button.
+ *
+ * @typedef {Object} WrapperCollection
+ * @property {string} Default - Default button wrapper.
+ * @property {string} Gateway - Wrapper for separate gateway.
+ * @property {string} Block - Wrapper for block checkout button.
+ * @property {string} MiniCart - Wrapper for mini-cart button.
+ * @property {string} SmartButton - Wrapper for smart button container.
+ */
+
+/**
+ * Adds the provided PaymentButton instance to a global payment-button collection.
+ *
+ * This is debugging logic that should not be used on a production site.
+ *
+ * @param {string} methodName - Used to group the buttons.
+ * @param {PaymentButton} button - Appended to the button collection.
+ */
+const addToDebuggingCollection = ( methodName, button ) => {
+ window.ppcpPaymentButtonList = window.ppcpPaymentButtonList || {};
+
+ const collection = window.ppcpPaymentButtonList;
+
+ collection[ methodName ] = collection[ methodName ] || [];
+ collection[ methodName ].push( button );
+};
+
+/**
+ * Provides a context-independent instance Map for `PaymentButton` components.
+ *
+ * This function addresses a potential issue in multi-context environments, such as pages using
+ * Block-components. In these scenarios, multiple React execution contexts can lead to duplicate
+ * `PaymentButton` instances. To prevent this, we store instances in a `Map` that is bound to the
+ * document's `body` (the rendering context) rather than to individual React components
+ * (execution contexts).
+ *
+ * The `Map` is created as a non-enumerable, non-writable, and non-configurable property of
+ * `document.body` to ensure its integrity and prevent accidental modifications.
+ *
+ * @return {Map} A Map containing all `PaymentButton` instances for the current page.
+ */
+const getInstances = () => {
+ const collectionKey = '__ppcpPBInstances';
+
+ if ( ! document.body[ collectionKey ] ) {
+ Object.defineProperty( document.body, collectionKey, {
+ value: new Map(),
+ enumerable: false,
+ writable: false,
+ configurable: false,
+ } );
+ }
+
+ return document.body[ collectionKey ];
+};
+
+/**
+ * Base class for APM payment buttons, like GooglePay and ApplePay.
+ *
+ * This class is not intended for the PayPal button.
+ */
+export default class PaymentButton {
+ /**
+ * Defines the implemented payment method.
+ *
+ * Used to identify and address the button internally.
+ * Overwrite this in the derived class.
+ *
+ * @type {string}
+ */
+ static methodId = 'generic';
+
+ /**
+ * CSS class that is added to the payment button wrapper.
+ *
+ * Overwrite this in the derived class.
+ *
+ * @type {string}
+ */
+ static cssClass = '';
+
+ /**
+ * @type {ConsoleLogger}
+ */
+ #logger;
+
+ /**
+ * Whether the payment button is initialized.
+ *
+ * @type {boolean}
+ */
+ #isInitialized = false;
+
+ /**
+ * The button's context.
+ *
+ * @type {string}
+ */
+ #context;
+
+ /**
+ * Object containing the IDs of all possible wrapper elements that might contain this
+ * button; only one wrapper is relevant, depending on the value of the context.
+ *
+ * @type {Object}
+ */
+ #wrappers;
+
+ /**
+ * @type {StylesCollection}
+ */
+ #styles;
+
+ /**
+ * Keeps track of CSS classes that were added to the wrapper element.
+ * We use this list to remove CSS classes that we've added, e.g. to change shape from
+ * pill to rect in the preview.
+ *
+ * @type {string[]}
+ */
+ #appliedClasses = [];
+
+ /**
+ * APM relevant configuration; e.g., configuration of the GooglePay button.
+ */
+ #buttonConfig;
+
+ /**
+ * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
+ */
+ #ppcpConfig;
+
+ /**
+ * A variation of a context bootstrap handler.
+ */
+ #externalHandler;
+
+ /**
+ * A variation of a context handler object, like CheckoutHandler.
+ * This handler provides a standardized interface for certain standardized checks and actions.
+ */
+ #contextHandler;
+
+ /**
+ * Whether the current browser/website support the payment method.
+ *
+ * @type {boolean}
+ */
+ #isEligible = false;
+
+ /**
+ * Whether this button is visible. Modified by `show()` and `hide()`
+ *
+ * @type {boolean}
+ */
+ #isVisible = true;
+
+ /**
+ * The currently visible payment button.
+ *
+ * @see {PaymentButton.insertButton}
+ * @type {HTMLElement|null}
+ */
+ #button = null;
+
+ /**
+ * Factory method to create a new PaymentButton while limiting a single instance per context.
+ *
+ * @param {string} context - Button context name.
+ * @param {unknown} externalHandler - Handler object.
+ * @param {Object} buttonConfig - Payment button specific configuration.
+ * @param {Object} ppcpConfig - Plugin wide configuration object.
+ * @param {unknown} contextHandler - Handler object.
+ * @return {PaymentButton} The button instance.
+ */
+ static createButton(
+ context,
+ externalHandler,
+ buttonConfig,
+ ppcpConfig,
+ contextHandler
+ ) {
+ const buttonInstances = getInstances();
+ const instanceKey = `${ this.methodId }.${ context }`;
+
+ if ( ! buttonInstances.has( instanceKey ) ) {
+ const button = new this(
+ context,
+ externalHandler,
+ buttonConfig,
+ ppcpConfig,
+ contextHandler
+ );
+
+ buttonInstances.set( instanceKey, button );
+ }
+
+ return buttonInstances.get( instanceKey );
+ }
+
+ /**
+ * Returns a list with all wrapper IDs for the implemented payment method, categorized by
+ * context.
+ *
+ * @abstract
+ * @param {Object} buttonConfig - Payment method specific configuration.
+ * @param {Object} ppcpConfig - Global plugin configuration.
+ * @return {{MiniCart, Gateway, Block, SmartButton, Default}} The wrapper ID collection.
+ */
+ // eslint-disable-next-line no-unused-vars
+ static getWrappers( buttonConfig, ppcpConfig ) {
+ throw new Error( 'Must be implemented in the child class' );
+ }
+
+ /**
+ * Returns a list of all button styles for the implemented payment method, categorized by
+ * context.
+ *
+ * @abstract
+ * @param {Object} buttonConfig - Payment method specific configuration.
+ * @param {Object} ppcpConfig - Global plugin configuration.
+ * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
+ */
+ // eslint-disable-next-line no-unused-vars
+ static getStyles( buttonConfig, ppcpConfig ) {
+ throw new Error( 'Must be implemented in the child class' );
+ }
+
+ /**
+ * Initialize the payment button instance.
+ *
+ * Do not create new button instances directly; use the `createButton` method instead
+ * to avoid multiple button instances handling the same context.
+ *
+ * @private
+ * @param {string} context - Button context name.
+ * @param {Object} externalHandler - Handler object.
+ * @param {Object} buttonConfig - Payment button specific configuration.
+ * @param {Object} ppcpConfig - Plugin wide configuration object.
+ * @param {Object} contextHandler - Handler object.
+ */
+ constructor(
+ context,
+ externalHandler = null,
+ buttonConfig = {},
+ ppcpConfig = {},
+ contextHandler = null
+ ) {
+ if ( this.methodId === PaymentButton.methodId ) {
+ throw new Error( 'Cannot initialize the PaymentButton base class' );
+ }
+
+ if ( ! buttonConfig ) {
+ buttonConfig = {};
+ }
+
+ const isDebugging = !! buttonConfig?.is_debug;
+ const methodName = this.methodId.replace( /^ppcp?-/, '' );
+
+ this.#context = context;
+ this.#buttonConfig = buttonConfig;
+ this.#ppcpConfig = ppcpConfig;
+ this.#externalHandler = externalHandler;
+ this.#contextHandler = contextHandler;
+
+ this.#logger = new ConsoleLogger( methodName, context );
+
+ if ( isDebugging ) {
+ this.#logger.enabled = true;
+ addToDebuggingCollection( methodName, this );
+ }
+
+ this.#wrappers = this.constructor.getWrappers(
+ this.#buttonConfig,
+ this.#ppcpConfig
+ );
+ this.applyButtonStyles( this.#buttonConfig );
+
+ apmButtonsInit( this.#ppcpConfig );
+ this.initEventListeners();
+ }
+
+ /**
+ * Internal ID of the payment gateway.
+ *
+ * @readonly
+ * @return {string} The internal gateway ID, defined in the derived class.
+ */
+ get methodId() {
+ return this.constructor.methodId;
+ }
+
+ /**
+ * CSS class that is added to the button wrapper.
+ *
+ * @readonly
+ * @return {string} CSS class, defined in the derived class.
+ */
+ get cssClass() {
+ return this.constructor.cssClass;
+ }
+
+ /**
+ * Whether the payment button was fully initialized.
+ *
+ * @readonly
+ * @return {boolean} True indicates, that the button was fully initialized.
+ */
+ get isInitialized() {
+ return this.#isInitialized;
+ }
+
+ /**
+ * The button's context.
+ *
+ * TODO: Convert the string to a context-object (primitive obsession smell)
+ *
+ * @readonly
+ * @return {string} The button context.
+ */
+ get context() {
+ return this.#context;
+ }
+
+ /**
+ * Configuration, specific for the implemented payment button.
+ *
+ * @return {Object} Configuration object.
+ */
+ get buttonConfig() {
+ return this.#buttonConfig;
+ }
+
+ /**
+ * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
+ *
+ * @return {Object} Configuration object.
+ */
+ get ppcpConfig() {
+ return this.#ppcpConfig;
+ }
+
+ /**
+ * @return {Object} The bootstrap handler instance, or an empty object.
+ */
+ get externalHandler() {
+ return this.#externalHandler || {};
+ }
+
+ /**
+ * Access the button's context handler.
+ * When no context handler was provided (like for a preview button), an empty object is
+ * returned.
+ *
+ * @return {Object} The context handler instance, or an empty object.
+ */
+ get contextHandler() {
+ return this.#contextHandler || {};
+ }
+
+ /**
+ * Whether customers need to provide shipping details during payment.
+ *
+ * Can be extended by child classes to take method specific configuration into account.
+ *
+ * @return {boolean} True means, shipping fields are displayed and must be filled.
+ */
+ get requiresShipping() {
+ // Default check: Is shipping enabled in WooCommerce?
+ return (
+ 'function' === typeof this.contextHandler.shippingAllowed &&
+ this.contextHandler.shippingAllowed()
+ );
+ }
+
+ /**
+ * Button wrapper details.
+ *
+ * @readonly
+ * @return {WrapperCollection} Wrapper IDs.
+ */
+ get wrappers() {
+ return this.#wrappers;
+ }
+
+ /**
+ * Returns the context-relevant button style object.
+ *
+ * @readonly
+ * @return {string} Styling options.
+ */
+ get style() {
+ if ( PaymentContext.MiniCart === this.context ) {
+ return this.#styles.MiniCart;
+ }
+
+ return this.#styles.Default;
+ }
+
+ /**
+ * Returns the context-relevant wrapper ID.
+ *
+ * @readonly
+ * @return {string} The wrapper-element's ID (without the `#` prefix).
+ */
+ get wrapperId() {
+ if ( PaymentContext.MiniCart === this.context ) {
+ return this.wrappers.MiniCart;
+ } else if ( this.isSeparateGateway ) {
+ return this.wrappers.Gateway;
+ } else if ( PaymentContext.Blocks.includes( this.context ) ) {
+ return this.wrappers.Block;
+ }
+
+ return this.wrappers.Default;
+ }
+
+ /**
+ * Determines if the current payment button should be rendered as a stand-alone gateway.
+ * The return value `false` usually means, that the payment button is bundled with all available
+ * payment buttons.
+ *
+ * The decision depends on the button context (placement) and the plugin settings.
+ *
+ * @return {boolean} True, if the current button represents a stand-alone gateway.
+ */
+ get isSeparateGateway() {
+ return (
+ this.#buttonConfig.is_wc_gateway_enabled &&
+ PaymentContext.Gateways.includes( this.context )
+ );
+ }
+
+ /**
+ * Whether the currently selected payment gateway is set to the payment method.
+ *
+ * Only relevant on checkout pages, when `this.isSeparateGateway` is true.
+ *
+ * @return {boolean} True means that this payment method is selected as current gateway.
+ */
+ get isCurrentGateway() {
+ if ( ! this.isSeparateGateway ) {
+ return false;
+ }
+
+ /*
+ * We need to rely on `getCurrentPaymentMethod()` here, as the `CheckoutBootstrap.js`
+ * module fires the "ButtonEvents.RENDER" event before any PaymentButton instances are
+ * created. I.e. we cannot observe the initial gateway selection event.
+ */
+ return this.methodId === getCurrentPaymentMethod();
+ }
+
+ /**
+ * Flags a preview button without actual payment logic.
+ *
+ * @return {boolean} True indicates a preview instance that has no payment logic.
+ */
+ get isPreview() {
+ return PaymentContext.Preview === this.context;
+ }
+
+ /**
+ * Whether the browser can accept this payment method.
+ *
+ * @return {boolean} True, if payments are technically possible.
+ */
+ get isEligible() {
+ return this.#isEligible;
+ }
+
+ /**
+ * Changes the eligibility state of this button component.
+ *
+ * @param {boolean} newState Whether the browser can accept payments.
+ */
+ set isEligible( newState ) {
+ if ( newState === this.#isEligible ) {
+ return;
+ }
+
+ this.#isEligible = newState;
+ this.triggerRedraw();
+ }
+
+ /**
+ * The visibility state of the button.
+ * This flag does not reflect actual visibility on the page, but rather, if the button
+ * is intended/allowed to be displayed, in case all other checks pass.
+ *
+ * @return {boolean} True indicates, that the button can be displayed.
+ */
+ get isVisible() {
+ return this.#isVisible;
+ }
+
+ /**
+ * Change the visibility of the button.
+ *
+ * A visible button does not always force the button to render on the page. It only means, that
+ * the button is allowed or not allowed to render, if certain other conditions are met.
+ *
+ * @param {boolean} newState Whether rendering the button is allowed.
+ */
+ set isVisible( newState ) {
+ if ( this.#isVisible === newState ) {
+ return;
+ }
+
+ this.#isVisible = newState;
+ this.triggerRedraw();
+ }
+
+ /**
+ * Returns the HTML element that wraps the current button
+ *
+ * @readonly
+ * @return {HTMLElement|null} The wrapper element, or null.
+ */
+ get wrapperElement() {
+ return document.getElementById( this.wrapperId );
+ }
+
+ /**
+ * Checks whether the main button-wrapper is present in the current DOM.
+ *
+ * @readonly
+ * @return {boolean} True, if the button context (wrapper element) is found.
+ */
+ get isPresent() {
+ return this.wrapperElement instanceof HTMLElement;
+ }
+
+ /**
+ * Checks, if the payment button is still attached to the DOM.
+ *
+ * WooCommerce performs some partial reloads in many cases, which can lead to our payment
+ * button
+ * to move into the browser's memory. In that case, we need to recreate the button in the
+ * updated DOM.
+ *
+ * @return {boolean} True means, the button is still present (and typically visible) on the
+ * page.
+ */
+ get isButtonAttached() {
+ if ( ! this.#button ) {
+ return false;
+ }
+
+ let parent = this.#button.parentElement;
+ while ( parent?.parentElement ) {
+ if ( 'BODY' === parent.tagName ) {
+ return true;
+ }
+
+ parent = parent.parentElement;
+ }
+
+ return false;
+ }
+
+ /**
+ * Log a debug detail to the browser console.
+ *
+ * @param {any} args
+ */
+ log( ...args ) {
+ this.#logger.log( ...args );
+ }
+
+ /**
+ * Log an error message to the browser console.
+ *
+ * @param {any} args
+ */
+ error( ...args ) {
+ this.#logger.error( ...args );
+ }
+
+ /**
+ * Determines if the current button instance has valid and complete configuration details.
+ * Used during initialization to decide if the button can be initialized or should be skipped.
+ *
+ * Can be implemented by the derived class.
+ *
+ * @param {boolean} [silent=false] - Set to true to suppress console errors.
+ * @return {boolean} True indicates the config is valid and initialization can continue.
+ */
+ validateConfiguration( silent = false ) {
+ return true;
+ }
+
+ applyButtonStyles( buttonConfig, ppcpConfig = null ) {
+ if ( ! ppcpConfig ) {
+ ppcpConfig = this.ppcpConfig;
+ }
+
+ this.#styles = this.constructor.getStyles( buttonConfig, ppcpConfig );
+
+ if ( this.isInitialized ) {
+ this.triggerRedraw();
+ }
+ }
+
+ /**
+ * Configures the button instance. Must be called before the initial `init()`.
+ *
+ * Parameters are defined by the derived class.
+ *
+ * @abstract
+ */
+ configure() {}
+
+ /**
+ * Must be named `init()` to simulate "protected" visibility:
+ * Since the derived class also implements a method with the same name, this method can only
+ * be called by the derived class, but not from any other code.
+ */
+ init() {
+ this.#isInitialized = true;
+ }
+
+ /**
+ * Must be named `reinit()` to simulate "protected" visibility:
+ * Since the derived class also implements a method with the same name, this method can only
+ * be called by the derived class, but not from any other code.
+ */
+ reinit() {
+ this.#isInitialized = false;
+ this.#isEligible = false;
+ }
+
+ triggerRedraw() {
+ this.showPaymentGateway();
+
+ dispatchButtonEvent( {
+ event: ButtonEvents.REDRAW,
+ paymentMethod: this.methodId,
+ } );
+ }
+
+ /**
+ * Attaches event listeners to show or hide the payment button when needed.
+ */
+ initEventListeners() {
+ // Refresh the button - this might show, hide or re-create the payment button.
+ observeButtonEvent( {
+ event: ButtonEvents.REDRAW,
+ paymentMethod: this.methodId,
+ callback: () => this.refresh(),
+ } );
+
+ // Events relevant for buttons inside a payment gateway.
+ if ( PaymentContext.Gateways.includes( this.context ) ) {
+ const parentMethod = this.isSeparateGateway
+ ? this.methodId
+ : PaymentMethods.PAYPAL;
+
+ // Hide the button right after the user selected _any_ gateway.
+ observeButtonEvent( {
+ event: ButtonEvents.INVALIDATE,
+ callback: () => ( this.isVisible = false ),
+ } );
+
+ // Show the button (again) when the user selected the current gateway.
+ observeButtonEvent( {
+ event: ButtonEvents.RENDER,
+ paymentMethod: parentMethod,
+ callback: () => ( this.isVisible = true ),
+ } );
+ }
+ }
+
+ /**
+ * Refreshes the payment button on the page.
+ */
+ refresh() {
+ if ( ! this.isPresent ) {
+ return;
+ }
+
+ this.applyWrapperStyles();
+
+ if ( this.isEligible && this.isPresent && this.isVisible ) {
+ if ( ! this.isButtonAttached ) {
+ this.log( 'refresh.addButton' );
+ this.addButton();
+ }
+ }
+ }
+
+ /**
+ * Makes the custom payment gateway visible by removing initial inline styles from the DOM.
+ *
+ * Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
+ */
+ showPaymentGateway() {
+ if ( ! this.isSeparateGateway || ! this.isEligible ) {
+ return;
+ }
+
+ const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`;
+
+ const styles = document.querySelectorAll( styleSelectors );
+
+ if ( ! styles.length ) {
+ return;
+ }
+ this.log( 'Show gateway' );
+
+ styles.forEach( ( el ) => el.remove() );
+
+ // This code runs only once, during button initialization, and fixes the initial visibility.
+ this.isVisible = this.isCurrentGateway;
+ }
+
+ /**
+ * Applies CSS classes and inline styling to the payment button wrapper.
+ */
+ applyWrapperStyles() {
+ const wrapper = this.wrapperElement;
+ const { shape, height } = this.style;
+
+ for ( const classItem of this.#appliedClasses ) {
+ wrapper.classList.remove( classItem );
+ }
+
+ this.#appliedClasses = [];
+
+ const newClasses = [
+ `ppcp-button-${ shape }`,
+ 'ppcp-button-apm',
+ this.cssClass,
+ ];
+
+ wrapper.classList.add( ...newClasses );
+ this.#appliedClasses.push( ...newClasses );
+
+ if ( height ) {
+ wrapper.style.height = `${ height }px`;
+ }
+
+ // Apply the wrapper visibility.
+ wrapper.style.display = this.isVisible ? 'block' : 'none';
+ }
+
+ /**
+ * Creates a new payment button (HTMLElement) and must call `this.insertButton()` to display
+ * that button in the correct wrapper.
+ *
+ * @abstract
+ */
+ addButton() {
+ throw new Error( 'Must be implemented by the child class' );
+ }
+
+ /**
+ * Prepares the button wrapper element and inserts the provided payment button into the DOM.
+ *
+ * If a payment button was previously inserted to the wrapper, calling this method again will
+ * first remove the previous button.
+ *
+ * @param {HTMLElement} button - The button element to inject.
+ */
+ insertButton( button ) {
+ if ( ! this.isPresent ) {
+ return;
+ }
+
+ const wrapper = this.wrapperElement;
+
+ if ( this.#button ) {
+ this.removeButton();
+ }
+
+ this.log( 'addButton', button );
+
+ this.#button = button;
+ wrapper.appendChild( this.#button );
+ }
+
+ /**
+ * Removes the payment button from the DOM.
+ */
+ removeButton() {
+ if ( ! this.isPresent || ! this.#button ) {
+ return;
+ }
+
+ this.log( 'removeButton' );
+
+ try {
+ this.wrapperElement.removeChild( this.#button );
+ } catch ( Exception ) {
+ // Ignore this.
+ }
+
+ this.#button = null;
+ }
+}
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js
index 42e715904..1ea2f04bc 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js
@@ -1,11 +1,17 @@
import { loadCustomScript } from '@paypal/paypal-js';
import widgetBuilder from './WidgetBuilder';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
+import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
/**
* Manages all PreviewButton instances of a certain payment method on the page.
*/
class PreviewButtonManager {
+ /**
+ * @type {ConsoleLogger}
+ */
+ #logger;
+
/**
* Resolves the promise.
* Used by `this.boostrap()` to process enqueued initialization logic.
@@ -32,6 +38,9 @@ class PreviewButtonManager {
this.apiConfig = null;
this.apiError = '';
+ this.#logger = new ConsoleLogger( this.methodName, 'preview-manager' );
+ this.#logger.enabled = true; // Manually set this to true for development.
+
this.#onInit = new Promise( ( resolve ) => {
this.#onInitResolver = resolve;
} );
@@ -61,9 +70,11 @@ class PreviewButtonManager {
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
+ * @abstract
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
+ // eslint-disable-next-line no-unused-vars
async fetchConfig( payPal ) {
throw new Error(
'The "fetchConfig" method must be implemented by the derived class'
@@ -74,9 +85,11 @@ class PreviewButtonManager {
* Protected method that needs to be implemented by the derived class.
* This method is responsible for creating a new PreviewButton instance and returning it.
*
+ * @abstract
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {PreviewButton}
*/
+ // eslint-disable-next-line no-unused-vars
createButtonInstance( wrapperId ) {
throw new Error(
'The "createButtonInstance" method must be implemented by the derived class'
@@ -90,7 +103,7 @@ class PreviewButtonManager {
* This dummy is only visible on the admin side, and not rendered on the front-end.
*
* @todo Consider refactoring this into a new class that extends the PreviewButton class.
- * @param wrapperId
+ * @param {string} wrapperId
* @return {any}
*/
createDummy( wrapperId ) {
@@ -128,13 +141,24 @@ class PreviewButtonManager {
);
}
+ /**
+ * Output a debug message to the console, with a module-specific prefix.
+ *
+ * @param {string} message - Log message.
+ * @param {...any} args - Optional. Additional args to output.
+ */
+ log( message, ...args ) {
+ this.#logger.log( message, ...args );
+ }
+
/**
* Output an error message to the console, with a module-specific prefix.
- * @param message
- * @param {...any} args
+ *
+ * @param {string} message - Log message.
+ * @param {...any} args - Optional. Additional args to output.
*/
error( message, ...args ) {
- console.error( `${ this.methodName } ${ message }`, ...args );
+ this.#logger.error( message, ...args );
}
/**
@@ -242,21 +266,21 @@ class PreviewButtonManager {
}
if ( ! this.shouldInsertPreviewButton( id ) ) {
+ this.log( 'Skip preview rendering for this preview-box', id );
return;
}
if ( ! this.buttons[ id ] ) {
this._addButton( id, ppcpConfig );
} else {
- // This is a debounced method, that fires after 100ms.
- this._configureAllButtons( ppcpConfig );
+ this._configureButton( id, ppcpConfig );
}
}
/**
* Determines if the preview box supports the current button.
*
- * When this function returns false, this manager instance does not create a new preview button.
+ * E.g. "Should the current preview-box display Google Pay buttons?"
*
* @param {string} previewId - ID of the inner preview box container.
* @return {boolean} True if the box is eligible for the preview button, false otherwise.
@@ -271,10 +295,14 @@ class PreviewButtonManager {
/**
* Applies a new configuration to an existing preview button.
+ *
+ * @private
* @param id
* @param ppcpConfig
*/
_configureButton( id, ppcpConfig ) {
+ this.log( 'configureButton', id, ppcpConfig );
+
this.buttons[ id ]
.setDynamic( this.isDynamic() )
.setPpcpConfig( ppcpConfig )
@@ -283,9 +311,13 @@ class PreviewButtonManager {
/**
* Apples the provided configuration to all existing preview buttons.
- * @param ppcpConfig
+ *
+ * @private
+ * @param ppcpConfig - The new styling to use for the preview buttons.
*/
_configureAllButtons( ppcpConfig ) {
+ this.log( 'configureAllButtons', ppcpConfig );
+
Object.entries( this.buttons ).forEach( ( [ id, button ] ) => {
this._configureButton( id, {
...ppcpConfig,
@@ -302,13 +334,20 @@ class PreviewButtonManager {
/**
* Creates a new preview button, that is rendered once the bootstrapping Promise resolves.
- * @param id
- * @param ppcpConfig
+ *
+ * @private
+ * @param id - The button to add.
+ * @param ppcpConfig - The styling to apply to the preview button.
*/
_addButton( id, ppcpConfig ) {
+ this.log( 'addButton', id, ppcpConfig );
+
const createButton = () => {
if ( ! this.buttons[ id ] ) {
+ this.log( 'createButton.new', id );
+
let newInst;
+
if ( this.apiConfig && 'object' === typeof this.apiConfig ) {
newInst = this.createButtonInstance( id ).setButtonConfig(
this.buttonConfig
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index 448432e3c..7d6139d4c 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -139,7 +139,7 @@ class Renderer {
};
// Check the condition and add the handler if needed
- if ( this.defaultSettings.should_handle_shipping_in_paypal ) {
+ if ( this.defaultSettings.should_handle_shipping_in_paypal && this.defaultSettings.needShipping ) {
options.onShippingOptionsChange = ( data, actions ) => {
let shippingOptionsChange =
! this.isVenmoButtonClickedWhenVaultingIsEnabled(
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index 4b309818e..e54b3c97f 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -1295,6 +1295,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'is_logged' => is_user_logged_in(),
),
'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(),
+ 'needShipping' => WC()->cart->needs_shipping(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
);
diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php
index 5ff2b02e6..888a567c5 100644
--- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php
+++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php
@@ -303,13 +303,6 @@ class WooCommerceOrderCreator {
$item->set_tax_class( $product->get_tax_class() );
$item->set_total_tax( (float) array_sum( $taxes ) );
-
- foreach ( $taxes as $tax_rate_id => $tax_amount ) {
- if ( $tax_amount > 0 ) {
- $item->add_meta_data( 'tax_rate_id', $tax_rate_id, true );
- $item->add_meta_data( 'tax_amount', $tax_amount, true );
- }
- }
}
/**
diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss
index 6cf2119a0..93cb05f47 100644
--- a/modules/ppcp-googlepay/resources/css/styles.scss
+++ b/modules/ppcp-googlepay/resources/css/styles.scss
@@ -1,3 +1,10 @@
+/* Front end display */
+.ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container {
+ outline-offset: -1px;
+ border-radius: var(--apm-button-border-radius);
+}
+
+/* Admin preview */
.ppcp-button-googlepay {
min-height: 40px;
diff --git a/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js b/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js
index 251b4f3c4..b4a07709e 100644
--- a/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js
+++ b/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js
@@ -4,7 +4,7 @@ import CheckoutActionHandler from '../../../../ppcp-button/resources/js/modules/
import FormValidator from '../../../../ppcp-button/resources/js/modules/Helper/FormValidator';
class CheckoutHandler extends BaseHandler {
- transactionInfo() {
+ validateForm() {
return new Promise( async ( resolve, reject ) => {
try {
const spinner = new Spinner();
@@ -23,7 +23,7 @@ class CheckoutHandler extends BaseHandler {
: null;
if ( ! formValidator ) {
- resolve( super.transactionInfo() );
+ resolve();
return;
}
@@ -42,7 +42,7 @@ class CheckoutHandler extends BaseHandler {
reject();
} else {
- resolve( super.transactionInfo() );
+ resolve();
}
} );
} catch ( error ) {
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 87bc642f0..4b267c4e7 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,10 +1,120 @@
-import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
-import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
+import {
+ combineStyles,
+ combineWrapperIds,
+} from '../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
+import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
-import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
+import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
+
+/**
+ * Plugin-specific styling.
+ *
+ * Note that most properties of this object do not apply to the Google Pay button.
+ *
+ * @typedef {Object} PPCPStyle
+ * @property {string} shape - Outline shape.
+ * @property {?number} height - Button height in pixel.
+ */
+
+/**
+ * Style options that are defined by the Google Pay SDK and are required to render the button.
+ *
+ * @typedef {Object} GooglePayStyle
+ * @property {string} type - Defines the button label.
+ * @property {string} color - Button color
+ * @property {string} language - The locale; an empty string will apply the user-agent's language.
+ */
+
+/**
+ * Google Pay JS SDK
+ *
+ * @see https://developers.google.com/pay/api/web/reference/request-objects
+ * @typedef {Object} GooglePaySDK
+ * @property {typeof PaymentsClient} PaymentsClient - Main API client for payment actions.
+ */
+
+/**
+ * The Payments Client class, generated by the Google Pay SDK.
+ *
+ * @see https://developers.google.com/pay/api/web/reference/client
+ * @typedef {Object} PaymentsClient
+ * @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage.
+ * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API.
+ * @property {Function} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters
+ * @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet.
+ * @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options.
+ */
+
+/**
+ * This object describes the transaction details.
+ *
+ * @see https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo
+ * @typedef {Object} TransactionInfo
+ * @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code.
+ * @property {string} countryCode - Optional. required for EEA countries,
+ * @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting.
+ * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used:
+ * @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places.
+ * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
+ * @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items.
+ * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet.
+ */
+
+class GooglepayButton extends PaymentButton {
+ /**
+ * @inheritDoc
+ */
+ static methodId = PaymentMethods.GOOGLEPAY;
+
+ /**
+ * @inheritDoc
+ */
+ static cssClass = 'google-pay';
+
+ /**
+ * Client reference, provided by the Google Pay JS SDK.
+ */
+ #paymentsClient = null;
+
+ /**
+ * Details about the processed transaction.
+ *
+ * @type {?TransactionInfo}
+ */
+ #transactionInfo = null;
+
+ googlePayConfig = null;
+
+ /**
+ * @inheritDoc
+ */
+ static getWrappers( buttonConfig, ppcpConfig ) {
+ return combineWrapperIds(
+ buttonConfig?.button?.wrapper || '',
+ buttonConfig?.button?.mini_cart_wrapper || '',
+ ppcpConfig?.button?.wrapper || '',
+ 'ppc-button-googlepay-container',
+ 'ppc-button-ppcp-googlepay'
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ static getStyles( buttonConfig, ppcpConfig ) {
+ const styles = combineStyles(
+ ppcpConfig?.button || {},
+ buttonConfig?.button || {}
+ );
+
+ if ( 'buy' === styles.MiniCart.type ) {
+ styles.MiniCart.type = 'pay';
+ }
+
+ return styles;
+ }
-class GooglepayButton {
constructor(
context,
externalHandler,
@@ -12,274 +122,257 @@ class GooglepayButton {
ppcpConfig,
contextHandler
) {
- apmButtonsInit( ppcpConfig );
+ // Disable debug output in the browser console:
+ // buttonConfig.is_debug = false;
- this.isInitialized = false;
+ super(
+ context,
+ externalHandler,
+ buttonConfig,
+ ppcpConfig,
+ contextHandler
+ );
- this.context = context;
- this.externalHandler = externalHandler;
- this.buttonConfig = buttonConfig;
- this.ppcpConfig = ppcpConfig;
- this.contextHandler = contextHandler;
+ this.init = this.init.bind( this );
+ this.onPaymentAuthorized = this.onPaymentAuthorized.bind( this );
+ this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
+ this.onButtonClick = this.onButtonClick.bind( this );
- this.paymentsClient = null;
+ this.log( 'Create instance' );
+ }
- this.log = function () {
- if ( this.buttonConfig.is_debug ) {
- //console.log('[GooglePayButton]', ...arguments);
+ /**
+ * @inheritDoc
+ */
+ get requiresShipping() {
+ return super.requiresShipping && this.buttonConfig.shipping?.enabled;
+ }
+
+ /**
+ * The Google Pay API.
+ *
+ * @return {?GooglePaySDK} API for the Google Pay JS SDK, or null when SDK is not ready yet.
+ */
+ get googlePayApi() {
+ return window.google?.payments?.api;
+ }
+
+ /**
+ * The Google Pay PaymentsClient instance created by this button.
+ * @see https://developers.google.com/pay/api/web/reference/client
+ *
+ * @return {?PaymentsClient} The SDK object, or null when SDK is not ready yet.
+ */
+ get paymentsClient() {
+ return this.#paymentsClient;
+ }
+
+ /**
+ * Details about the processed transaction.
+ *
+ * This object defines the price that is charged, and text that is displayed inside the
+ * payment sheet.
+ *
+ * @return {?TransactionInfo} The TransactionInfo object.
+ */
+ get transactionInfo() {
+ return this.#transactionInfo;
+ }
+
+ /**
+ * Assign the new transaction details to the payment button.
+ *
+ * @param {TransactionInfo} newTransactionInfo - Transaction details.
+ */
+ set transactionInfo( newTransactionInfo ) {
+ this.#transactionInfo = newTransactionInfo;
+
+ this.refresh();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ validateConfiguration( silent = false ) {
+ const validEnvs = [ 'PRODUCTION', 'TEST' ];
+
+ const isInvalid = ( ...args ) => {
+ if ( ! silent ) {
+ this.error( ...args );
}
+ return false;
};
- }
- init( config, transactionInfo ) {
- if ( this.isInitialized ) {
- return;
- }
- this.isInitialized = true;
-
- if ( ! this.validateConfig() ) {
- return;
+ if ( ! validEnvs.includes( this.buttonConfig.environment ) ) {
+ return isInvalid(
+ 'Invalid environment:',
+ this.buttonConfig.environment
+ );
}
- if ( ! this.contextHandler.validateContext() ) {
- return;
+ // Preview buttons only need a valid environment.
+ if ( this.isPreview ) {
+ return true;
}
- this.googlePayConfig = config;
- this.transactionInfo = transactionInfo;
- this.allowedPaymentMethods = config.allowedPaymentMethods;
- this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
-
- this.initClient();
- this.initEventHandlers();
-
- this.paymentsClient
- .isReadyToPay(
- this.buildReadyToPayRequest(
- this.allowedPaymentMethods,
- config
- )
- )
- .then( ( response ) => {
- if ( response.result ) {
- if (
- ( this.context === 'checkout' ||
- this.context === 'pay-now' ) &&
- this.buttonConfig.is_wc_gateway_enabled === '1'
- ) {
- const wrapper = document.getElementById(
- 'ppc-button-ppcp-googlepay'
- );
-
- if ( wrapper ) {
- const { ppcpStyle, buttonStyle } =
- this.contextConfig();
-
- wrapper.classList.add(
- `ppcp-button-${ ppcpStyle.shape }`,
- 'ppcp-button-apm',
- 'ppcp-button-googlepay'
- );
-
- if ( ppcpStyle.height ) {
- wrapper.style.height = `${ ppcpStyle.height }px`;
- }
-
- this.addButtonCheckout(
- this.baseCardPaymentMethod,
- wrapper,
- buttonStyle
- );
-
- return;
- }
- }
-
- this.addButton( this.baseCardPaymentMethod );
- }
- } )
- .catch( function ( err ) {
- console.error( err );
- } );
- }
-
- reinit() {
if ( ! this.googlePayConfig ) {
- return;
+ return isInvalid(
+ 'No API configuration - missing configure() call?'
+ );
}
- this.isInitialized = false;
- this.init( this.googlePayConfig, this.transactionInfo );
- }
-
- validateConfig() {
- if (
- [ 'PRODUCTION', 'TEST' ].indexOf(
- this.buttonConfig.environment
- ) === -1
- ) {
- console.error(
- '[GooglePayButton] Invalid environment.',
- this.buttonConfig.environment
+ if ( ! this.transactionInfo ) {
+ return isInvalid(
+ 'No transactionInfo - missing configure() call?'
);
- return false;
}
- if ( ! this.contextHandler ) {
- console.error(
- '[GooglePayButton] Invalid context handler.',
- this.contextHandler
- );
- return false;
+ if ( ! typeof this.contextHandler?.validateContext() ) {
+ return isInvalid( 'Invalid context handler.', this.contextHandler );
}
return true;
}
/**
- * Returns configurations relative to this button context.
+ * Configures the button instance. Must be called before the initial `init()`.
+ *
+ * @param {Object} apiConfig - API configuration.
+ * @param {Object} transactionInfo - Transaction details; required before "init" call.
*/
- contextConfig() {
- const config = {
- wrapper: this.buttonConfig.button.wrapper,
- ppcpStyle: this.ppcpConfig.button.style,
- buttonStyle: this.buttonConfig.button.style,
- ppcpButtonWrapper: this.ppcpConfig.button.wrapper,
- };
+ configure( apiConfig, transactionInfo ) {
+ this.googlePayConfig = apiConfig;
+ this.#transactionInfo = transactionInfo;
- if ( this.context === 'mini-cart' ) {
- config.wrapper = this.buttonConfig.button.mini_cart_wrapper;
- config.ppcpStyle = this.ppcpConfig.button.mini_cart_style;
- config.buttonStyle = this.buttonConfig.button.mini_cart_style;
- config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper;
-
- // Handle incompatible types.
- if ( config.buttonStyle.type === 'buy' ) {
- config.buttonStyle.type = 'pay';
- }
- }
-
- if (
- [ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1
- ) {
- config.ppcpButtonWrapper =
- '#express-payment-method-ppcp-gateway-paypal';
- }
-
- return config;
+ this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods;
+ this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
}
- initClient() {
- const callbacks = {
- onPaymentAuthorized: this.onPaymentAuthorized.bind( this ),
- };
-
- if (
- this.buttonConfig.shipping.enabled &&
- this.contextHandler.shippingAllowed()
- ) {
- callbacks.onPaymentDataChanged =
- this.onPaymentDataChanged.bind( this );
+ init() {
+ // Use `reinit()` to force a full refresh of an initialized button.
+ if ( this.isInitialized ) {
+ return;
}
- this.paymentsClient = new google.payments.api.PaymentsClient( {
+ // Stop, if configuration is invalid.
+ if ( ! this.validateConfiguration() ) {
+ return;
+ }
+
+ super.init();
+ this.#paymentsClient = this.createPaymentsClient();
+
+ if ( ! this.isPresent ) {
+ this.log( 'Payment wrapper not found', this.wrapperId );
+ return;
+ }
+
+ if ( ! this.paymentsClient ) {
+ this.log( 'Could not initialize the payments client' );
+ return;
+ }
+
+ this.paymentsClient
+ .isReadyToPay(
+ this.buildReadyToPayRequest(
+ this.allowedPaymentMethods,
+ this.googlePayConfig
+ )
+ )
+ .then( ( response ) => {
+ this.log( 'PaymentsClient.isReadyToPay response:', response );
+ this.isEligible = !! response.result;
+ } )
+ .catch( ( err ) => {
+ this.error( err );
+ this.isEligible = false;
+ } );
+ }
+
+ reinit() {
+ // Missing (invalid) configuration indicates, that the first `init()` call did not happen yet.
+ if ( ! this.validateConfiguration( true ) ) {
+ return;
+ }
+
+ super.reinit();
+
+ this.init();
+ }
+
+ /**
+ * Provides an object with relevant paymentDataCallbacks for the current button instance.
+ *
+ * @return {Object} An object containing callbacks for the current scope & configuration.
+ */
+ preparePaymentDataCallbacks() {
+ const callbacks = {};
+
+ // We do not attach any callbacks to preview buttons.
+ if ( this.isPreview ) {
+ return callbacks;
+ }
+
+ callbacks.onPaymentAuthorized = this.onPaymentAuthorized;
+
+ if ( this.requiresShipping ) {
+ callbacks.onPaymentDataChanged = this.onPaymentDataChanged;
+ }
+
+ return callbacks;
+ }
+
+ createPaymentsClient() {
+ if ( ! this.googlePayApi ) {
+ return null;
+ }
+
+ const callbacks = this.preparePaymentDataCallbacks();
+
+ /**
+ * Consider providing merchant info here:
+ *
+ * @see https://developers.google.com/pay/api/web/reference/request-objects#PaymentOptions
+ */
+ return new this.googlePayApi.PaymentsClient( {
environment: this.buttonConfig.environment,
- // add merchant info maybe
paymentDataCallbacks: callbacks,
} );
}
- initEventHandlers() {
- const { wrapper, ppcpButtonWrapper } = this.contextConfig();
-
- if ( wrapper === ppcpButtonWrapper ) {
- throw new Error(
- `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
- );
- }
-
- const syncButtonVisibility = () => {
- const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
- setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
- setEnabled(
- wrapper,
- ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
- );
- };
-
- jQuery( document ).on(
- 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
- ( ev, data ) => {
- if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
- syncButtonVisibility();
- }
- }
- );
-
- syncButtonVisibility();
- }
-
buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) {
+ this.log( 'Ready To Pay request', baseRequest, allowedPaymentMethods );
+
return Object.assign( {}, baseRequest, {
allowedPaymentMethods,
} );
}
/**
- * Add a Google Pay purchase button
- * @param baseCardPaymentMethod
+ * Creates the payment button and calls `this.insertButton()` to make the button visible in the
+ * correct wrapper.
*/
- addButton( baseCardPaymentMethod ) {
- this.log( 'addButton', this.context );
+ addButton() {
+ if ( ! this.paymentsClient ) {
+ return;
+ }
- const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
+ const baseCardPaymentMethod = this.baseCardPaymentMethod;
+ const { color, type, language } = this.style;
- this.waitForWrapper( wrapper, () => {
- jQuery( wrapper ).addClass( 'ppcp-button-' + ppcpStyle.shape );
-
- if ( ppcpStyle.height ) {
- jQuery( wrapper ).css( 'height', `${ ppcpStyle.height }px` );
- }
-
- const button = this.paymentsClient.createButton( {
- onClick: this.onButtonClick.bind( this ),
- allowedPaymentMethods: [ baseCardPaymentMethod ],
- buttonColor: buttonStyle.color || 'black',
- buttonType: buttonStyle.type || 'pay',
- buttonLocale: buttonStyle.language || 'en',
- buttonSizeMode: 'fill',
- } );
-
- jQuery( wrapper ).append( button );
- } );
- }
-
- addButtonCheckout( baseCardPaymentMethod, wrapper, buttonStyle ) {
+ /**
+ * @see https://developers.google.com/pay/api/web/reference/client#createButton
+ */
const button = this.paymentsClient.createButton( {
- onClick: this.onButtonClick.bind( this ),
+ onClick: this.onButtonClick,
allowedPaymentMethods: [ baseCardPaymentMethod ],
- buttonColor: buttonStyle.color || 'black',
- buttonType: buttonStyle.type || 'pay',
- buttonLocale: buttonStyle.language || 'en',
+ buttonColor: color || 'black',
+ buttonType: type || 'pay',
+ buttonLocale: language || 'en',
buttonSizeMode: 'fill',
} );
- wrapper.appendChild( button );
- }
-
- waitForWrapper( selector, callback, delay = 100, timeout = 2000 ) {
- const startTime = Date.now();
- const interval = setInterval( () => {
- const el = document.querySelector( selector );
- const timeElapsed = Date.now() - startTime;
-
- if ( el ) {
- clearInterval( interval );
- callback( el );
- } else if ( timeElapsed > timeout ) {
- clearInterval( interval );
- }
- }, delay );
+ this.insertButton( button );
}
//------------------------
@@ -290,19 +383,49 @@ class GooglepayButton {
* Show Google Pay payment sheet when Google Pay payment button is clicked
*/
onButtonClick() {
- this.log( 'onButtonClick', this.context );
+ this.log( 'onButtonClick' );
- const paymentDataRequest = this.paymentDataRequest();
+ const initiatePaymentRequest = () => {
+ window.ppcpFundingSource = 'googlepay';
+ const paymentDataRequest = this.paymentDataRequest();
+ this.log(
+ 'onButtonClick: paymentDataRequest',
+ paymentDataRequest,
+ this.context
+ );
+ this.paymentsClient.loadPaymentData( paymentDataRequest );
+ };
- this.log(
- 'onButtonClick: paymentDataRequest',
- paymentDataRequest,
- this.context
- );
+ const validateForm = () => {
+ if ( 'function' !== typeof this.contextHandler.validateForm ) {
+ return Promise.resolve();
+ }
- window.ppcpFundingSource = 'googlepay'; // Do this on another place like on create order endpoint handler.
+ return this.contextHandler.validateForm().catch( ( error ) => {
+ this.error( 'Form validation failed:', error );
+ throw error;
+ } );
+ };
- this.paymentsClient.loadPaymentData( paymentDataRequest );
+ const getTransactionInfo = () => {
+ if ( 'function' !== typeof this.contextHandler.transactionInfo ) {
+ return Promise.resolve();
+ }
+
+ return this.contextHandler
+ .transactionInfo()
+ .then( ( transactionInfo ) => {
+ this.transactionInfo = transactionInfo;
+ } )
+ .catch( ( error ) => {
+ this.error( 'Failed to get transaction info:', error );
+ throw error;
+ } );
+ };
+
+ validateForm()
+ .then( getTransactionInfo )
+ .then( initiatePaymentRequest );
}
paymentDataRequest() {
@@ -318,10 +441,7 @@ class GooglepayButton {
paymentDataRequest.transactionInfo = this.transactionInfo;
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
- if (
- this.buttonConfig.shipping.enabled &&
- this.contextHandler.shippingAllowed()
- ) {
+ if ( this.requiresShipping ) {
paymentDataRequest.callbackIntents = [
'SHIPPING_ADDRESS',
'SHIPPING_OPTION',
@@ -350,8 +470,7 @@ class GooglepayButton {
}
onPaymentDataChanged( paymentData ) {
- this.log( 'onPaymentDataChanged', this.context );
- this.log( 'paymentData', paymentData );
+ this.log( 'onPaymentDataChanged', paymentData );
return new Promise( async ( resolve, reject ) => {
try {
@@ -396,7 +515,7 @@ class GooglepayButton {
resolve( paymentDataRequestUpdate );
} catch ( error ) {
- console.error( 'Error during onPaymentDataChanged:', error );
+ this.error( 'Error during onPaymentDataChanged:', error );
reject( error );
}
} );
@@ -424,18 +543,18 @@ class GooglepayButton {
//------------------------
onPaymentAuthorized( paymentData ) {
- this.log( 'onPaymentAuthorized', this.context );
+ this.log( 'onPaymentAuthorized' );
return this.processPayment( paymentData );
}
async processPayment( paymentData ) {
- this.log( 'processPayment', this.context );
+ this.log( 'processPayment' );
return new Promise( async ( resolve, reject ) => {
try {
const id = await this.contextHandler.createOrder();
- this.log( 'processPayment: createOrder', id, this.context );
+ this.log( 'processPayment: createOrder', id );
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
@@ -446,8 +565,7 @@ class GooglepayButton {
this.log(
'processPayment: confirmOrder',
- confirmOrderResponse,
- this.context
+ confirmOrderResponse
);
/** Capture the Order on the Server */
@@ -517,7 +635,7 @@ class GooglepayButton {
};
}
- this.log( 'processPaymentResponse', response, this.context );
+ this.log( 'processPaymentResponse', response );
return response;
}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js
index e267f1b8a..aaf85a6b0 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js
@@ -20,7 +20,7 @@ class GooglepayManager {
bootstrap.handler
);
- const button = new GooglepayButton(
+ const button = GooglepayButton.createButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
@@ -30,13 +30,19 @@ class GooglepayManager {
this.buttons.push( button );
+ const initButton = () => {
+ button.configure( this.googlePayConfig, this.transactionInfo );
+ button.init();
+ };
+
// Initialize button only if googlePayConfig and transactionInfo are already fetched.
if ( this.googlePayConfig && this.transactionInfo ) {
- button.init( this.googlePayConfig, this.transactionInfo );
+ initButton();
} else {
await this.init();
+
if ( this.googlePayConfig && this.transactionInfo ) {
- button.init( this.googlePayConfig, this.transactionInfo );
+ initButton();
}
}
} );
@@ -53,8 +59,18 @@ class GooglepayManager {
this.transactionInfo = await this.fetchTransactionInfo();
}
- for ( const button of this.buttons ) {
- button.init( this.googlePayConfig, this.transactionInfo );
+ if ( ! this.googlePayConfig ) {
+ console.error( 'No GooglePayConfig received during init' );
+ } else if ( ! this.transactionInfo ) {
+ console.error( 'No transactionInfo found during init' );
+ } else {
+ for ( const button of this.buttons ) {
+ button.configure(
+ this.googlePayConfig,
+ this.transactionInfo
+ );
+ button.init();
+ }
}
} catch ( error ) {
console.error( 'Error during initialization:', error );
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayPreviewButton.js b/modules/ppcp-googlepay/resources/js/GooglepayPreviewButton.js
new file mode 100644
index 000000000..6c3629a8a
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/GooglepayPreviewButton.js
@@ -0,0 +1,78 @@
+import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
+import ContextHandlerFactory from './Context/ContextHandlerFactory';
+import GooglepayButton from './GooglepayButton';
+
+/**
+ * A single GooglePay preview button instance.
+ */
+export default class GooglePayPreviewButton extends PreviewButton {
+ /**
+ * Instance of the preview button.
+ *
+ * @type {?PaymentButton}
+ */
+ #button = null;
+
+ constructor( args ) {
+ super( args );
+
+ this.selector = `${ args.selector }GooglePay`;
+ this.defaultAttributes = {
+ button: {
+ style: {
+ type: 'pay',
+ color: 'black',
+ language: 'en',
+ },
+ },
+ };
+ }
+
+ createNewWrapper() {
+ const element = super.createNewWrapper();
+ element.addClass( 'ppcp-button-googlepay' );
+
+ return element;
+ }
+
+ createButton( buttonConfig ) {
+ const contextHandler = ContextHandlerFactory.create(
+ 'preview',
+ buttonConfig,
+ this.ppcpConfig,
+ null
+ );
+
+ if ( ! this.#button ) {
+ /* Intentionally using `new` keyword, instead of the `.createButton()` factory,
+ * as the factory is designed to only create a single button per context, while a single
+ * page can contain multiple instances of a preview button.
+ */
+ this.#button = new GooglepayButton(
+ 'preview',
+ null,
+ buttonConfig,
+ this.ppcpConfig,
+ contextHandler
+ );
+ }
+
+ this.#button.configure( this.apiConfig, null );
+ this.#button.applyButtonStyles( buttonConfig, this.ppcpConfig );
+ this.#button.reinit();
+ }
+
+ /**
+ * Merge form details into the config object for preview.
+ * Mutates the previewConfig object; no return value.
+ *
+ * @param {Object} buttonConfig
+ * @param {Object} ppcpConfig
+ */
+ dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
+ // Merge the current form-values into the preview-button configuration.
+ if ( ppcpConfig.button && buttonConfig.button ) {
+ Object.assign( buttonConfig.button.style, ppcpConfig.button.style );
+ }
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js
index b9f7b3c87..7b5342078 100644
--- a/modules/ppcp-googlepay/resources/js/boot-admin.js
+++ b/modules/ppcp-googlepay/resources/js/boot-admin.js
@@ -1,7 +1,5 @@
-import GooglepayButton from './GooglepayButton';
-import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
-import ContextHandlerFactory from './Context/ContextHandlerFactory';
+import GooglePayPreviewButton from './GooglepayPreviewButton';
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
@@ -33,7 +31,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager {
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
- * @return {Promise<{}>}
+ * @return {Promise<{}>} Promise that resolves when API configuration is available.
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Googlepay()?.config;
@@ -59,7 +57,7 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager {
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
- * @return {GooglePayPreviewButton}
+ * @return {GooglePayPreviewButton} The new preview button instance.
*/
createButtonInstance( wrapperId ) {
return new GooglePayPreviewButton( {
@@ -69,64 +67,5 @@ class GooglePayPreviewButtonManager extends PreviewButtonManager {
}
}
-/**
- * A single GooglePay preview button instance.
- */
-class GooglePayPreviewButton extends PreviewButton {
- constructor( args ) {
- super( args );
-
- this.selector = `${ args.selector }GooglePay`;
- this.defaultAttributes = {
- button: {
- style: {
- type: 'pay',
- color: 'black',
- language: 'en',
- },
- },
- };
- }
-
- createNewWrapper() {
- const element = super.createNewWrapper();
- element.addClass( 'ppcp-button-googlepay' );
-
- return element;
- }
-
- createButton( buttonConfig ) {
- const contextHandler = ContextHandlerFactory.create(
- 'preview',
- buttonConfig,
- this.ppcpConfig,
- null
- );
-
- const button = new GooglepayButton(
- 'preview',
- null,
- buttonConfig,
- this.ppcpConfig,
- contextHandler
- );
-
- button.init( this.apiConfig, null );
- }
-
- /**
- * Merge form details into the config object for preview.
- * Mutates the previewConfig object; no return value.
- * @param buttonConfig
- * @param ppcpConfig
- */
- dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
- // Merge the current form-values into the preview-button configuration.
- if ( ppcpConfig.button && buttonConfig.button ) {
- Object.assign( buttonConfig.button.style, ppcpConfig.button.style );
- }
- }
-}
-
// Initialize the preview button manager.
buttonManager();
diff --git a/modules/ppcp-googlepay/services.php b/modules/ppcp-googlepay/services.php
index 11cbd740b..110b56937 100644
--- a/modules/ppcp-googlepay/services.php
+++ b/modules/ppcp-googlepay/services.php
@@ -25,23 +25,24 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// If GooglePay can be configured.
- 'googlepay.eligible' => static function ( ContainerInterface $container ): bool {
+ 'googlepay.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'googlepay.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
- return $apm_applies->for_country_currency();
+ return $apm_applies->for_country() && $apm_applies->for_currency();
},
- 'googlepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
+ 'googlepay.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
- $container->get( 'googlepay.supported-country-currency-matrix' ),
+ $container->get( 'googlepay.supported-countries' ),
+ $container->get( 'googlepay.supported-currencies' ),
$container->get( 'api.shop.currency' ),
$container->get( 'api.shop.country' )
);
},
// If GooglePay is configured and onboarded.
- 'googlepay.available' => static function ( ContainerInterface $container ): bool {
+ 'googlepay.available' => static function ( ContainerInterface $container ): bool {
if ( apply_filters( 'woocommerce_paypal_payments_googlepay_validate_product_status', true ) ) {
$status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $status instanceof ApmProductStatus );
@@ -54,14 +55,14 @@ return array(
},
// We assume it's a referral if we can check product status without API request failures.
- 'googlepay.is_referral' => static function ( ContainerInterface $container ): bool {
+ 'googlepay.is_referral' => static function ( ContainerInterface $container ): bool {
$status = $container->get( 'googlepay.helpers.apm-product-status' );
assert( $status instanceof ApmProductStatus );
return ! $status->has_request_failure();
},
- 'googlepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
+ 'googlepay.availability_notice' => static function ( ContainerInterface $container ): AvailabilityNotice {
return new AvailabilityNotice(
$container->get( 'googlepay.helpers.apm-product-status' ),
$container->get( 'wcgateway.is-wc-gateways-list-page' ),
@@ -69,7 +70,7 @@ return array(
);
},
- 'googlepay.helpers.apm-product-status' => SingletonDecorator::make(
+ 'googlepay.helpers.apm-product-status' => SingletonDecorator::make(
static function( ContainerInterface $container ): ApmProductStatus {
return new ApmProductStatus(
$container->get( 'wcgateway.settings' ),
@@ -81,772 +82,93 @@ return array(
),
/**
- * The matrix which countries and currency combinations can be used for GooglePay.
+ * The list of which countries can be used for GooglePay.
*/
- 'googlepay.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
+ 'googlepay.supported-countries' => static function ( ContainerInterface $container ) : array {
/**
- * Returns which countries and currency combinations can be used for GooglePay.
+ * Returns which countries can be used for GooglePay.
*/
return apply_filters(
- 'woocommerce_paypal_payments_googlepay_supported_country_currency_matrix',
+ 'woocommerce_paypal_payments_googlepay_supported_countries',
+ // phpcs:disable Squiz.Commenting.InlineComment
array(
- 'AU' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'AT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'BE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'BG' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'CA' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'CY' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'CZ' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'DK' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'EE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'FI' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'FR' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'DE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'GR' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'HU' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'IE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'IT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LV' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LI' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'LU' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'MT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'NO' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'NL' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'PL' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'PT' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'RO' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'SK' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'SI' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'ES' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'SE' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'GB' => array(
- 'AUD',
- 'BRL',
- 'CAD',
- 'CHF',
- 'CZK',
- 'DKK',
- 'EUR',
- 'GBP',
- 'HKD',
- 'HUF',
- 'ILS',
- 'JPY',
- 'MXN',
- 'NOK',
- 'NZD',
- 'PHP',
- 'PLN',
- 'SEK',
- 'SGD',
- 'THB',
- 'TWD',
- 'USD',
- ),
- 'US' => array(
- 'AUD',
- 'CAD',
- 'EUR',
- 'GBP',
- 'JPY',
- 'USD',
- ),
+ 'AU', // Australia
+ 'AT', // Austria
+ 'BE', // Belgium
+ 'BG', // Bulgaria
+ 'CA', // Canada
+ 'CN', // China
+ 'CY', // Cyprus
+ 'CZ', // Czech Republic
+ 'DK', // Denmark
+ 'EE', // Estonia
+ 'FI', // Finland
+ 'FR', // France
+ 'DE', // Germany
+ 'GR', // Greece
+ 'HU', // Hungary
+ 'IE', // Ireland
+ 'IT', // Italy
+ 'LV', // Latvia
+ 'LI', // Liechtenstein
+ 'LT', // Lithuania
+ 'LU', // Luxembourg
+ 'MT', // Malta
+ 'NL', // Netherlands
+ 'NO', // Norway
+ 'PL', // Poland
+ 'PT', // Portugal
+ 'RO', // Romania
+ 'SK', // Slovakia
+ 'SI', // Slovenia
+ 'ES', // Spain
+ 'SE', // Sweden
+ 'US', // United States
+ 'GB', // United Kingdom
)
+ // phpcs:enable Squiz.Commenting.InlineComment
);
},
- 'googlepay.button' => static function ( ContainerInterface $container ): ButtonInterface {
+ /**
+ * The list of which currencies can be used for GooglePay.
+ */
+ 'googlepay.supported-currencies' => static function ( ContainerInterface $container ) : array {
+ /**
+ * Returns which currencies can be used for GooglePay.
+ */
+ return apply_filters(
+ 'woocommerce_paypal_payments_googlepay_supported_currencies',
+ // phpcs:disable Squiz.Commenting.InlineComment
+ array(
+ 'AUD', // Australian Dollar
+ 'BRL', // Brazilian Real
+ 'CAD', // Canadian Dollar
+ 'CHF', // Swiss Franc
+ 'CZK', // Czech Koruna
+ 'DKK', // Danish Krone
+ 'EUR', // Euro
+ 'GBP', // British Pound Sterling
+ 'HKD', // Hong Kong Dollar
+ 'HUF', // Hungarian Forint
+ 'ILS', // Israeli New Shekel
+ 'JPY', // Japanese Yen
+ 'MXN', // Mexican Peso
+ 'NOK', // Norwegian Krone
+ 'NZD', // New Zealand Dollar
+ 'PHP', // Philippine Peso
+ 'PLN', // Polish Zloty
+ 'SEK', // Swedish Krona
+ 'SGD', // Singapore Dollar
+ 'THB', // Thai Baht
+ 'TWD', // New Taiwan Dollar
+ 'USD', // United States Dollar
+ )
+ // phpcs:enable Squiz.Commenting.InlineComment
+ );
+ },
+
+ 'googlepay.button' => static function ( ContainerInterface $container ): ButtonInterface {
return new Button(
$container->get( 'googlepay.url' ),
$container->get( 'googlepay.sdk_url' ),
@@ -860,7 +182,7 @@ return array(
);
},
- 'googlepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
+ 'googlepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface {
return new BlocksPaymentMethod(
'ppcp-googlepay',
$container->get( 'googlepay.url' ),
@@ -870,7 +192,7 @@ return array(
);
},
- 'googlepay.url' => static function ( ContainerInterface $container ): string {
+ 'googlepay.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
@@ -881,26 +203,26 @@ return array(
);
},
- 'googlepay.sdk_url' => static function ( ContainerInterface $container ): string {
+ 'googlepay.sdk_url' => static function ( ContainerInterface $container ): string {
return 'https://pay.google.com/gp/p/js/pay.js';
},
- 'googlepay.endpoint.update-payment-data' => static function ( ContainerInterface $container ): UpdatePaymentDataEndpoint {
+ 'googlepay.endpoint.update-payment-data' => static function ( ContainerInterface $container ): UpdatePaymentDataEndpoint {
return new UpdatePaymentDataEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
- 'googlepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
+ 'googlepay.enable-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY';
},
- 'googlepay.enable-url-live' => static function ( ContainerInterface $container ): string {
+ 'googlepay.enable-url-live' => static function ( ContainerInterface $container ): string {
return 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY';
},
- 'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
+ 'googlepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
@@ -938,7 +260,7 @@ return array(
esc_html( $button_text )
);
},
- 'googlepay.wc-gateway' => static function ( ContainerInterface $container ): GooglePayGateway {
+ 'googlepay.wc-gateway' => static function ( ContainerInterface $container ): GooglePayGateway {
return new GooglePayGateway(
$container->get( 'wcgateway.order-processor' ),
$container->get( 'api.factory.paypal-checkout-url' ),
diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php
index bd6faea79..575def21f 100644
--- a/modules/ppcp-googlepay/src/Assets/Button.php
+++ b/modules/ppcp-googlepay/src/Assets/Button.php
@@ -290,6 +290,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
+ $this->hide_gateway_until_eligible();
},
21
);
@@ -303,6 +304,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
+ $this->hide_gateway_until_eligible();
},
21
);
@@ -335,6 +337,23 @@ class Button implements ButtonInterface {
+
+ allowed_country_currency_matrix = $allowed_country_currency_matrix;
- $this->currency = $currency;
- $this->country = $country;
+ $this->allowed_countries = $allowed_countries;
+ $this->allowed_currencies = $allowed_currencies;
+ $this->currency = $currency;
+ $this->country = $country;
}
/**
- * Returns whether GooglePay can be used in the current country and the current currency used.
+ * Returns whether GooglePay can be used in the current country used.
*
* @return bool
*/
- public function for_country_currency(): bool {
- if ( ! in_array( $this->country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
- return false;
- }
- return in_array( $this->currency, $this->allowed_country_currency_matrix[ $this->country ], true );
+ public function for_country(): bool {
+ return in_array( $this->country, $this->allowed_countries, true );
+ }
+
+ /**
+ * Returns whether GooglePay can be used in the current currency used.
+ *
+ * @return bool
+ */
+ public function for_currency(): bool {
+ return in_array( $this->currency, $this->allowed_currencies, true );
}
}
diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php
index b76519f62..e7f493999 100644
--- a/modules/ppcp-onboarding/services.php
+++ b/modules/ppcp-onboarding/services.php
@@ -189,9 +189,8 @@ return array(
$login_seller_sandbox = $container->get( 'api.endpoint.login-seller-sandbox' );
$partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' );
$settings = $container->get( 'wcgateway.settings' );
- $logger = $container->get( 'woocommerce.logger.woocommerce' );
-
$cache = new Cache( 'ppcp-paypal-bearer' );
+ $logger = $container->get( 'woocommerce.logger.woocommerce' );
return new LoginSellerEndpoint(
$request_data,
$login_seller_production,
@@ -199,7 +198,8 @@ return array(
$partner_referrals_data,
$settings,
$cache,
- $logger
+ $logger,
+ new Cache( 'ppcp-client-credentials-cache' )
);
},
'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint {
diff --git a/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php b/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php
index 20ab7b95a..e1d23234f 100644
--- a/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php
+++ b/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php
@@ -12,6 +12,8 @@ namespace WooCommerce\PayPalCommerce\Onboarding\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
+use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
+use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
@@ -76,6 +78,13 @@ class LoginSellerEndpoint implements EndpointInterface {
*/
protected $logger;
+ /**
+ * The client credentials cache.
+ *
+ * @var Cache
+ */
+ private $client_credentials_cache;
+
/**
* LoginSellerEndpoint constructor.
*
@@ -86,6 +95,7 @@ class LoginSellerEndpoint implements EndpointInterface {
* @param Settings $settings The Settings.
* @param Cache $cache The Cache.
* @param LoggerInterface $logger The logger.
+ * @param Cache $client_credentials_cache The client credentials cache.
*/
public function __construct(
RequestData $request_data,
@@ -94,16 +104,18 @@ class LoginSellerEndpoint implements EndpointInterface {
PartnerReferralsData $partner_referrals_data,
Settings $settings,
Cache $cache,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ Cache $client_credentials_cache
) {
- $this->request_data = $request_data;
- $this->login_seller_production = $login_seller_production;
- $this->login_seller_sandbox = $login_seller_sandbox;
- $this->partner_referrals_data = $partner_referrals_data;
- $this->settings = $settings;
- $this->cache = $cache;
- $this->logger = $logger;
+ $this->request_data = $request_data;
+ $this->login_seller_production = $login_seller_production;
+ $this->login_seller_sandbox = $login_seller_sandbox;
+ $this->partner_referrals_data = $partner_referrals_data;
+ $this->settings = $settings;
+ $this->cache = $cache;
+ $this->logger = $logger;
+ $this->client_credentials_cache = $client_credentials_cache;
}
/**
@@ -175,6 +187,9 @@ class LoginSellerEndpoint implements EndpointInterface {
if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) {
$this->cache->delete( PayPalBearer::CACHE_KEY );
}
+ if ( $this->client_credentials_cache->has( SdkClientToken::CACHE_KEY ) ) {
+ $this->client_credentials_cache->delete( SdkClientToken::CACHE_KEY );
+ }
wp_schedule_single_event(
time() + 5,
diff --git a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php
index db7335cf5..f82095bf4 100644
--- a/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php
+++ b/modules/ppcp-save-payment-methods/src/SavePaymentMethodsModule.php
@@ -31,6 +31,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Endpoint\SubscriptionChangePaymentMethod;
+use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class SavePaymentMethodsModule
@@ -84,7 +85,9 @@ class SavePaymentMethodsModule implements ModuleInterface {
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function( array $localized_script_data ) use ( $c ) {
- if ( ! is_user_logged_in() ) {
+ $subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
+ assert( $subscriptions_helper instanceof SubscriptionHelper );
+ if ( ! is_user_logged_in() && ! $subscriptions_helper->cart_contains_subscription() ) {
return $localized_script_data;
}
diff --git a/modules/ppcp-wc-gateway/resources/css/common.scss b/modules/ppcp-wc-gateway/resources/css/common.scss
index b694547c8..ac4bcab32 100644
--- a/modules/ppcp-wc-gateway/resources/css/common.scss
+++ b/modules/ppcp-wc-gateway/resources/css/common.scss
@@ -133,6 +133,11 @@ $background-ident-color: #fbfbfb;
}
}
+ .ppcp-notice-list {
+ list-style-type: disc;
+ padding-left: 20px;
+ }
+
th, td {
border-top: 1px solid $border-color;
}
diff --git a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
new file mode 100644
index 000000000..c76aa8960
--- /dev/null
+++ b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
@@ -0,0 +1,58 @@
+/**
+ * Helper component to log debug details to the browser console.
+ *
+ * A utility class that is used by payment buttons on the front-end, like the GooglePayButton.
+ */
+export default class ConsoleLogger {
+ /**
+ * The prefix to display before every log output.
+ *
+ * @type {string}
+ */
+ #prefix = '';
+
+ /**
+ * Whether logging is enabled, disabled by default.
+ *
+ * @type {boolean}
+ */
+ #enabled = false;
+
+ constructor( ...prefixes ) {
+ if ( prefixes.length ) {
+ this.#prefix = `[${ prefixes.join( ' | ' ) }]`;
+ }
+ }
+
+ /**
+ * Enable or disable logging. Only impacts `log()` output.
+ *
+ * @param {boolean} state True to enable log output.
+ */
+ set enabled( state ) {
+ this.#enabled = state;
+ }
+
+ /**
+ * Output log-level details to the browser console, if logging is enabled.
+ *
+ * @param {...any} args - All provided values are output to the browser console.
+ */
+ log( ...args ) {
+ if ( this.#enabled ) {
+ // eslint-disable-next-line
+ console.log( this.#prefix, ...args );
+ }
+ }
+
+ /**
+ * Generate an error message in the browser's console.
+ *
+ * Error messages are always output, even when logging is disabled.
+ *
+ * @param {...any} args - All provided values are output to the browser console.
+ */
+ error( ...args ) {
+ console.error( this.#prefix, ...args );
+ }
+}
diff --git a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js
index 30b71f511..d9c4f8264 100644
--- a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js
+++ b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js
@@ -1,9 +1,11 @@
+/* global jQuery */
+
/**
* Returns a Map with all input fields that are relevant to render the preview of the
* given payment button.
*
* @param {string} apmName - Value of the custom attribute `data-ppcp-apm-name`.
- * @return {Map}
+ * @return {Map} List of input elements found on the current admin page.
*/
export function getButtonFormFields( apmName ) {
const inputFields = document.querySelectorAll(
@@ -28,9 +30,9 @@ export function getButtonFormFields( apmName ) {
/**
* Returns a function that triggers an update of the specified preview button, when invoked.
-
+ *
* @param {string} apmName
- * @return {((object) => void)}
+ * @return {((object) => void)} Trigger-function; updates preview buttons when invoked.
*/
export function buttonRefreshTriggerFactory( apmName ) {
const eventName = `ppcp_paypal_render_preview_${ apmName }`;
@@ -44,7 +46,7 @@ export function buttonRefreshTriggerFactory( apmName ) {
* Returns a function that gets the current form values of the specified preview button.
*
* @param {string} apmName
- * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}}
+ * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}} Getter-function; returns preview config details when invoked.
*/
export function buttonSettingsGetterFactory( apmName ) {
const fields = getButtonFormFields( apmName );
diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php
index 92acc6b0c..e92b7ee6a 100644
--- a/modules/ppcp-wc-gateway/services.php
+++ b/modules/ppcp-wc-gateway/services.php
@@ -369,7 +369,8 @@ return array(
$container->get( 'api.partner_merchant_id-production' ),
$container->get( 'api.partner_merchant_id-sandbox' ),
$container->get( 'api.endpoint.billing-agreements' ),
- $logger
+ $logger,
+ new Cache( 'ppcp-client-credentials-cache' )
);
},
'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor {
@@ -1413,10 +1414,10 @@ return array(
return $label;
},
'wcgateway.enable-dcc-url-sandbox' => static function ( ContainerInterface $container ): string {
- return 'https://www.sandbox.paypal.com/bizsignup/entry/product/ppcp';
+ return 'https://www.sandbox.paypal.com/bizsignup/entry?product=ppcp';
},
'wcgateway.enable-dcc-url-live' => static function ( ContainerInterface $container ): string {
- return 'https://www.paypal.com/bizsignup/entry/product/ppcp';
+ return 'https://www.paypal.com/bizsignup/entry?product=ppcp';
},
'wcgateway.enable-pui-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/entry?country.x=DE&product=payment_methods&capabilities=PAY_UPON_INVOICE';
diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
index 9986d5edc..6c01830d6 100644
--- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
+++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
@@ -12,6 +12,8 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
+use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
+use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
@@ -164,6 +166,13 @@ class SettingsListener {
*/
private $logger;
+ /**
+ * The client credentials cache.
+ *
+ * @var Cache
+ */
+ private $client_credentials_cache;
+
/**
* SettingsListener constructor.
*
@@ -183,6 +192,7 @@ class SettingsListener {
* @param string $partner_merchant_id_sandbox Partner merchant ID sandbox.
* @param BillingAgreementsEndpoint $billing_agreements_endpoint Billing Agreements endpoint.
* @param ?LoggerInterface $logger The logger.
+ * @param Cache $client_credentials_cache The client credentials cache.
*/
public function __construct(
Settings $settings,
@@ -200,7 +210,8 @@ class SettingsListener {
string $partner_merchant_id_production,
string $partner_merchant_id_sandbox,
BillingAgreementsEndpoint $billing_agreements_endpoint,
- LoggerInterface $logger = null
+ LoggerInterface $logger = null,
+ Cache $client_credentials_cache
) {
$this->settings = $settings;
@@ -219,6 +230,7 @@ class SettingsListener {
$this->partner_merchant_id_sandbox = $partner_merchant_id_sandbox;
$this->billing_agreements_endpoint = $billing_agreements_endpoint;
$this->logger = $logger ?: new NullLogger();
+ $this->client_credentials_cache = $client_credentials_cache;
}
/**
@@ -490,6 +502,9 @@ class SettingsListener {
if ( $this->cache->has( PayPalBearer::CACHE_KEY ) ) {
$this->cache->delete( PayPalBearer::CACHE_KEY );
}
+ if ( $this->client_credentials_cache->has( SdkClientToken::CACHE_KEY ) ) {
+ $this->client_credentials_cache->delete( SdkClientToken::CACHE_KEY );
+ }
if ( $this->pui_status_cache->has( PayUponInvoiceProductStatus::PUI_STATUS_CACHE_KEY ) ) {
$this->pui_status_cache->delete( PayUponInvoiceProductStatus::PUI_STATUS_CACHE_KEY );
diff --git a/package.json b/package.json
index 61c625ca9..72aad99db 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
- "version": "2.8.2",
+ "version": "2.8.3",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",
diff --git a/readme.txt b/readme.txt
index e4404042e..1aed175cb 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, checkout, cart, pay later, apple
Requires at least: 5.3
Tested up to: 6.6
Requires PHP: 7.2
-Stable tag: 2.8.2
+Stable tag: 2.8.3
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -179,6 +179,23 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
+= 2.8.3 - 2024-08-12 =
+* Fix - Google Pay: Prevent field validation from being triggered on checkout page load #2474
+* Fix - Do not add tax info into order meta during order creation #2471
+* Fix - PayPal declares subscription support when for Subscription mode is set Disable PayPal for subscription #2425
+* Fix - PayPal js files loaded on non PayPal pages #2411
+* Fix - Google Pay: Fix the incorrect popup triggering #2414
+* Fix - Add tax configurator when programmatically creating WC orders #2431
+* Fix - Shipping callback compatibility with WC Name Your Price plugin #2402
+* Fix - Uncaught Error: Cannot use object of type ...\Settings as array in .../AbstractPaymentMethodType.php (3253) #2334
+* Fix - Prevent displaying smart button multiple times on variable product page #2420
+* Fix - Prevent enabling Standard Card Button when ACDC is enabled #2404
+* Fix - Use client credentials for user tokens #2491
+* Fix - Apple Pay: Fix the shipping callback #2492
+* Enhancement - Separate Google Pay button for Classic Checkout #2430
+* Enhancement - Add Apple Pay and Google Pay support for China, simplify country-currency matrix #2468
+* Enhancement - Add AMEX support for Advanced Card Processing in China #2469
+
= 2.8.2 - 2024-07-22 =
* Fix - Sold individually checkbox automatically disabled after adding product to the cart more than once #2415
* Fix - All products "Sold individually" when PayPal Subscriptions selected as Subscriptions Mode #2400
diff --git a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php
index c34146f3e..efa1b7de6 100644
--- a/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php
+++ b/tests/PHPUnit/WcGateway/Settings/SettingsListenerTest.php
@@ -43,6 +43,7 @@ class SettingsListenerTest extends ModularTestCase
$billing_agreement_endpoint = Mockery::mock(BillingAgreementsEndpoint::class);
$subscription_helper = Mockery::mock(SubscriptionHelper::class);
$logger = Mockery::mock(LoggerInterface::class);
+ $client_credentials_cache = Mockery::mock(Cache::class);
$testee = new SettingsListener(
$settings,
@@ -60,7 +61,8 @@ class SettingsListenerTest extends ModularTestCase
'',
'',
$billing_agreement_endpoint,
- $logger
+ $logger,
+ $client_credentials_cache
);
$_GET['section'] = PayPalGateway::ID;
@@ -94,6 +96,9 @@ class SettingsListenerTest extends ModularTestCase
->andReturn(false);
$dcc_status_cache->shouldReceive('has')
->andReturn(false);
+ $client_credentials_cache->shouldReceive('has')->andReturn(true);
+ $client_credentials_cache->shouldReceive('delete');
+
$testee->listen();
}
diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php
index 2cdf4f8ed..fcf66ae12 100644
--- a/woocommerce-paypal-payments.php
+++ b/woocommerce-paypal-payments.php
@@ -3,7 +3,7 @@
* 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.8.2
+ * Version: 2.8.3
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
@@ -26,7 +26,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-07-17' );
+define( 'PAYPAL_INTEGRATION_DATE', '2024-08-07' );
define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );