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 e980e51a0..8d811ad35 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,23 @@ *** Changelog *** -= 2.8.2 - xxxx-xx-xx = += 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 * Fix - W3 Total Cache: Remove type from file parameter as sometimes null gets passed causing errors #2403 diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index e0293b71b..611d01693 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; @@ -705,6 +706,7 @@ return array( 'CN' => array( 'mastercard' => array(), 'visa' => array(), + 'amex' => array(), ), 'CY' => $mastercard_visa_amex, 'CZ' => $mastercard_visa_amex, @@ -811,18 +813,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 2bcac16fc..281a08ae1 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -644,10 +644,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, products: JSON.stringify( this.products ), caller_page: 'productDetail', @@ -662,10 +664,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, }; @@ -948,6 +952,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 f763ac5d9..00d7bd463 100644 --- a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js @@ -1,6 +1,5 @@ import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler'; import CartActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler'; -import { isPayPalSubscription } from '../../../../ppcp-blocks/resources/js/Helper/Subscription'; class BaseHandler { constructor( buttonConfig, ppcpConfig ) { @@ -24,7 +23,7 @@ class BaseHandler { } shippingAllowed() { - return this.buttonConfig.product.needsShipping; + return this.buttonConfig.product.needShipping; } transactionInfo() { @@ -76,13 +75,6 @@ class BaseHandler { document.querySelector( '.woocommerce-notices-wrapper' ) ); } - - errorHandler() { - return new ErrorHandler( - this.ppcpConfig.labels.error.generic, - document.querySelector( '.woocommerce-notices-wrapper' ) - ); - } } export default BaseHandler; diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index 76f3fa9e9..e4a3297dc 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,95 +165,103 @@ return array( $container->get( 'blocks.method' ) ); }, - /** - * The matrix which countries and currency combinations can be used for ApplePay. - */ - 'applepay.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array { - $default_currencies = array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ); + /** + * The list of which countries can be used for ApplePay. + */ + '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' => $default_currencies, - 'AT' => $default_currencies, - 'BE' => $default_currencies, - 'BG' => $default_currencies, - 'CA' => $default_currencies, - 'CY' => $default_currencies, - 'CZ' => $default_currencies, - 'DK' => $default_currencies, - 'EE' => $default_currencies, - 'FI' => $default_currencies, - 'FR' => $default_currencies, - 'DE' => $default_currencies, - 'GR' => $default_currencies, - 'HK' => $default_currencies, - 'HU' => $default_currencies, - 'IE' => $default_currencies, - 'IT' => $default_currencies, - 'LV' => $default_currencies, - 'LI' => $default_currencies, - 'LT' => $default_currencies, - 'LU' => $default_currencies, - 'MT' => $default_currencies, - 'NO' => $default_currencies, - 'NL' => $default_currencies, - 'PL' => $default_currencies, - 'PT' => $default_currencies, - 'RO' => $default_currencies, - 'SG' => $default_currencies, - 'SK' => $default_currencies, - 'SI' => $default_currencies, - 'ES' => $default_currencies, - 'SE' => $default_currencies, - 'GB' => $default_currencies, - '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 '

' . $notice_content . '

'; + '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 '

' . $notice_content . '

'; }, + '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 ? '

' . $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 ? '

' . $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 '

    ' . $notice_content . '

    '; + } + + /** + * 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/src/AdvancedCardPaymentMethod.php b/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php index 2c6fd60f4..df113f8f0 100644 --- a/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php +++ b/modules/ppcp-blocks/src/AdvancedCardPaymentMethod.php @@ -52,7 +52,7 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType { * * @var Settings */ - protected $settings; + protected $plugin_settings; /** * AdvancedCardPaymentMethod constructor. @@ -70,12 +70,12 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType { $smart_button, Settings $settings ) { - $this->name = CreditCardGateway::ID; - $this->module_url = $module_url; - $this->version = $version; - $this->gateway = $gateway; - $this->smart_button = $smart_button; - $this->settings = $settings; + $this->name = CreditCardGateway::ID; + $this->module_url = $module_url; + $this->version = $version; + $this->gateway = $gateway; + $this->smart_button = $smart_button; + $this->plugin_settings = $settings; } /** @@ -118,8 +118,8 @@ class AdvancedCardPaymentMethod extends AbstractPaymentMethodType { 'scriptData' => $script_data, 'supports' => $this->gateway->supports, 'save_card_text' => esc_html__( 'Save your card', 'woocommerce-paypal-payments' ), - 'is_vaulting_enabled' => $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ), - 'card_icons' => $this->settings->has( 'card_icons' ) ? (array) $this->settings->get( 'card_icons' ) : array(), + 'is_vaulting_enabled' => $this->plugin_settings->has( 'vault_enabled_dcc' ) && $this->plugin_settings->get( 'vault_enabled_dcc' ), + 'card_icons' => $this->plugin_settings->has( 'card_icons' ) ? (array) $this->plugin_settings->get( 'card_icons' ) : array(), ); } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 848285815..33d1ecfd3 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -181,9 +181,14 @@ class CheckoutBootstap { const isSeparateButtonGateway = [ PaymentMethods.CARD_BUTTON ].includes( currentPaymentMethod ); + const isGooglePayMethod = + currentPaymentMethod === PaymentMethods.GOOGLEPAY; const isSavedCard = isCard && isSavedCardSelected(); const isNotOurGateway = - ! isPaypal && ! isCard && ! isSeparateButtonGateway; + ! isPaypal && + ! isCard && + ! isSeparateButtonGateway && + ! isGooglePayMethod; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== ''; @@ -227,6 +232,8 @@ class CheckoutBootstap { } } + setVisible( '#ppc-button-ppcp-googlepay', isGooglePayMethod ); + jQuery( document.body ).trigger( 'ppcp_checkout_rendered' ); } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index b05219957..59e3a0d2c 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -7,6 +7,7 @@ import { getPlanIdFromVariation } from '../Helper/Subscriptions'; import SimulateCart from '../Helper/SimulateCart'; import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils'; import merge from 'deepmerge'; +import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce'; class SingleProductBootstap { constructor( gateway, renderer, errorHandler ) { @@ -20,9 +21,13 @@ class SingleProductBootstap { // Prevent simulate cart being called too many times in a burst. this.simulateCartThrottled = throttle( - this.simulateCart, + this.simulateCart.bind( this ), this.gateway.simulate_cart.throttling || 5000 ); + this.debouncedHandleChange = debounce( + this.handleChange.bind( this ), + 100 + ); this.renderer.onButtonsInit( this.gateway.button.wrapper, @@ -74,7 +79,7 @@ class SingleProductBootstap { } jQuery( document ).on( 'change', this.formSelector, () => { - this.handleChange(); + this.debouncedHandleChange(); } ); this.mutationObserver.observe( form, { childList: true, diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js index 0ea05f255..3e284c8ef 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js +++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js @@ -3,6 +3,7 @@ export const PaymentMethods = { CARDS: 'ppcp-credit-card-gateway', OXXO: 'ppcp-oxxo-gateway', CARD_BUTTON: 'ppcp-card-button-gateway', + GOOGLEPAY: 'ppcp-googlepay', }; export const ORDER_BUTTON_SELECTOR = '#place_order'; 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/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index 625157a05..888a567c5 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -14,8 +14,10 @@ use WC_Cart; use WC_Order; use WC_Order_Item_Product; use WC_Order_Item_Shipping; +use WC_Product; use WC_Subscription; use WC_Subscriptions_Product; +use WC_Tax; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping; @@ -106,6 +108,7 @@ class WooCommerceOrderCreator { * @param Payer|null $payer The payer. * @param Shipping|null $shipping The shipping. * @return void + * @psalm-suppress InvalidScalarArgument */ protected function configure_line_items( WC_Order $wc_order, WC_Cart $wc_cart, ?Payer $payer, ?Shipping $shipping ): void { $cart_contents = $wc_cart->get_cart(); @@ -130,18 +133,21 @@ class WooCommerceOrderCreator { return; } - $total = $product->get_price() * $quantity; + $subtotal = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $subtotal = apply_filters( 'woocommerce_paypal_payments_shipping_callback_cart_line_item_total', $subtotal, $cart_item ); $item->set_name( $product->get_name() ); - $item->set_subtotal( $total ); - $item->set_total( $total ); + $item->set_subtotal( $subtotal ); + $item->set_total( $subtotal ); + + $this->configure_taxes( $product, $item, $subtotal ); $product_id = $product->get_id(); if ( $this->is_subscription( $product_id ) ) { $subscription = $this->create_subscription( $wc_order, $product_id ); $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product ); - $subscription_total = $total + $sign_up_fee; + $subscription_total = (float) $subtotal + (float) $sign_up_fee; $item->set_subtotal( $subscription_total ); $item->set_total( $subscription_total ); @@ -282,6 +288,23 @@ class WooCommerceOrderCreator { } } + /** + * Configures the taxes. + * + * @param WC_Product $product The Product. + * @param WC_Order_Item_Product $item The line item. + * @param float|string $subtotal The subtotal. + * @return void + * @psalm-suppress InvalidScalarArgument + */ + protected function configure_taxes( WC_Product $product, WC_Order_Item_Product $item, $subtotal ): void { + $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $taxes = WC_Tax::calc_tax( $subtotal, $tax_rates, true ); + + $item->set_tax_class( $product->get_tax_class() ); + $item->set_total_tax( (float) array_sum( $taxes ) ); + } + /** * Checks if the product with given ID is WC subscription. * diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index 70ae6dac2..d27091162 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -83,6 +83,9 @@ return array( 'compat.wc_shipping_tax.is_supported_plugin_version_active' => function (): bool { return class_exists( 'WC_Connect_Loader' ); }, + 'compat.nyp.is_supported_plugin_version_active' => function (): bool { + return function_exists( 'wc_nyp_init' ); + }, 'compat.module.url' => static function ( ContainerInterface $container ): string { /** diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index 09c35c24f..1749dcc0d 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -56,6 +56,11 @@ class CompatModule implements ModuleInterface { $this->fix_page_builders(); $this->exclude_cache_plugins_js_minification( $c ); $this->set_elementor_checkout_context(); + + $is_nyp_active = $c->get( 'compat.nyp.is_supported_plugin_version_active' ); + if ( $is_nyp_active ) { + $this->initialize_nyp_compat_layer(); + } } /** @@ -387,4 +392,24 @@ class CompatModule implements ModuleInterface { 3 ); } + + /** + * Sets up the compatibility layer for PayPal Shipping callback & WooCommerce Name Your Price plugin. + * + * @return void + */ + protected function initialize_nyp_compat_layer(): void { + add_filter( + 'woocommerce_paypal_payments_shipping_callback_cart_line_item_total', + static function( string $total, array $cart_item ) { + if ( ! isset( $cart_item['nyp'] ) ) { + return $total; + } + + return $cart_item['nyp']; + }, + 10, + 2 + ); + } } diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index c60212a2e..6cf2119a0 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -13,3 +13,7 @@ min-width: 0 !important; } } + +#ppc-button-ppcp-googlepay { + display: none; +} 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 973648af3..ac477404b 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -1,4 +1,3 @@ -import ContextHandlerFactory from './Context/ContextHandlerFactory'; import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding'; import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler'; import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; @@ -6,7 +5,13 @@ import UpdatePaymentData from './Helper/UpdatePaymentData'; import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons'; class GooglepayButton { - constructor( context, externalHandler, buttonConfig, ppcpConfig ) { + constructor( + context, + externalHandler, + buttonConfig, + ppcpConfig, + contextHandler + ) { apmButtonsInit( ppcpConfig ); this.isInitialized = false; @@ -15,16 +20,10 @@ class GooglepayButton { this.externalHandler = externalHandler; this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; + this.contextHandler = contextHandler; this.paymentsClient = null; - this.contextHandler = ContextHandlerFactory.create( - this.context, - this.buttonConfig, - this.ppcpConfig, - this.externalHandler - ); - this.log = function () { if ( this.buttonConfig.is_debug ) { //console.log('[GooglePayButton]', ...arguments); @@ -32,7 +31,7 @@ class GooglepayButton { }; } - init( config ) { + init( config, transactionInfo ) { if ( this.isInitialized ) { return; } @@ -47,6 +46,7 @@ class GooglepayButton { } this.googlePayConfig = config; + this.transactionInfo = transactionInfo; this.allowedPaymentMethods = config.allowedPaymentMethods; this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ]; @@ -62,6 +62,39 @@ class GooglepayButton { ) .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 ); } } ) @@ -76,7 +109,7 @@ class GooglepayButton { } this.isInitialized = false; - this.init( this.googlePayConfig ); + this.init( this.googlePayConfig, this.transactionInfo ); } validateConfig() { @@ -221,6 +254,19 @@ class GooglepayButton { } ); } + addButtonCheckout( baseCardPaymentMethod, wrapper, buttonStyle ) { + 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', + } ); + + wrapper.appendChild( button ); + } + waitForWrapper( selector, callback, delay = 100, timeout = 2000 ) { const startTime = Date.now(); const interval = setInterval( () => { @@ -243,22 +289,39 @@ class GooglepayButton { /** * Show Google Pay payment sheet when Google Pay payment button is clicked */ - async onButtonClick() { + onButtonClick() { this.log( 'onButtonClick', this.context ); - const paymentDataRequest = await this.paymentDataRequest(); - this.log( - 'onButtonClick: paymentDataRequest', - paymentDataRequest, - this.context - ); + const initiatePaymentRequest = () => { + window.ppcpFundingSource = 'googlepay'; - window.ppcpFundingSource = 'googlepay'; // Do this on another place like on create order endpoint handler. + const paymentDataRequest = this.paymentDataRequest(); - this.paymentsClient.loadPaymentData( paymentDataRequest ); + this.log( + 'onButtonClick: paymentDataRequest', + paymentDataRequest, + this.context + ); + + this.paymentsClient.loadPaymentData( paymentDataRequest ); + }; + + if ( 'function' === typeof this.contextHandler.validateForm ) { + // During regular checkout, validate the checkout form before initiating the payment. + this.contextHandler + .validateForm() + .then( initiatePaymentRequest, () => { + console.error( + '[GooglePayButton] Form validation failed.' + ); + } ); + } else { + // This is the flow on product page, cart, and other non-checkout pages. + initiatePaymentRequest(); + } } - async paymentDataRequest() { + paymentDataRequest() { const baseRequest = { apiVersion: 2, apiVersionMinor: 0, @@ -268,8 +331,7 @@ class GooglepayButton { const paymentDataRequest = Object.assign( {}, baseRequest ); paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods; - paymentDataRequest.transactionInfo = - await this.contextHandler.transactionInfo(); + paymentDataRequest.transactionInfo = this.transactionInfo; paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo; if ( @@ -308,43 +370,51 @@ class GooglepayButton { this.log( 'paymentData', paymentData ); return new Promise( async ( resolve, reject ) => { - const paymentDataRequestUpdate = {}; + try { + const paymentDataRequestUpdate = {}; - const updatedData = await new UpdatePaymentData( - this.buttonConfig.ajax.update_payment_data - ).update( paymentData ); - const transactionInfo = await this.contextHandler.transactionInfo(); + const updatedData = await new UpdatePaymentData( + this.buttonConfig.ajax.update_payment_data + ).update( paymentData ); + const transactionInfo = this.transactionInfo; - this.log( 'onPaymentDataChanged:updatedData', updatedData ); - this.log( 'onPaymentDataChanged:transactionInfo', transactionInfo ); + this.log( 'onPaymentDataChanged:updatedData', updatedData ); + this.log( + 'onPaymentDataChanged:transactionInfo', + transactionInfo + ); - updatedData.country_code = transactionInfo.countryCode; - updatedData.currency_code = transactionInfo.currencyCode; - updatedData.total_str = transactionInfo.totalPrice; + updatedData.country_code = transactionInfo.countryCode; + updatedData.currency_code = transactionInfo.currencyCode; + updatedData.total_str = transactionInfo.totalPrice; + + // Handle unserviceable address. + if ( ! updatedData.shipping_options?.shippingOptions?.length ) { + paymentDataRequestUpdate.error = + this.unserviceableShippingAddressError(); + resolve( paymentDataRequestUpdate ); + return; + } + + switch ( paymentData.callbackTrigger ) { + case 'INITIALIZE': + case 'SHIPPING_ADDRESS': + paymentDataRequestUpdate.newShippingOptionParameters = + updatedData.shipping_options; + paymentDataRequestUpdate.newTransactionInfo = + this.calculateNewTransactionInfo( updatedData ); + break; + case 'SHIPPING_OPTION': + paymentDataRequestUpdate.newTransactionInfo = + this.calculateNewTransactionInfo( updatedData ); + break; + } - // Handle unserviceable address. - if ( ! updatedData.shipping_options?.shippingOptions?.length ) { - paymentDataRequestUpdate.error = - this.unserviceableShippingAddressError(); resolve( paymentDataRequestUpdate ); - return; + } catch ( error ) { + console.error( 'Error during onPaymentDataChanged:', error ); + reject( error ); } - - switch ( paymentData.callbackTrigger ) { - case 'INITIALIZE': - case 'SHIPPING_ADDRESS': - paymentDataRequestUpdate.newShippingOptionParameters = - updatedData.shipping_options; - paymentDataRequestUpdate.newTransactionInfo = - this.calculateNewTransactionInfo( updatedData ); - break; - case 'SHIPPING_OPTION': - paymentDataRequestUpdate.newTransactionInfo = - this.calculateNewTransactionInfo( updatedData ); - break; - } - - resolve( paymentDataRequestUpdate ); } ); } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js index ebe8365df..e267f1b8a 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js @@ -1,39 +1,76 @@ import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import GooglepayButton from './GooglepayButton'; +import ContextHandlerFactory from './Context/ContextHandlerFactory'; class GooglepayManager { constructor( buttonConfig, ppcpConfig ) { this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; this.googlePayConfig = null; + this.transactionInfo = null; + this.contextHandler = null; this.buttons = []; - buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => { + buttonModuleWatcher.watchContextBootstrap( async ( bootstrap ) => { + this.contextHandler = ContextHandlerFactory.create( + bootstrap.context, + buttonConfig, + ppcpConfig, + bootstrap.handler + ); + const button = new GooglepayButton( bootstrap.context, bootstrap.handler, buttonConfig, - ppcpConfig + ppcpConfig, + this.contextHandler ); this.buttons.push( button ); - if ( this.googlePayConfig ) { - button.init( this.googlePayConfig ); + // Initialize button only if googlePayConfig and transactionInfo are already fetched. + if ( this.googlePayConfig && this.transactionInfo ) { + button.init( this.googlePayConfig, this.transactionInfo ); + } else { + await this.init(); + if ( this.googlePayConfig && this.transactionInfo ) { + button.init( this.googlePayConfig, this.transactionInfo ); + } } } ); } - init() { - ( async () => { - // Gets GooglePay configuration of the PayPal merchant. - this.googlePayConfig = await paypal.Googlepay().config(); + async init() { + try { + if ( ! this.googlePayConfig ) { + // Gets GooglePay configuration of the PayPal merchant. + this.googlePayConfig = await paypal.Googlepay().config(); + } + + if ( ! this.transactionInfo ) { + this.transactionInfo = await this.fetchTransactionInfo(); + } for ( const button of this.buttons ) { - button.init( this.googlePayConfig ); + button.init( this.googlePayConfig, this.transactionInfo ); } - } )(); + } catch ( error ) { + console.error( 'Error during initialization:', error ); + } + } + + async fetchTransactionInfo() { + try { + if ( ! this.contextHandler ) { + throw new Error( 'ContextHandler is not initialized' ); + } + return await this.contextHandler.transactionInfo(); + } catch ( error ) { + console.error( 'Error fetching transaction info:', error ); + throw error; + } } reinit() { diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js index 41cace0c0..b9f7b3c87 100644 --- a/modules/ppcp-googlepay/resources/js/boot-admin.js +++ b/modules/ppcp-googlepay/resources/js/boot-admin.js @@ -1,6 +1,7 @@ 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'; /** * Accessor that creates and returns a single PreviewButtonManager instance. @@ -95,14 +96,22 @@ class GooglePayPreviewButton extends PreviewButton { } createButton( buttonConfig ) { + const contextHandler = ContextHandlerFactory.create( + 'preview', + buttonConfig, + this.ppcpConfig, + null + ); + const button = new GooglepayButton( 'preview', null, buttonConfig, - this.ppcpConfig + this.ppcpConfig, + contextHandler ); - button.init( this.apiConfig ); + button.init( this.apiConfig, null ); } /** diff --git a/modules/ppcp-googlepay/services.php b/modules/ppcp-googlepay/services.php index 65a96ca06..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,86 +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 { - $default_currencies = array( - 'AUD', - 'BRL', - 'CAD', - 'CHF', - 'CZK', - 'DKK', - 'EUR', - 'GBP', - 'HKD', - 'HUF', - 'ILS', - 'JPY', - 'MXN', - 'NOK', - 'NZD', - 'PHP', - 'PLN', - 'SEK', - 'SGD', - 'THB', - 'TWD', - 'USD', - ); - + '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' => $default_currencies, - 'AT' => $default_currencies, - 'BE' => $default_currencies, - 'BG' => $default_currencies, - 'CA' => $default_currencies, - 'CY' => $default_currencies, - 'CZ' => $default_currencies, - 'DK' => $default_currencies, - 'EE' => $default_currencies, - 'FI' => $default_currencies, - 'FR' => $default_currencies, - 'DE' => $default_currencies, - 'GR' => $default_currencies, - 'HK' => $default_currencies, - 'HU' => $default_currencies, - 'IE' => $default_currencies, - 'IT' => $default_currencies, - 'LV' => $default_currencies, - 'LI' => $default_currencies, - 'LT' => $default_currencies, - 'LU' => $default_currencies, - 'MT' => $default_currencies, - 'NO' => $default_currencies, - 'NL' => $default_currencies, - 'PL' => $default_currencies, - 'PT' => $default_currencies, - 'RO' => $default_currencies, - 'SG' => $default_currencies, - 'SK' => $default_currencies, - 'SI' => $default_currencies, - 'ES' => $default_currencies, - 'SE' => $default_currencies, - 'GB' => $default_currencies, - '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' ), @@ -174,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' ), @@ -184,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 ''; @@ -195,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 ''; @@ -252,5 +260,14 @@ return array( esc_html( $button_text ) ); }, - + 'googlepay.wc-gateway' => static function ( ContainerInterface $container ): GooglePayGateway { + return new GooglePayGateway( + $container->get( 'wcgateway.order-processor' ), + $container->get( 'api.factory.paypal-checkout-url' ), + $container->get( 'wcgateway.processor.refunds' ), + $container->get( 'wcgateway.transaction-url-provider' ), + $container->get( 'session.handler' ), + $container->get( 'googlepay.url' ) + ); + }, ); diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php index 6fab601b5..bd6faea79 100644 --- a/modules/ppcp-googlepay/src/Assets/Button.php +++ b/modules/ppcp-googlepay/src/Assets/Button.php @@ -13,7 +13,9 @@ use Exception; use Psr\Log\LoggerInterface; use WC_Countries; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; @@ -25,6 +27,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; */ class Button implements ButtonInterface { + use ContextTrait; + /** * The URL to the module. * @@ -409,7 +413,7 @@ class Button implements ButtonInterface { */ public function script_data(): array { $shipping = array( - 'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' ) + 'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' ) ? boolval( $this->settings->get( 'googlepay_button_shipping_enabled' ) ) : false, 'configured' => wc_shipping_enabled() && wc_get_shipping_method_count( false, true ) > 0, @@ -421,19 +425,23 @@ class Button implements ButtonInterface { $is_enabled = $this->settings->has( 'googlepay_button_enabled' ) && $this->settings->get( 'googlepay_button_enabled' ); + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $is_wc_gateway_enabled = isset( $available_gateways[ GooglePayGateway::ID ] ); + return array( - 'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION', - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, - 'is_enabled' => $is_enabled, - 'sdk_url' => $this->sdk_url, - 'button' => array( + 'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION', + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'is_enabled' => $is_enabled, + 'is_wc_gateway_enabled' => $is_wc_gateway_enabled, + 'sdk_url' => $this->sdk_url, + 'button' => array( 'wrapper' => '#ppc-button-googlepay-container', 'style' => $this->button_styles_for_context( 'cart' ), // For now use cart. Pass the context if necessary. 'mini_cart_wrapper' => '#ppc-button-googlepay-container-minicart', 'mini_cart_style' => $this->button_styles_for_context( 'mini-cart' ), ), - 'shipping' => $shipping, - 'ajax' => array( + 'shipping' => $shipping, + 'ajax' => array( 'update_payment_data' => array( 'endpoint' => \WC_AJAX::get_endpoint( UpdatePaymentDataEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( UpdatePaymentDataEndpoint::nonce() ), diff --git a/modules/ppcp-googlepay/src/GooglePayGateway.php b/modules/ppcp-googlepay/src/GooglePayGateway.php new file mode 100644 index 000000000..26a84f326 --- /dev/null +++ b/modules/ppcp-googlepay/src/GooglePayGateway.php @@ -0,0 +1,238 @@ +id = self::ID; + + $this->method_title = __( 'Google Pay (via PayPal) ', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'The separate payment gateway with the Google Pay button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' ); + + $this->title = $this->get_option( 'title', __( 'Google Pay', 'woocommerce-paypal-payments' ) ); + $this->description = $this->get_option( 'description', '' ); + + $this->module_url = $module_url; + $this->icon = esc_url( $this->module_url ) . 'assets/images/googlepay.png'; + + $this->init_form_fields(); + $this->init_settings(); + $this->order_processor = $order_processor; + $this->paypal_checkout_url_factory = $paypal_checkout_url_factory; + $this->refund_processor = $refund_processor; + $this->transaction_url_provider = $transaction_url_provider; + $this->session_handler = $session_handler; + + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( + $this, + 'process_admin_options', + ) + ); + } + + /** + * Initialize the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'Google Pay', 'woocommerce-paypal-payments' ), + 'default' => 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable Google Pay payment gateway.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + ); + } + + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + /** + * If the WC_Order is paid through the approved webhook. + */ + //phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_REQUEST['ppcp-resume-order'] ) && $wc_order->has_status( 'processing' ) ) { + return $this->handle_payment_success( $wc_order ); + } + //phpcs:enable WordPress.Security.NonceVerification.Recommended + + do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order ); + + try { + try { + $this->order_processor->process( $wc_order ); + + do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalOrderMissingException $exc ) { + $order = $this->order_processor->create_order( $wc_order ); + + return array( + 'result' => 'success', + 'redirect' => ( $this->paypal_checkout_url_factory )( $order->id() ), + ); + } + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( Exception $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, \WC_Order::class ) ) { + return false; + } + return $this->refund_processor->process( $order, (float) $amount, (string) $reason ); + } + + /** + * Return transaction url for this gateway and given order. + * + * @param \WC_Order $order WC order to get transaction url by. + * + * @return string + */ + public function get_transaction_url( $order ): string { + $this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order ); + + return parent::get_transaction_url( $order ); + } +} diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index e2b0ea8c5..b7feedc07 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Googlepay; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; +use WC_Payment_Gateway; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint; @@ -159,6 +160,46 @@ class GooglepayModule implements ModuleInterface { }, 1 ); + + add_filter( + 'woocommerce_payment_gateways', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + static function ( $methods ) use ( $c ): array { + if ( ! is_array( $methods ) ) { + return $methods; + } + + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + if ( $settings->has( 'googlepay_button_enabled' ) && $settings->get( 'googlepay_button_enabled' ) ) { + $googlepay_gateway = $c->get( 'googlepay.wc-gateway' ); + assert( $googlepay_gateway instanceof WC_Payment_Gateway ); + + $methods[] = $googlepay_gateway; + } + + return $methods; + } + ); + + add_action( + 'woocommerce_review_order_after_submit', + function () { + echo '
    '; + } + ); + + add_action( + 'woocommerce_pay_order_after_submit', + function () { + echo '
    '; + } + ); } /** diff --git a/modules/ppcp-googlepay/src/Helper/ApmApplies.php b/modules/ppcp-googlepay/src/Helper/ApmApplies.php index 84ad2e057..96a16b210 100644 --- a/modules/ppcp-googlepay/src/Helper/ApmApplies.php +++ b/modules/ppcp-googlepay/src/Helper/ApmApplies.php @@ -16,11 +16,18 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper; class ApmApplies { /** - * The matrix which countries and currency combinations can be used for GooglePay. + * The list of which countries can be used for GooglePay. * * @var array */ - private $allowed_country_currency_matrix; + private $allowed_countries; + + /** + * The list of which currencies can be used for GooglePay. + * + * @var array + */ + private $allowed_currencies; /** * 3-letter currency code of the shop. @@ -39,30 +46,39 @@ class ApmApplies { /** * DccApplies constructor. * - * @param array $allowed_country_currency_matrix The matrix which countries and currency combinations can be used for GooglePay. + * @param array $allowed_countries The list of which countries can be used for GooglePay. + * @param array $allowed_currencies The list of which currencies can be used for GooglePay. * @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 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/Assets/OnboardingAssets.php b/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php index 4b0c9c165..c5385216d 100644 --- a/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php +++ b/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php @@ -96,21 +96,21 @@ class OnboardingAssets { */ public function register(): bool { - $url = untrailingslashit( $this->module_url ) . '/assets/css/onboarding.css'; wp_register_style( 'ppcp-onboarding', - $url, + $this->module_url . '/assets/css/onboarding.css', array(), $this->version ); - $url = untrailingslashit( $this->module_url ) . '/assets/js/settings.js'; + wp_register_script( 'ppcp-settings', - $url, + $this->module_url . '/assets/js/settings.js', array(), $this->version, true ); + wp_localize_script( 'ppcp-settings', 'PayPalCommerceSettings', @@ -122,14 +122,14 @@ class OnboardingAssets { ) ); - $url = untrailingslashit( $this->module_url ) . '/assets/js/onboarding.js'; wp_register_script( 'ppcp-onboarding', - $url, + $this->module_url . '/assets/js/onboarding.js', array( 'jquery' ), $this->version, true ); + wp_localize_script( 'ppcp-onboarding', 'PayPalCommerceGatewayOnboarding', @@ -164,17 +164,22 @@ class OnboardingAssets { /** * Enqueues the necessary scripts. * - * @return bool + * @return void */ - public function enqueue(): bool { - wp_enqueue_style( 'ppcp-onboarding' ); - wp_enqueue_script( 'ppcp-settings' ); - if ( ! $this->should_render_onboarding_script() ) { - return false; + public function enqueue(): void { + // Do not enqueue anything when we are not on a PayPal Payments settings tab. + if ( ! $this->page_id ) { + return; } - wp_enqueue_script( 'ppcp-onboarding' ); - return true; + // Enqueue general assets for the plugin's settings page. + wp_enqueue_script( 'ppcp-settings' ); + wp_enqueue_style( 'ppcp-onboarding' ); // File also contains general settings styles. + + // Conditionally enqueue the onboarding script, when needed. + if ( $this->should_render_onboarding_script() ) { + wp_enqueue_script( 'ppcp-onboarding' ); + } } /** 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/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js index 6bab57ad6..82152b2ad 100644 --- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js +++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js @@ -188,10 +188,6 @@ document.addEventListener( 'DOMContentLoaded', () => { } function shouldDisableCardButton() { - if ( currentTabId() === 'ppcp-card-button-gateway' ) { - return false; - } - return ( PayPalCommerceGatewaySettings.is_acdc_enabled || jQuery( '#ppcp-allow_card_button_gateway' ).is( ':checked' ) @@ -230,6 +226,14 @@ document.addEventListener( 'DOMContentLoaded', () => { } if ( shouldDisableCardButton() ) { + const standardCardButtonInput = document.querySelector( + '#woocommerce_ppcp-card-button-gateway_enabled' + ); + + if ( standardCardButtonInput ) { + standardCardButtonInput.disabled = true; + } + disabledSources = disabledSources.concat( 'card' ); } diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index f0d848216..52a41576e 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway; use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\State; @@ -196,6 +197,7 @@ return array( OXXOGateway::ID, Settings::PAY_LATER_TAB_ID, AxoGateway::ID, + GooglePayGateway::ID, ), true ); @@ -217,6 +219,7 @@ return array( CardButtonGateway::ID, Settings::PAY_LATER_TAB_ID, Settings::CONNECTION_TAB_ID, + GooglePayGateway::ID, ), true ); @@ -363,7 +366,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 { diff --git a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php index de07ecfef..a55fe621c 100644 --- a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php +++ b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php @@ -106,6 +106,12 @@ class DisableGateways { return $methods; } + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_method = wc_clean( wp_unslash( $_POST['payment_method'] ?? '' ) ); + if ( $payment_method && is_string( $payment_method ) ) { + return array( $payment_method => $methods[ $payment_method ] ); + } + return array( PayPalGateway::ID => $methods[ PayPalGateway::ID ] ); } diff --git a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php index 81b057423..2dfc88904 100644 --- a/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php +++ b/modules/ppcp-wc-gateway/src/Notice/GatewayWithoutPayPalAdminNotice.php @@ -19,9 +19,10 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; * Creates the admin message about the gateway being enabled without the PayPal gateway. */ class GatewayWithoutPayPalAdminNotice { - private const NOTICE_OK = ''; - private const NOTICE_DISABLED_GATEWAY = 'disabled_gateway'; - private const NOTICE_DISABLED_LOCATION = 'disabled_location'; + private const NOTICE_OK = ''; + private const NOTICE_DISABLED_GATEWAY = 'disabled_gateway'; + private const NOTICE_DISABLED_LOCATION = 'disabled_location'; + private const NOTICE_DISABLED_CARD_BUTTON = 'disabled_card'; /** * The gateway ID. @@ -99,6 +100,9 @@ class GatewayWithoutPayPalAdminNotice { public function message(): ?Message { $notice_type = $this->check(); + $url1 = ''; + $url2 = ''; + switch ( $notice_type ) { case self::NOTICE_DISABLED_GATEWAY: /* translators: %1$s the gateway name, %2$s URL. */ @@ -114,6 +118,15 @@ class GatewayWithoutPayPalAdminNotice { 'woocommerce-paypal-payments' ); break; + case self::NOTICE_DISABLED_CARD_BUTTON: + /* translators: %1$s Standard Card Button section URL, %2$s Advanced Card Processing section URL. */ + $text = __( + 'The Standard Card Button cannot be used while Advanced Card Processing is enabled.', + 'woocommerce-paypal-payments' + ); + $url1 = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-card-button-gateway' ); + $url2 = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=ppcp-credit-card-gateway' ); + break; default: return null; } @@ -130,6 +143,15 @@ class GatewayWithoutPayPalAdminNotice { $name, admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) ); + + if ( $notice_type === self::NOTICE_DISABLED_CARD_BUTTON ) { + $message = sprintf( + $text, + $url1, + $url2 + ); + } + return new Message( $message, 'warning' ); } @@ -160,6 +182,13 @@ class GatewayWithoutPayPalAdminNotice { return self::NOTICE_DISABLED_LOCATION; } + $is_dcc_enabled = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ?? false; + $is_card_button_allowed = $this->settings->has( 'allow_card_button_gateway' ) && $this->settings->get( 'allow_card_button_gateway' ); + + if ( $is_dcc_enabled && $is_card_button_allowed ) { + return self::NOTICE_DISABLED_CARD_BUTTON; + } + return self::NOTICE_OK; } 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/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 76de0478c..0b1315376 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -528,6 +528,14 @@ class WCGatewayModule implements ModuleInterface { return $methods; } + $is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false; + $standard_card_button = get_option( 'woocommerce_ppcp-card-button-gateway_settings' ); + + if ( $is_dcc_enabled && isset( $standard_card_button['enabled'] ) ) { + $standard_card_button['enabled'] = 'no'; + update_option( 'woocommerce_ppcp-card-button-gateway_settings', $standard_card_button ); + } + $dcc_applies = $container->get( 'api.helpers.dccapplies' ); assert( $dcc_applies instanceof DccApplies ); diff --git a/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php b/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php index df31b45bc..835ec4d4a 100644 --- a/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php +++ b/modules/ppcp-wc-subscriptions/src/WcSubscriptionsModule.php @@ -416,7 +416,9 @@ class WcSubscriptionsModule implements ModuleInterface { $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof Settings ); - if ( $subscriptions_helper->plugin_is_active() ) { + $subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : ''; + + if ( 'disable_paypal_subscriptions' !== $subscriptions_mode && $subscriptions_helper->plugin_is_active() ) { $supports = array( 'subscriptions', 'subscription_cancellation', @@ -442,7 +444,12 @@ class WcSubscriptionsModule implements ModuleInterface { $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); - if ( $subscriptions_helper->plugin_is_active() ) { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $vaulting_enabled = $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ); + + if ( $vaulting_enabled && $subscriptions_helper->plugin_is_active() ) { $supports = array( 'subscriptions', 'subscription_cancellation', @@ -467,7 +474,12 @@ class WcSubscriptionsModule implements ModuleInterface { $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); - if ( $subscriptions_helper->plugin_is_active() ) { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + $subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : ''; + + if ( 'disable_paypal_subscriptions' !== $subscriptions_mode && $subscriptions_helper->plugin_is_active() ) { $supports = array( 'subscriptions', 'subscription_cancellation', diff --git a/modules/ppcp-webhooks/src/WebhookModule.php b/modules/ppcp-webhooks/src/WebhookModule.php index 2458e5b4d..939a8d512 100644 --- a/modules/ppcp-webhooks/src/WebhookModule.php +++ b/modules/ppcp-webhooks/src/WebhookModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks; +use WC_Order; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; 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 ffe94e2a5..1aed175cb 100644 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ === WooCommerce PayPal Payments === -Contributors: woocommerce, automattic, inpsyde +Contributors: woocommerce, automattic, syde Tags: woocommerce, paypal, payments, ecommerce, checkout, cart, pay later, apple pay, subscriptions, debit card, credit card, google pay 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,7 +179,24 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == -= 2.8.2 - xxxx-xx-xx = += 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 * Fix - W3 Total Cache: Remove type from file parameter as sometimes null gets passed causing errors #2403 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 69c797785..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' ); @@ -223,6 +223,22 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); } ); + add_action( + 'in_plugin_update_message-woocommerce-paypal-payments/woocommerce-paypal-payments.php', + static function( array $plugin_data, \stdClass $new_data ) { + if ( version_compare( $plugin_data['Version'], '3.0.0', '<' ) && + version_compare( $new_data->new_version, '3.0.0', '>=' ) ) { + printf( + '
    %s: %s', + esc_html__( 'Warning', 'woocommerce-paypal-payments' ), + esc_html__( 'WooCommerce PayPal Payments version 3.0.0 contains significant changes that may impact your website. We strongly recommend reviewing the changes and testing the update on a staging site before updating it on your production environment.', 'woocommerce-paypal-payments' ) + ); + } + }, + 10, + 2 + ); + /** * Check if WooCommerce is active. *