diff --git a/modules.php b/modules.php index 7e93c89ea..5b8955527 100644 --- a/modules.php +++ b/modules.php @@ -39,7 +39,7 @@ return function ( string $root_dir ): iterable { if ( apply_filters( //phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores 'woocommerce.feature-flags.woocommerce_paypal_payments.googlepay_enabled', - getenv( 'PCP_GOOGLEPAY_ENABLED' ) === '1' + getenv( 'PCP_GOOGLEPAY_ENABLED' ) !== '0' ) ) { $modules[] = ( require "$modules_dir/ppcp-googlepay/module.php" )(); } diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index ec73e16fd..a222f0782 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\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; @@ -120,7 +121,8 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'api.factory.sellerstatus' ), $container->get( 'api.partner_merchant_id' ), - $container->get( 'api.merchant_id' ) + $container->get( 'api.merchant_id' ), + $container->get( 'api.helper.failure-registry' ) ); }, 'api.factory.sellerstatus' => static function ( ContainerInterface $container ) : SellerStatusFactory { @@ -846,6 +848,10 @@ return array( $purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' ); return new OrderTransient( $cache, $purchase_unit_sanitizer ); }, + 'api.helper.failure-registry' => static function( ContainerInterface $container ): FailureRegistry { + $cache = new Cache( 'ppcp-paypal-api-status-cache' ); + return new FailureRegistry( $cache ); + }, 'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make( static function( ContainerInterface $container ): PurchaseUnitSanitizer { $settings = $container->get( 'wcgateway.settings' ); diff --git a/modules/ppcp-api-client/src/ApiModule.php b/modules/ppcp-api-client/src/ApiModule.php index f12e69668..ed09ecddc 100644 --- a/modules/ppcp-api-client/src/ApiModule.php +++ b/modules/ppcp-api-client/src/ApiModule.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; @@ -81,6 +82,18 @@ class ApiModule implements ModuleInterface { 10, 2 ); + add_action( + 'woocommerce_paypal_payments_clear_apm_product_status', + function () use ( $c ) { + $failure_registry = $c->has( 'api.helper.failure-registry' ) ? $c->get( 'api.helper.failure-registry' ) : null; + + if ( $failure_registry instanceof FailureRegistry ) { + $failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY ); + } + }, + 10, + 2 + ); } /** diff --git a/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php index 275f94c97..b7624d2de 100644 --- a/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PartnersEndpoint.php @@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; /** * Class PartnersEndpoint @@ -65,6 +66,13 @@ class PartnersEndpoint { */ private $merchant_id; + /** + * The failure registry. + * + * @var FailureRegistry + */ + private $failure_registry; + /** * PartnersEndpoint constructor. * @@ -74,6 +82,7 @@ class PartnersEndpoint { * @param SellerStatusFactory $seller_status_factory The seller status factory. * @param string $partner_id The partner ID. * @param string $merchant_id The merchant ID. + * @param FailureRegistry $failure_registry The API failure registry. */ public function __construct( string $host, @@ -81,7 +90,8 @@ class PartnersEndpoint { LoggerInterface $logger, SellerStatusFactory $seller_status_factory, string $partner_id, - string $merchant_id + string $merchant_id, + FailureRegistry $failure_registry ) { $this->host = $host; $this->bearer = $bearer; @@ -89,6 +99,7 @@ class PartnersEndpoint { $this->seller_status_factory = $seller_status_factory; $this->partner_id = $partner_id; $this->merchant_id = $merchant_id; + $this->failure_registry = $failure_registry; } /** @@ -140,9 +151,15 @@ class PartnersEndpoint { 'response' => $response, ) ); + + // Register the failure on api failure registry. + $this->failure_registry->add_failure( FailureRegistry::SELLER_STATUS_KEY ); + throw $error; } + $this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY ); + $status = $this->seller_status_factory->from_paypal_reponse( $json ); return $status; } diff --git a/modules/ppcp-api-client/src/Helper/FailureRegistry.php b/modules/ppcp-api-client/src/Helper/FailureRegistry.php new file mode 100644 index 000000000..68889b13c --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/FailureRegistry.php @@ -0,0 +1,94 @@ +cache = $cache; + } + + /** + * Returns if there was a failure within a given timeframe. + * + * @param string $key The cache key. + * @param int $seconds The timeframe in seconds. + * @return bool + */ + public function has_failure_in_timeframe( string $key, int $seconds ): bool { + $cache_key = $this->cache_key( $key ); + $failure_time = $this->cache->get( $cache_key ); + + if ( ! $failure_time ) { + return false; + } + + $expiration = $failure_time + $seconds; + return $expiration > time(); + } + + /** + * Registers a failure. + * + * @param string $key The cache key. + * @return void + */ + public function add_failure( string $key ) { + $cache_key = $this->cache_key( $key ); + $this->cache->set( $cache_key, time(), self::CACHE_TIMEOUT ); + } + + /** + * Clear a given failure. + * + * @param string $key The cache key. + * @return void + */ + public function clear_failures( string $key ) { + $cache_key = $this->cache_key( $key ); + if ( $this->cache->has( $cache_key ) ) { + $this->cache->delete( $cache_key ); + } + } + + /** + * Build cache key. + * + * @param string $key The cache key. + * @return string + */ + private function cache_key( string $key ): string { + return implode( '_', array( self::CACHE_KEY, $key ) ); + } + +} diff --git a/modules/ppcp-api-client/src/Helper/OrderTransient.php b/modules/ppcp-api-client/src/Helper/OrderTransient.php index d0c7d4a01..b6b7a0d99 100644 --- a/modules/ppcp-api-client/src/Helper/OrderTransient.php +++ b/modules/ppcp-api-client/src/Helper/OrderTransient.php @@ -17,11 +17,11 @@ use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; /** - * Class OrderHelper + * Class OrderTransient */ class OrderTransient { const CACHE_KEY = 'order_transient'; - const CACHE_TIMEOUT = DAY_IN_SECONDS; // If necessary we can increase this. + const CACHE_TIMEOUT = 60 * 60 * 24; // DAY_IN_SECONDS, if necessary we can increase this. /** * The Cache. diff --git a/modules/ppcp-applepay/extensions.php b/modules/ppcp-applepay/extensions.php index 64c41b52c..38955fba0 100644 --- a/modules/ppcp-applepay/extensions.php +++ b/modules/ppcp-applepay/extensions.php @@ -13,10 +13,20 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( 'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array { + + // Eligibility check. + if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) { + return $fields; + } + + $is_available = $container->get( 'applepay.enabled' ); + $is_referral = $container->get( 'applepay.is_referral' ); + $insert_after = function ( array $array, string $key, array $new ): array { $keys = array_keys( $array ); $index = array_search( $key, $keys, true ); @@ -27,9 +37,27 @@ return array( $display_manager = $container->get( 'wcgateway.display-manager' ); assert( $display_manager instanceof DisplayManager ); - if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) { + // Connection tab fields. + $fields = $insert_after( + $fields, + 'ppcp_dcc_status', + array( + 'applepay_status' => array( + 'title' => __( 'Apple Pay Payments', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-text', + 'text' => $container->get( 'applepay.settings.connection.status-text' ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, + ), + ) + ); + + if ( ! $is_available && $is_referral ) { $connection_url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading' ); - $connection_link = ''; + $connection_link = ''; return $insert_after( $fields, 'allow_card_button_gateway', @@ -57,7 +85,7 @@ return array( array( $display_manager ->rule() - ->condition_element( 'applepay_button_enabled', '1' ) + ->condition_is_true( false ) ->action_enable( 'applepay_button_enabled' ) ->to_array(), ) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 7a336ea44..e0fe0992d 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -31,6 +31,12 @@ class ApplepayButton { this.updated_contact_info = [] this.selectedShippingMethod = [] this.nonce = document.getElementById('woocommerce-process-checkout-nonce').value + + this.log = function() { + if ( this.buttonConfig.is_debug ) { + console.log('[ApplePayButton]', ...arguments); + } + } } init(config) { @@ -52,12 +58,12 @@ class ApplepayButton { const id = "#apple-" + this.buttonConfig.button.wrapper; if (this.context === 'mini-cart') { - document.querySelector(id_minicart).addEventListener('click', (evt) => { + document.querySelector(id_minicart)?.addEventListener('click', (evt) => { evt.preventDefault(); this.onButtonClick(); }); } else { - document.querySelector(id).addEventListener('click', (evt) => { + document.querySelector(id)?.addEventListener('click', (evt) => { evt.preventDefault(); this.onButtonClick(); }); @@ -149,7 +155,10 @@ class ApplepayButton { const language = this.buttonConfig.button.lang; const color = this.buttonConfig.button.color; const id = "apple-" + wrapper; - appleContainer.innerHTML = ``; + + if (appleContainer) { + appleContainer.innerHTML = ``; + } jQuery('#' + wrapper).addClass('ppcp-button-' + shape); jQuery(wrapper).append(appleContainer); @@ -256,6 +265,7 @@ class ApplepayButton { }) }) .catch(validateError => { + console.error(validateError); //call backend to update validation to false jQuery.ajax({ url: this.buttonConfig.ajax_url, diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index 1cc6f81d3..de5c22943 100644 --- a/modules/ppcp-applepay/services.php +++ b/modules/ppcp-applepay/services.php @@ -16,6 +16,9 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; use WooCommerce\PayPalCommerce\Applepay\Assets\DataToAppleButtonScripts; use WooCommerce\PayPalCommerce\Applepay\Assets\BlocksPaymentMethod; use WooCommerce\PayPalCommerce\Applepay\Helper\ApmApplies; +use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus; +use WooCommerce\PayPalCommerce\Onboarding\Environment; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -36,12 +39,21 @@ return array( '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 { + $status = $container->get( 'applepay.apple-product-status' ); + assert( $status instanceof AppleProductStatus ); + + return ! $status->has_request_failure(); + }, 'applepay.apple-product-status' => static function( ContainerInterface $container ): AppleProductStatus { return new AppleProductStatus( $container->get( 'wcgateway.settings' ), $container->get( 'api.endpoint.partners' ), $container->get( 'applepay.status-cache' ), - $container->get( 'onboarding.state' ) + $container->get( 'onboarding.state' ), + $container->get( 'api.helper.failure-registry' ) ); }, 'applepay.enabled' => static function ( ContainerInterface $container ): bool { @@ -116,4 +128,52 @@ return array( ) ); }, + + '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 { + return 'https://www.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=APPLE_PAY'; + }, + + 'applepay.settings.connection.status-text' => static function ( ContainerInterface $container ): string { + $state = $container->get( 'onboarding.state' ); + if ( $state->current_state() < State::STATE_ONBOARDED ) { + return ''; + } + + $product_status = $container->get( 'applepay.apple-product-status' ); + assert( $product_status instanceof AppleProductStatus ); + + $environment = $container->get( 'onboarding.environment' ); + assert( $environment instanceof Environment ); + + $enabled = $product_status->apple_is_active(); + + $enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' ); + $disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' ); + + $button_text = $enabled + ? esc_html__( 'Settings', 'woocommerce-paypal-payments' ) + : esc_html__( 'Enable Apple Pay', 'woocommerce-paypal-payments' ); + + $enable_url = $environment->current_environment_is( Environment::PRODUCTION ) + ? $container->get( 'applepay.enable-url-live' ) + : $container->get( 'applepay.enable-url-sandbox' ); + + $button_url = $enabled + ? admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway#field-alternative_payment_methods' ) + : $enable_url; + + return sprintf( + '

%1$s %2$s

%5$s

', + $enabled ? $enabled_status_text : $disabled_status_text, + $enabled ? '' : '', + $enabled ? '_self' : '_blank', + esc_url( $button_url ), + esc_html( $button_text ) + ); + }, + ); diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 22f1ba4e7..bbac0fe13 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -147,6 +147,19 @@ class ApplePayButton implements ButtonInterface { */ public function initialize(): void { add_filter( 'ppcp_onboarding_options', array( $this, 'add_apple_onboarding_option' ), 10, 1 ); + add_filter( + 'ppcp_partner_referrals_option', + function ( array $option ): array { + if ( $option['valid'] ) { + return $option; + } + if ( $option['field'] === 'ppcp-onboarding-apple' ) { + $option['valid'] = true; + $option['value'] = ( $option['value'] ? '1' : '' ); + } + return $option; + } + ); add_filter( 'ppcp_partner_referrals_data', function ( array $data ): array { @@ -165,7 +178,6 @@ class ApplePayButton implements ButtonInterface { $data['products'][0] = 'PAYMENT_METHODS'; } $data['capabilities'][] = 'APPLE_PAY'; - $nonce = $data['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['seller_nonce']; $data['operations'][] = array( 'operation' => 'API_INTEGRATION', 'api_integration_preference' => array( @@ -173,11 +185,10 @@ class ApplePayButton implements ButtonInterface { 'integration_method' => 'PAYPAL', 'integration_type' => 'THIRD_PARTY', 'third_party_details' => array( - 'features' => array( + 'features' => array( 'PAYMENT', 'REFUND', ), - 'seller_nonce' => $nonce, ), ), ), @@ -196,6 +207,10 @@ class ApplePayButton implements ButtonInterface { * @return string */ public function add_apple_onboarding_option( $options ): string { + if ( ! apply_filters( 'woocommerce_paypal_payments_apple_pay_onboarding_option', false ) ) { + return $options; + } + $checked = ''; try { $onboard_with_apple = $this->settings->get( 'ppcp-onboarding-apple' ); @@ -206,7 +221,7 @@ class ApplePayButton implements ButtonInterface { $checked = ''; } - return $options . '
  • '; diff --git a/modules/ppcp-applepay/src/Assets/AppleProductStatus.php b/modules/ppcp-applepay/src/Assets/AppleProductStatus.php index e804bc069..83b6e7942 100644 --- a/modules/ppcp-applepay/src/Assets/AppleProductStatus.php +++ b/modules/ppcp-applepay/src/Assets/AppleProductStatus.php @@ -11,8 +11,8 @@ namespace WooCommerce\PayPalCommerce\Applepay\Assets; use Throwable; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -36,6 +36,14 @@ class AppleProductStatus { * @var bool|null */ private $current_status_cache; + + /** + * If there was a request failure. + * + * @var bool + */ + private $has_request_failure = false; + /** * The settings. * @@ -57,6 +65,13 @@ class AppleProductStatus { */ private $onboarding_state; + /** + * The API failure registry + * + * @var FailureRegistry + */ + private $api_failure_registry; + /** * PayUponInvoiceProductStatus constructor. * @@ -64,17 +79,20 @@ class AppleProductStatus { * @param PartnersEndpoint $partners_endpoint The Partner Endpoint. * @param Cache $cache The cache. * @param State $onboarding_state The onboarding state. + * @param FailureRegistry $api_failure_registry The API failure registry. */ public function __construct( Settings $settings, PartnersEndpoint $partners_endpoint, Cache $cache, - State $onboarding_state + State $onboarding_state, + FailureRegistry $api_failure_registry ) { - $this->settings = $settings; - $this->partners_endpoint = $partners_endpoint; - $this->cache = $cache; - $this->onboarding_state = $onboarding_state; + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; + $this->cache = $cache; + $this->onboarding_state = $onboarding_state; + $this->api_failure_registry = $api_failure_registry; } /** @@ -99,9 +117,17 @@ class AppleProductStatus { return true; } + // Check API failure registry to prevent multiple failed API requests. + if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) { + $this->has_request_failure = true; + $this->current_status_cache = false; + return $this->current_status_cache; + } + try { $seller_status = $this->partners_endpoint->seller_status(); } catch ( Throwable $error ) { + $this->has_request_failure = true; $this->current_status_cache = false; return false; } @@ -124,4 +150,14 @@ class AppleProductStatus { $this->current_status_cache = false; return false; } + + /** + * Returns if there was a request failure. + * + * @return bool + */ + public function has_request_failure(): bool { + return $this->has_request_failure; + } + } diff --git a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php index 90f25b6c1..6558ac472 100644 --- a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php +++ b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php @@ -127,6 +127,7 @@ class DataToAppleButtonScripts { return array( 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, 'button' => array( 'wrapper' => 'applepay-container', 'mini_cart_wrapper' => 'applepay-container-minicart', @@ -180,6 +181,7 @@ class DataToAppleButtonScripts { return array( 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, 'button' => array( 'wrapper' => 'applepay-container', 'mini_cart_wrapper' => 'applepay-container-minicart', diff --git a/modules/ppcp-button/resources/css/gateway.scss b/modules/ppcp-button/resources/css/gateway.scss index b563fcab9..f78d1b5b9 100644 --- a/modules/ppcp-button/resources/css/gateway.scss +++ b/modules/ppcp-button/resources/css/gateway.scss @@ -7,3 +7,7 @@ -webkit-filter: grayscale(100%); filter: grayscale(100%); } + +.ppc-button-wrapper #ppcp-messages:first-child { + padding-top: 10px; +} diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 27f9ca882..e7fae33b0 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -156,8 +156,15 @@ class CheckoutBootstap { } shouldShowMessages() { - return getCurrentPaymentMethod() === PaymentMethods.PAYPAL - && !PayPalCommerceGateway.is_free_trial_cart; + // hide when another method selected only if messages are near buttons + const messagesWrapper = document.querySelector(this.gateway.messages.wrapper); + if (getCurrentPaymentMethod() !== PaymentMethods.PAYPAL && + messagesWrapper && jQuery(messagesWrapper).closest('.ppc-button-wrapper').length + ) { + return false; + } + + return !PayPalCommerceGateway.is_free_trial_cart; } disableCreditCardFields() { diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js index 85f1475e6..b1261a466 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js @@ -28,15 +28,9 @@ class MessageRenderer { return; } - const newWrapper = document.createElement('div'); - newWrapper.setAttribute('id', this.config.wrapper.replace('#', '')); + const wrapper = document.querySelector(this.config.wrapper); this.currentNumber++; - newWrapper.setAttribute('data-render-number', this.currentNumber); - - const oldWrapper = document.querySelector(this.config.wrapper); - const sibling = oldWrapper.nextSibling; - oldWrapper.parentElement.removeChild(oldWrapper); - sibling.parentElement.insertBefore(newWrapper, sibling); + wrapper.setAttribute('data-render-number', this.currentNumber); widgetBuilder.registerMessages(this.config.wrapper, options); widgetBuilder.renderMessages(this.config.wrapper); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js index 07d7c057c..b094f6bdf 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js @@ -27,6 +27,7 @@ class WidgetBuilder { setPaypal(paypal) { this.paypal = paypal; + jQuery(document).trigger('ppcp-paypal-loaded', paypal); } registerButtons(wrapper, options) { @@ -84,11 +85,14 @@ class WidgetBuilder { return; } + const entry = this.messages.get(wrapper); + if (this.hasRendered(wrapper)) { + const element = document.querySelector(wrapper); + element.setAttribute('data-pp-amount', entry.options.amount); return; } - const entry = this.messages.get(wrapper); const btn = this.paypal.Messages(entry.options); btn.render(entry.wrapper); @@ -174,4 +178,5 @@ class WidgetBuilder { } } -export default new WidgetBuilder(); +window.widgetBuilder = window.widgetBuilder || new WidgetBuilder(); +export default window.widgetBuilder; diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 97a203276..2c2762970 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use WC_Order; use WC_Product; use WC_Product_Variation; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; @@ -382,54 +383,48 @@ class SmartButton implements SmartButtonInterface { * Registers the hooks to render the credit messaging HTML depending on the settings. * * @return bool - * @throws NotFoundException When a setting was not found. */ private function render_message_wrapper_registrar(): bool { if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) { return false; } - $selected_locations = $this->settings->has( 'pay_later_messaging_locations' ) ? $this->settings->get( 'pay_later_messaging_locations' ) : array(); + $location = $this->location(); - $not_enabled_on_cart = ! in_array( 'cart', $selected_locations, true ); + if ( ! $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ) ) { + return false; + } + + $get_hook = function ( string $location ): ?array { + switch ( $location ) { + case 'checkout': + return $this->messages_renderer_hook( $location, 'woocommerce_review_order_before_payment', 10 ); + case 'cart': + return $this->messages_renderer_hook( $location, $this->proceed_to_checkout_button_renderer_hook(), 19 ); + case 'pay-now': + return $this->messages_renderer_hook( 'pay_order', 'woocommerce_pay_order_before_submit', 10 ); + case 'product': + return $this->messages_renderer_hook( $location, $this->single_product_renderer_hook(), 30 ); + case 'shop': + return $this->messages_renderer_hook( $location, 'woocommerce_archive_description', 10 ); + case 'home': + return $this->messages_renderer_hook( $location, 'loop_start', 20 ); + default: + return null; + } + }; + + $hook = $get_hook( $location ); + if ( ! $hook ) { + return false; + } add_action( - $this->proceed_to_checkout_button_renderer_hook(), - function() use ( $not_enabled_on_cart ) { - if ( ! is_cart() || $not_enabled_on_cart ) { - return; - } - $this->message_renderer(); - }, - 19 + $hook['name'], + array( $this, 'message_renderer' ), + $hook['priority'] ); - $not_enabled_on_product_page = ! in_array( 'product', $selected_locations, true ); - if ( - ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) - && ! $not_enabled_on_product_page - && ! is_checkout() - ) { - add_action( - $this->single_product_renderer_hook(), - array( $this, 'message_renderer' ), - 30 - ); - } - - $not_enabled_on_checkout = ! in_array( 'checkout', $selected_locations, true ); - if ( ! $not_enabled_on_checkout ) { - add_action( - $this->checkout_dcc_button_renderer_hook(), - array( $this, 'message_renderer' ), - 11 - ); - add_action( - $this->pay_order_renderer_hook(), - array( $this, 'message_renderer' ), - 15 - ); - } return true; } @@ -528,8 +523,8 @@ class SmartButton implements SmartButtonInterface { * Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded. */ public function should_load_ppcp_script(): bool { - $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); - if ( ! $buttons_enabled ) { + $pcp_gateway_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); + if ( ! $pcp_gateway_enabled ) { return false; } @@ -537,37 +532,65 @@ class SmartButton implements SmartButtonInterface { return false; } - return $this->should_load_buttons() || $this->can_render_dcc(); + return $this->should_load_buttons() || $this->should_load_messages() || $this->can_render_dcc(); } /** * Determines whether the button component should be loaded. */ public function should_load_buttons() : bool { - $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); - if ( ! $buttons_enabled ) { + $pcp_gateway_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); + if ( ! $pcp_gateway_enabled ) { return false; } $smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() ); $smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ); - $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() ); switch ( $this->context() ) { case 'checkout': case 'cart': case 'pay-now': - return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location; case 'checkout-block': case 'cart-block': return $smart_button_enabled_for_current_location; case 'product': - return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location || $smart_button_enabled_for_mini_cart; + return $smart_button_enabled_for_current_location || $smart_button_enabled_for_mini_cart; default: return $smart_button_enabled_for_mini_cart; } } + /** + * Determines whether the Pay Later messages component should be loaded. + */ + public function should_load_messages() : bool { + $pcp_gateway_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); + if ( ! $pcp_gateway_enabled ) { + return false; + } + + if ( ! $this->messages_apply->for_country() || $this->is_free_trial_cart() ) { + return false; + } + + $location = $this->location(); + + $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $location ); + + switch ( $location ) { + case 'checkout': + case 'cart': + case 'pay-now': + case 'product': + case 'shop': + case 'home': + return $messaging_enabled_for_current_location; + default: + return false; + } + } + /** * Whether DCC fields can be rendered. */ @@ -630,8 +653,24 @@ class SmartButton implements SmartButtonInterface { // The wrapper is needed for the loading spinner, // otherwise jQuery block() prevents buttons rendering. echo '
    '; + + $hook_gateway_id = str_replace( '-', '_', $gateway_id ); + /** + * A hook executed after rendering of the opening tag for the PCP wrapper (before the inner wrapper for the buttons). + * + * For the PayPal gateway the hook name is ppcp_start_button_wrapper_ppcp_gateway. + */ + do_action( 'ppcp_start_button_wrapper_' . $hook_gateway_id ); + echo '
    '; + /** + * A hook executed before rendering of the closing tag for the PCP wrapper (before the inner wrapper for the buttons). + * + * For the PayPal gateway the hook name is ppcp_end_button_wrapper_ppcp_gateway. + */ + do_action( 'ppcp_end_button_wrapper_' . $hook_gateway_id ); + if ( null !== $action_name ) { do_action( $action_name ); } @@ -646,8 +685,10 @@ class SmartButton implements SmartButtonInterface { $product = wc_get_product(); + $location = $this->location(); + if ( - ! is_checkout() && is_a( $product, WC_Product::class ) + $location === 'product' && is_a( $product, WC_Product::class ) /** * The filter returning true if PayPal buttons can be rendered, or false otherwise. */ @@ -663,24 +704,47 @@ class SmartButton implements SmartButtonInterface { * The values for the credit messaging. * * @return array - * @throws NotFoundException When a setting was not found. */ private function message_values(): array { if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) { return array(); } - $placement = is_checkout() ? 'payment' : ( is_cart() ? 'cart' : 'product' ); - $product = wc_get_product(); - $amount = ( is_a( $product, WC_Product::class ) ) ? wc_get_price_including_tax( $product ) : 0; + $location = $this->location(); + + switch ( $location ) { + case 'checkout': + case 'checkout-block': + case 'pay-now': + $placement = 'payment'; + break; + case 'cart': + case 'cart-block': + $placement = 'cart'; + break; + case 'product': + $placement = 'product'; + break; + case 'shop': + $placement = 'product-list'; + break; + case 'home': + $placement = 'home'; + break; + default: + $placement = 'payment'; + break; + } + + $product = wc_get_product(); + $amount = ( is_a( $product, WC_Product::class ) ) ? wc_get_price_including_tax( $product ) : 0; if ( is_checkout() || is_cart() ) { $amount = WC()->cart->get_total( 'raw' ); } $styling_per_location = $this->settings->has( 'pay_later_enable_styling_per_messaging_location' ) && $this->settings->get( 'pay_later_enable_styling_per_messaging_location' ); - $per_location = is_checkout() ? 'checkout' : ( is_cart() ? 'cart' : 'product' ); - $location = $styling_per_location ? $per_location : 'general'; + $location = $styling_per_location ? $location : 'general'; $setting_name_prefix = "pay_later_{$location}_message"; $layout = $this->settings->has( "{$setting_name_prefix}_layout" ) ? $this->settings->get( "{$setting_name_prefix}_layout" ) : 'text'; @@ -975,6 +1039,10 @@ class SmartButton implements SmartButtonInterface { 'funding_sources_without_redirect' => $this->funding_sources_without_redirect, ); + if ( 'pay-now' === $this->context() ) { + $localize['pay_now'] = $this->pay_now_script_data(); + } + if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) { $localize['button']['mini_cart_style']['tagline'] = false; } @@ -995,6 +1063,32 @@ class SmartButton implements SmartButtonInterface { return $localize; } + /** + * Returns pay-now payment data. + * + * @return array + */ + private function pay_now_script_data(): array { + $order_id = $this->get_order_pay_id(); + $base_location = wc_get_base_location(); + $shop_country_code = $base_location['country'] ?? ''; + $currency_code = get_woocommerce_currency(); + + $wc_order = wc_get_order( $order_id ); + if ( ! $wc_order instanceof WC_Order ) { + return array(); + } + + $total = (float) $wc_order->get_total( 'numeric' ); + + return array( + 'total' => $total, + 'total_str' => ( new Money( $total, $currency_code ) )->value_str(), + 'currency_code' => $currency_code, + 'country_code' => $shop_country_code, + ); + } + /** * If we can find the payer data for a current customer, we will return it. * @@ -1163,10 +1257,7 @@ class SmartButton implements SmartButtonInterface { $components[] = 'buttons'; $components[] = 'funding-eligibility'; } - if ( - $this->messages_apply->for_country() - && ! $this->is_free_trial_cart() - ) { + if ( $this->should_load_messages() ) { $components[] = 'messages'; } if ( $this->dcc_is_enabled() ) { @@ -1176,7 +1267,6 @@ class SmartButton implements SmartButtonInterface { * Filter to add further components from the extensions. * * @internal Matches filter name in APM extension. - * @since TODO * * @param array $components The array of components already registered. */ @@ -1300,6 +1390,35 @@ class SmartButton implements SmartButtonInterface { return (string) apply_filters( 'woocommerce_paypal_payments_pay_order_dcc_renderer_hook', 'woocommerce_pay_order_after_submit' ); } + /** + * Returns the action name that will be used for rendering Pay Later messages. + * + * @param string $location The location name like 'checkout', 'shop'. See render_message_wrapper_registrar. + * @param string $default_hook The default name of the hook. + * @param int $default_priority The default priority of the hook. + * @return array An array with 'name' and 'priority' keys. + */ + private function messages_renderer_hook( string $location, string $default_hook, int $default_priority ): array { + /** + * The filter returning the action name that will be used for rendering Pay Later messages. + */ + $hook = (string) apply_filters( + "woocommerce_paypal_payments_${location}_messages_renderer_hook", + $default_hook + ); + /** + * The filter returning the action priority that will be used for rendering Pay Later messages. + */ + $priority = (int) apply_filters( + "woocommerce_paypal_payments_${location}_messages_renderer_priority", + $default_priority + ); + return array( + 'name' => $hook, + 'priority' => $priority, + ); + } + /** * Returns action name that PayPal button will use for rendering next to Proceed to checkout button (normally displayed in cart). * diff --git a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php index dbf18e67b..83d4c74cf 100644 --- a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php @@ -76,7 +76,7 @@ class CartScriptParamsEndpoint implements EndpointInterface { // Shop settings. $base_location = wc_get_base_location(); - $shop_country_code = $base_location['country']; + $shop_country_code = $base_location['country'] ?? ''; $currency_code = get_woocommerce_currency(); wp_send_json_success( diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php index 77de2a078..189796b75 100644 --- a/modules/ppcp-button/src/Helper/ContextTrait.php +++ b/modules/ppcp-button/src/Helper/ContextTrait.php @@ -12,6 +12,27 @@ namespace WooCommerce\PayPalCommerce\Button\Helper; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; trait ContextTrait { + /** + * Checks WC is_checkout() + WC checkout ajax requests. + */ + private function is_checkout(): bool { + if ( is_checkout() ) { + return true; + } + + /** + * The filter returning whether to detect WC checkout ajax requests. + */ + if ( apply_filters( 'ppcp_check_ajax_checkout', true ) ) { + // phpcs:ignore WordPress.Security + $wc_ajax = $_GET['wc-ajax'] ?? ''; + if ( in_array( $wc_ajax, array( 'update_order_review' ), true ) ) { + return true; + } + } + + return false; + } /** * The current context. @@ -23,7 +44,7 @@ trait ContextTrait { // Do this check here instead of reordering outside conditions. // In order to have more control over the context. - if ( ( is_checkout() ) && ! $this->is_paypal_continuation() ) { + if ( $this->is_checkout() && ! $this->is_paypal_continuation() ) { return 'checkout'; } @@ -47,13 +68,35 @@ trait ContextTrait { return 'checkout-block'; } - if ( ( is_checkout() ) && ! $this->is_paypal_continuation() ) { + if ( $this->is_checkout() && ! $this->is_paypal_continuation() ) { return 'checkout'; } return 'mini-cart'; } + /** + * The current location. + * + * @return string + */ + protected function location(): string { + $context = $this->context(); + if ( $context !== 'mini-cart' ) { + return $context; + } + + if ( is_shop() ) { + return 'shop'; + } + + if ( is_front_page() ) { + return 'home'; + } + + return ''; + } + /** * Checks if PayPal payment was already initiated (on the product or cart pages). * diff --git a/modules/ppcp-googlepay/extensions.php b/modules/ppcp-googlepay/extensions.php index 05a57284a..18c8955be 100644 --- a/modules/ppcp-googlepay/extensions.php +++ b/modules/ppcp-googlepay/extensions.php @@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\Googlepay\Helper\PropertiesDictionary; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; return array( @@ -24,6 +25,9 @@ return array( return $fields; } + $is_available = $container->get( 'googlepay.available' ); + $is_referral = $container->get( 'googlepay.is_referral' ); + $insert_after = function( array $array, string $key, array $new ): array { $keys = array_keys( $array ); $index = array_search( $key, $keys, true ); @@ -35,6 +39,66 @@ return array( $display_manager = $container->get( 'wcgateway.display-manager' ); assert( $display_manager instanceof DisplayManager ); + // Connection tab fields. + $fields = $insert_after( + $fields, + 'ppcp_dcc_status', + array( + 'googlepay_status' => array( + 'title' => __( 'Google Pay Payments', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-text', + 'text' => $container->get( 'googlepay.settings.connection.status-text' ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, + ), + ) + ); + + if ( ! $is_available && $is_referral ) { + $connection_url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading' ); + $connection_link = ''; + return $insert_after( + $fields, + 'allow_card_button_gateway', + array( + 'googlepay_button_enabled' => array( + 'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'class' => array( 'ppcp-grayed-out-text' ), + 'input_class' => array( 'ppcp-disabled-checkbox' ), + 'label' => __( 'Enable Google Pay button', 'woocommerce-paypal-payments' ) + . '

    ' + . sprintf( + // translators: %1$s and %2$s are the opening and closing of HTML tag. + __( 'Your PayPal account %1$srequires additional permissions%2$s to enable Google Pay.', 'woocommerce-paypal-payments' ), + $connection_link, + '' + ) + . '

    ', + 'default' => 'yes', + 'screens' => array( State::STATE_ONBOARDED ), + 'gateway' => 'paypal', + 'requirements' => array(), + 'custom_attributes' => array( + 'data-ppcp-display' => wp_json_encode( + array( + $display_manager + ->rule() + ->condition_is_true( false ) + ->action_enable( 'googlepay_button_enabled' ) + ->to_array(), + ) + ), + ), + ), + ) + ); + } + + // Standard Payments tab fields. return $insert_after( $fields, 'allow_card_button_gateway', diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js index 73f4785c9..1074c94ec 100644 --- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js @@ -10,6 +10,10 @@ class BaseHandler { this.externalHandler = externalHandler; } + shippingAllowed() { + return true; + } + transactionInfo() { return new Promise((resolve, reject) => { diff --git a/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js b/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js index 9295c4302..3d24df9aa 100644 --- a/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/CheckoutBlockHandler.js @@ -2,6 +2,10 @@ import BaseHandler from "./BaseHandler"; class CheckoutBlockHandler extends BaseHandler{ + shippingAllowed() { + return false; + } + createOrder() { return this.externalHandler.createOrder(); } diff --git a/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js b/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js index ed8323a60..3f773a875 100644 --- a/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js +++ b/modules/ppcp-googlepay/resources/js/Context/CheckoutHandler.js @@ -6,6 +6,10 @@ import FormValidator from "../../../../ppcp-button/resources/js/modules/Helper/F class CheckoutHandler extends BaseHandler { + shippingAllowed() { + return false; + } + transactionInfo() { return new Promise(async (resolve, reject) => { diff --git a/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js b/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js index 4d2db4260..8c6bc261d 100644 --- a/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js +++ b/modules/ppcp-googlepay/resources/js/Context/ContextHandlerFactory.js @@ -4,6 +4,8 @@ import CheckoutHandler from "./CheckoutHandler"; import CartBlockHandler from "./CartBlockHandler"; import CheckoutBlockHandler from "./CheckoutBlockHandler"; import MiniCartHandler from "./MiniCartHandler"; +import PayNowHandler from "./PayNowHandler"; +import PreviewHandler from "./PreviewHandler"; class ContextHandlerFactory { @@ -14,14 +16,17 @@ class ContextHandlerFactory { case 'cart': return new CartHandler(buttonConfig, ppcpConfig, externalActionHandler); case 'checkout': - case 'pay-now': // same as checkout return new CheckoutHandler(buttonConfig, ppcpConfig, externalActionHandler); + case 'pay-now': + return new PayNowHandler(buttonConfig, ppcpConfig, externalActionHandler); case 'mini-cart': return new MiniCartHandler(buttonConfig, ppcpConfig, externalActionHandler); case 'cart-block': return new CartBlockHandler(buttonConfig, ppcpConfig, externalActionHandler); case 'checkout-block': return new CheckoutBlockHandler(buttonConfig, ppcpConfig, externalActionHandler); + case 'preview': + return new PreviewHandler(buttonConfig, ppcpConfig, externalActionHandler); } } } diff --git a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js new file mode 100644 index 000000000..add275608 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js @@ -0,0 +1,35 @@ +import Spinner from "../../../../ppcp-button/resources/js/modules/Helper/Spinner"; +import BaseHandler from "./BaseHandler"; +import CheckoutActionHandler + from "../../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler"; + +class PayNowHandler extends BaseHandler { + + shippingAllowed() { + return false; + } + + transactionInfo() { + return new Promise(async (resolve, reject) => { + const data = this.ppcpConfig['pay_now']; + + resolve({ + countryCode: data.country_code, + currencyCode: data.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: data.total_str + }); + }); + } + + actionHandler() { + return new CheckoutActionHandler( + this.ppcpConfig, + this.errorHandler(), + new Spinner() + ); + } + +} + +export default PayNowHandler; diff --git a/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js new file mode 100644 index 000000000..a637f078d --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Context/PreviewHandler.js @@ -0,0 +1,31 @@ +import BaseHandler from "./BaseHandler"; + +class CartHandler extends BaseHandler { + + constructor(buttonConfig, ppcpConfig, externalHandler) { + super(buttonConfig, ppcpConfig, externalHandler); + } + + transactionInfo() { + throw new Error('Transaction info fail. This is just a preview.'); + } + + createOrder() { + throw new Error('Create order fail. This is just a preview.'); + } + + approveOrder(data, actions) { + throw new Error('Approve order fail. This is just a preview.'); + } + + actionHandler() { + throw new Error('Action handler fail. This is just a preview.'); + } + + errorHandler() { + throw new Error('Error handler fail. This is just a preview.'); + } + +} + +export default CartHandler; diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 6cd97302f..57d29a75d 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -1,6 +1,8 @@ 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"; +import UpdatePaymentData from "./Helper/UpdatePaymentData"; class GooglepayButton { @@ -21,7 +23,11 @@ class GooglepayButton { this.externalHandler ); - console.log('[GooglePayButton] new Button', this); + this.log = function() { + if ( this.buttonConfig.is_debug ) { + console.log('[GooglePayButton]', ...arguments); + } + } } init(config) { @@ -54,6 +60,15 @@ class GooglepayButton { }); } + reinit() { + if (!this.googlePayConfig) { + return; + } + + this.isInitialized = false; + this.init(this.googlePayConfig); + } + validateConfig() { if ( ['PRODUCTION', 'TEST'].indexOf(this.buttonConfig.environment) === -1) { console.error('[GooglePayButton] Invalid environment.', this.buttonConfig.environment); @@ -99,13 +114,18 @@ class GooglepayButton { } initClient() { + const callbacks = { + onPaymentAuthorized: this.onPaymentAuthorized.bind(this) + } + + if ( this.buttonConfig.shipping.enabled && this.contextHandler.shippingAllowed() ) { + callbacks['onPaymentDataChanged'] = this.onPaymentDataChanged.bind(this); + } + this.paymentsClient = new google.payments.api.PaymentsClient({ environment: this.buttonConfig.environment, // add merchant info maybe - paymentDataCallbacks: { - //onPaymentDataChanged: onPaymentDataChanged, - onPaymentAuthorized: this.onPaymentAuthorized.bind(this), - } + paymentDataCallbacks: callbacks }); } @@ -137,22 +157,41 @@ class GooglepayButton { * Add a Google Pay purchase button */ addButton(baseCardPaymentMethod) { - console.log('[GooglePayButton] addButton', this.context); + this.log('addButton', this.context); const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig(); - jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape); + this.waitForWrapper(wrapper, () => { + jQuery(wrapper).addClass('ppcp-button-' + ppcpStyle.shape); - const button = - this.paymentsClient.createButton({ - onClick: this.onButtonClick.bind(this), - allowedPaymentMethods: [baseCardPaymentMethod], - buttonColor: buttonStyle.color || 'black', - buttonType: buttonStyle.type || 'pay', - buttonLocale: buttonStyle.language || 'en', - buttonSizeMode: 'fill', - }); - jQuery(wrapper).append(button); + const button = + this.paymentsClient.createButton({ + onClick: this.onButtonClick.bind(this), + allowedPaymentMethods: [baseCardPaymentMethod], + buttonColor: buttonStyle.color || 'black', + buttonType: buttonStyle.type || 'pay', + buttonLocale: buttonStyle.language || 'en', + buttonSizeMode: 'fill', + }); + + jQuery(wrapper).append(button); + }); + } + + waitForWrapper(selector, callback, delay = 100, timeout = 2000) { + const startTime = Date.now(); + const interval = setInterval(() => { + const el = document.querySelector(selector); + const timeElapsed = Date.now() - startTime; + + if (el) { + clearInterval(interval); + callback(el); + } else if (timeElapsed > timeout) { + clearInterval(interval); + console.error('Waiting for wrapper timed out.', selector); + } + }, delay); } //------------------------ @@ -163,10 +202,10 @@ class GooglepayButton { * Show Google Pay payment sheet when Google Pay payment button is clicked */ async onButtonClick() { - console.log('[GooglePayButton] onButtonClick', this.context); + this.log('onButtonClick', this.context); const paymentDataRequest = await this.paymentDataRequest(); - console.log('[GooglePayButton] onButtonClick: paymentDataRequest', paymentDataRequest, this.context); + this.log('onButtonClick: paymentDataRequest', paymentDataRequest, this.context); window.ppcpFundingSource = 'googlepay'; // Do this on another place like on create order endpoint handler. @@ -184,35 +223,111 @@ class GooglepayButton { paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods; paymentDataRequest.transactionInfo = await this.contextHandler.transactionInfo(); paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo; - paymentDataRequest.callbackIntents = ['PAYMENT_AUTHORIZATION']; + + if ( this.buttonConfig.shipping.enabled && this.contextHandler.shippingAllowed() ) { + paymentDataRequest.callbackIntents = ["SHIPPING_ADDRESS", "SHIPPING_OPTION", "PAYMENT_AUTHORIZATION"]; + paymentDataRequest.shippingAddressRequired = true; + paymentDataRequest.shippingAddressParameters = this.shippingAddressParameters(); + paymentDataRequest.shippingOptionRequired = true; + } else { + paymentDataRequest.callbackIntents = ['PAYMENT_AUTHORIZATION']; + } + return paymentDataRequest; } + //------------------------ + // Shipping processing + //------------------------ + + shippingAddressParameters() { + return { + allowedCountryCodes: this.buttonConfig.shipping.countries, + phoneNumberRequired: true + }; + } + + onPaymentDataChanged(paymentData) { + this.log('onPaymentDataChanged', this.context); + this.log('paymentData', paymentData); + + return new Promise(async (resolve, reject) => { + let paymentDataRequestUpdate = {}; + + const updatedData = await (new UpdatePaymentData(this.buttonConfig.ajax.update_payment_data)).update(paymentData); + const transactionInfo = await this.contextHandler.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; + + // Handle unserviceable address. + if(!updatedData.shipping_options || !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; + } + + resolve(paymentDataRequestUpdate); + }); + } + + unserviceableShippingAddressError() { + return { + reason: "SHIPPING_ADDRESS_UNSERVICEABLE", + message: "Cannot ship to the selected address", + intent: "SHIPPING_ADDRESS" + }; + } + + calculateNewTransactionInfo(updatedData) { + return { + countryCode: updatedData.country_code, + currencyCode: updatedData.currency_code, + totalPriceStatus: 'FINAL', + totalPrice: updatedData.total_str + }; + } + //------------------------ // Payment process //------------------------ onPaymentAuthorized(paymentData) { - console.log('[GooglePayButton] onPaymentAuthorized', this.context); + this.log('onPaymentAuthorized', this.context); return this.processPayment(paymentData); } async processPayment(paymentData) { - console.log('[GooglePayButton] processPayment', this.context); + this.log('processPayment', this.context); return new Promise(async (resolve, reject) => { try { let id = await this.contextHandler.createOrder(); - console.log('[GooglePayButton] processPayment: createOrder', id, this.context); + this.log('processPayment: createOrder', id, this.context); - const confirmOrderResponse = await paypal.Googlepay().confirmOrder({ + const confirmOrderResponse = await widgetBuilder.paypal.Googlepay().confirmOrder({ orderId: id, paymentMethodData: paymentData.paymentMethodData }); - console.log('[GooglePayButton] processPayment: confirmOrder', confirmOrderResponse, this.context); + this.log('processPayment: confirmOrder', confirmOrderResponse, this.context); /** Capture the Order on the Server */ if (confirmOrderResponse.status === "APPROVED") { @@ -259,7 +374,7 @@ class GooglepayButton { } } - console.log('[GooglePayButton] processPaymentResponse', response, this.context); + this.log('processPaymentResponse', response, this.context); return response; } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js index 95aac2c2d..72475cfe5 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js @@ -38,6 +38,12 @@ class GooglepayManager { })(); } + reinit() { + for (const button of this.buttons) { + button.reinit(); + } + } + } export default GooglepayManager; diff --git a/modules/ppcp-googlepay/resources/js/Helper/UpdatePaymentData.js b/modules/ppcp-googlepay/resources/js/Helper/UpdatePaymentData.js new file mode 100644 index 000000000..3d56d9316 --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/Helper/UpdatePaymentData.js @@ -0,0 +1,38 @@ + +class UpdatePaymentData { + + constructor(config) { + this.config = config; + } + + update(paymentData) { + return new Promise((resolve, reject) => { + fetch( + this.config.endpoint, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: JSON.stringify({ + nonce: this.config.nonce, + paymentData: paymentData, + }) + + } + ) + .then(result => result.json()) + .then(result => { + if (!result.success) { + return; + } + + resolve(result.data); + }); + }); + } + +} + +export default UpdatePaymentData; diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js new file mode 100644 index 000000000..577733b6b --- /dev/null +++ b/modules/ppcp-googlepay/resources/js/boot-admin.js @@ -0,0 +1,146 @@ +import {loadCustomScript} from "@paypal/paypal-js"; +import GooglepayButton from "./GooglepayButton"; +import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder"; + +(function ({ + buttonConfig, + jQuery +}) { + + let googlePayConfig; + let buttonQueue = []; + let activeButtons = {}; + let bootstrapped = false; + + // React to PayPal config changes. + jQuery(document).on('ppcp_paypal_render_preview', (ev, ppcpConfig) => { + if (bootstrapped) { + createButton(ppcpConfig); + } else { + buttonQueue.push({ + ppcpConfig: JSON.parse(JSON.stringify(ppcpConfig)) + }); + } + }); + + // React to GooglePay config changes. + jQuery([ + '#ppcp-googlepay_button_enabled', + '#ppcp-googlepay_button_type', + '#ppcp-googlepay_button_color', + '#ppcp-googlepay_button_language', + '#ppcp-googlepay_button_shipping_enabled' + ].join(',')).on('change', () => { + for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { + createButton(ppcpConfig); + } + }); + + // Maybe we can find a more elegant reload method when transitioning from styling modes. + jQuery([ + '#ppcp-smart_button_enable_styling_per_location' + ].join(',')).on('change', () => { + setTimeout(() => { + for (const [selector, ppcpConfig] of Object.entries(activeButtons)) { + createButton(ppcpConfig); + } + }, 100); + }); + + const applyConfigOptions = function (buttonConfig) { + buttonConfig.button = buttonConfig.button || {}; + buttonConfig.button.style = buttonConfig.button.style || {}; + buttonConfig.button.style.type = jQuery('#ppcp-googlepay_button_type').val(); + buttonConfig.button.style.color = jQuery('#ppcp-googlepay_button_color').val(); + buttonConfig.button.style.language = jQuery('#ppcp-googlepay_button_language').val(); + } + + const createButton = function (ppcpConfig) { + const selector = ppcpConfig.button.wrapper + 'GooglePay'; + + if (!jQuery('#ppcp-googlepay_button_enabled').is(':checked')) { + jQuery(selector).remove(); + return; + } + + buttonConfig = JSON.parse(JSON.stringify(buttonConfig)); + buttonConfig.button.wrapper = selector; + applyConfigOptions(buttonConfig); + + const wrapperElement = `
    `; + + if (!jQuery(selector).length) { + jQuery(ppcpConfig.button.wrapper).after(wrapperElement); + } else { + jQuery(selector).replaceWith(wrapperElement); + } + + const button = new GooglepayButton( + 'preview', + null, + buttonConfig, + ppcpConfig, + ); + + button.init(googlePayConfig); + + activeButtons[selector] = ppcpConfig; + } + + const bootstrap = async function () { + if (!widgetBuilder.paypal) { + return; + } + + googlePayConfig = await widgetBuilder.paypal.Googlepay().config(); + + // We need to set bootstrapped here otherwise googlePayConfig may not be set. + bootstrapped = true; + + let options; + while (options = buttonQueue.pop()) { + createButton(options.ppcpConfig); + } + }; + + document.addEventListener( + 'DOMContentLoaded', + () => { + + if (typeof (buttonConfig) === 'undefined') { + console.error('PayPal button could not be configured.'); + return; + } + + let paypalLoaded = false; + let googlePayLoaded = false; + + const tryToBoot = () => { + if (!bootstrapped && paypalLoaded && googlePayLoaded) { + bootstrap(); + } + } + + // Load GooglePay SDK + loadCustomScript({ url: buttonConfig.sdk_url }).then(() => { + googlePayLoaded = true; + tryToBoot(); + }); + + // Wait for PayPal to be loaded externally + if (typeof widgetBuilder.paypal !== 'undefined') { + paypalLoaded = true; + tryToBoot(); + } + + jQuery(document).on('ppcp-paypal-loaded', () => { + paypalLoaded = true; + tryToBoot(); + }); + }, + ); + +})({ + buttonConfig: window.wc_ppcp_googlepay_admin, + jQuery: window.jQuery +}); diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js index e6390c8dd..387822e67 100644 --- a/modules/ppcp-googlepay/resources/js/boot.js +++ b/modules/ppcp-googlepay/resources/js/boot.js @@ -8,11 +8,17 @@ import GooglepayManager from "./GooglepayManager"; jQuery }) { + let manager; + const bootstrap = function () { - const manager = new GooglepayManager(buttonConfig, ppcpConfig); + manager = new GooglepayManager(buttonConfig, ppcpConfig); manager.init(); }; + jQuery(document.body).on('updated_cart_totals updated_checkout', () => { + manager.reinit(); + }); + document.addEventListener( 'DOMContentLoaded', () => { @@ -20,12 +26,7 @@ import GooglepayManager from "./GooglepayManager"; (typeof (buttonConfig) === 'undefined') || (typeof (ppcpConfig) === 'undefined') ) { - console.error('PayPal button could not be configured.'); - return; - } - - // If button wrapper is not present then there is no need to load the scripts. - if (!jQuery(buttonConfig.button.wrapper).length) { + // No PayPal buttons present on this page. return; } diff --git a/modules/ppcp-googlepay/services.php b/modules/ppcp-googlepay/services.php index a7944a952..02a9ccc9d 100644 --- a/modules/ppcp-googlepay/services.php +++ b/modules/ppcp-googlepay/services.php @@ -11,10 +11,15 @@ namespace WooCommerce\PayPalCommerce\Googlepay; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; +use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator; use WooCommerce\PayPalCommerce\Googlepay\Assets\BlocksPaymentMethod; use WooCommerce\PayPalCommerce\Googlepay\Assets\Button; +use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint; use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmApplies; use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus; +use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice; +use WooCommerce\PayPalCommerce\Onboarding\Environment; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; return array( @@ -35,9 +40,9 @@ return array( ); }, - // If GooglePay is configured. + // If GooglePay is configured and onboarded. 'googlepay.available' => static function ( ContainerInterface $container ): bool { - if ( apply_filters( 'woocommerce_paypal_payments_googlepay_validate_product_status', false ) ) { + if ( apply_filters( 'woocommerce_paypal_payments_googlepay_validate_product_status', true ) ) { $status = $container->get( 'googlepay.helpers.apm-product-status' ); assert( $status instanceof ApmProductStatus ); /** @@ -48,14 +53,33 @@ return array( return true; }, - 'googlepay.helpers.apm-product-status' => static function( ContainerInterface $container ): ApmProductStatus { - return new ApmProductStatus( - $container->get( 'wcgateway.settings' ), - $container->get( 'api.endpoint.partners' ), - $container->get( 'onboarding.state' ) + // We assume it's a referral if we can check product status without API request failures. + '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 { + return new AvailabilityNotice( + $container->get( 'googlepay.helpers.apm-product-status' ), + $container->get( 'wcgateway.is-wc-gateways-list-page' ), + $container->get( 'wcgateway.is-ppcp-settings-page' ) ); }, + 'googlepay.helpers.apm-product-status' => SingletonDecorator::make( + static function( ContainerInterface $container ): ApmProductStatus { + return new ApmProductStatus( + $container->get( 'wcgateway.settings' ), + $container->get( 'api.endpoint.partners' ), + $container->get( 'onboarding.state' ), + $container->get( 'api.helper.failure-registry' ) + ); + } + ), + /** * The matrix which countries and currency combinations can be used for GooglePay. */ @@ -153,4 +177,58 @@ return array( return 'https://pay.google.com/gp/p/js/pay.js'; }, + '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 { + return 'https://www.sandbox.paypal.com/bizsignup/add-product?product=payment_methods&capabilities=GOOGLE_PAY'; + }, + + '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 { + $state = $container->get( 'onboarding.state' ); + if ( $state->current_state() < State::STATE_ONBOARDED ) { + return ''; + } + + $product_status = $container->get( 'googlepay.helpers.apm-product-status' ); + assert( $product_status instanceof ApmProductStatus ); + + $environment = $container->get( 'onboarding.environment' ); + assert( $environment instanceof Environment ); + + $enabled = $product_status->is_active(); + + $enabled_status_text = esc_html__( 'Status: Available', 'woocommerce-paypal-payments' ); + $disabled_status_text = esc_html__( 'Status: Not yet enabled', 'woocommerce-paypal-payments' ); + + $button_text = $enabled + ? esc_html__( 'Settings', 'woocommerce-paypal-payments' ) + : esc_html__( 'Enable Google Pay', 'woocommerce-paypal-payments' ); + + $enable_url = $environment->current_environment_is( Environment::PRODUCTION ) + ? $container->get( 'googlepay.enable-url-live' ) + : $container->get( 'googlepay.enable-url-sandbox' ); + + $button_url = $enabled + ? admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway#field-alternative_payment_methods' ) + : $enable_url; + + return sprintf( + '

    %1$s %2$s

    %5$s

    ', + $enabled ? $enabled_status_text : $disabled_status_text, + $enabled ? '' : '', + $enabled ? '_self' : '_blank', + esc_url( $button_url ), + esc_html( $button_text ) + ); + }, + ); diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php index 48848af56..b4ad9c297 100644 --- a/modules/ppcp-googlepay/src/Assets/Button.php +++ b/modules/ppcp-googlepay/src/Assets/Button.php @@ -11,7 +11,9 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Assets; use Exception; use Psr\Log\LoggerInterface; +use WC_Countries; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; +use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; @@ -127,6 +129,7 @@ class Button implements ButtonInterface { */ public function initialize(): void { add_filter( 'ppcp_onboarding_options', array( $this, 'add_onboarding_options' ), 10, 1 ); + add_filter( 'ppcp_partner_referrals_option', array( $this, 'filter_partner_referrals_option' ), 10, 1 ); add_filter( 'ppcp_partner_referrals_data', array( $this, 'add_partner_referrals_data' ), 10, 1 ); } @@ -139,6 +142,10 @@ class Button implements ButtonInterface { * @psalm-suppress MissingClosureParamType */ public function add_onboarding_options( $options ): string { + if ( ! apply_filters( 'woocommerce_paypal_payments_google_pay_onboarding_option', false ) ) { + return $options; + } + $checked = ''; try { $onboard_with_google = $this->settings->get( 'ppcp-onboarding-google' ); @@ -150,11 +157,28 @@ class Button implements ButtonInterface { } return $options - . '
  • '; } + /** + * Filters a partner referrals option. + * + * @param array $option The option data. + * @return array + */ + public function filter_partner_referrals_option( array $option ): array { + if ( $option['valid'] ) { + return $option; + } + if ( $option['field'] === 'ppcp-onboarding-google' ) { + $option['valid'] = true; + $option['value'] = ( $option['value'] ? '1' : '' ); + } + return $option; + } + /** * Adds to partner referrals data. * @@ -180,21 +204,17 @@ class Button implements ButtonInterface { } $data['capabilities'][] = 'GOOGLE_PAY'; - - $nonce = $data['operations'][0]['api_integration_preference']['rest_api_integration']['first_party_details']['seller_nonce']; - - $data['operations'][] = array( + $data['operations'][] = array( 'operation' => 'API_INTEGRATION', 'api_integration_preference' => array( 'rest_api_integration' => array( 'integration_method' => 'PAYPAL', 'integration_type' => 'THIRD_PARTY', 'third_party_details' => array( - 'features' => array( + 'features' => array( 'PAYMENT', 'REFUND', ), - 'seller_nonce' => $nonce, ), ), ), @@ -358,14 +378,53 @@ class Button implements ButtonInterface { ); } + /** + * Enqueues scripts/styles for admin. + */ + public function enqueue_admin(): void { + wp_register_style( + 'wc-ppcp-googlepay-admin', + untrailingslashit( $this->module_url ) . '/assets/css/styles.css', + array(), + $this->version + ); + wp_enqueue_style( 'wc-ppcp-googlepay-admin' ); + + wp_register_script( + 'wc-ppcp-googlepay-admin', + untrailingslashit( $this->module_url ) . '/assets/js/boot-admin.js', + array(), + $this->version, + true + ); + wp_enqueue_script( 'wc-ppcp-googlepay-admin' ); + + wp_localize_script( + 'wc-ppcp-googlepay-admin', + 'wc_ppcp_googlepay_admin', + $this->script_data() + ); + } + /** * The configuration for the smart buttons. * * @return array */ public function script_data(): array { + $shipping = array( + 'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' ) + ? boolval( $this->settings->get( 'googlepay_button_shipping_enabled' ) ) + : false, + ); + + if ( $shipping['enabled'] ) { + $shipping['countries'] = array_keys( $this->wc_countries()->get_shipping_countries() ); + } + return array( 'environment' => $this->environment->current_environment_is( Environment::SANDBOX ) ? 'TEST' : 'PRODUCTION', + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, 'sdk_url' => $this->sdk_url, 'button' => array( 'wrapper' => '#ppc-button-googlepay-container', @@ -373,6 +432,13 @@ class Button implements ButtonInterface { 'mini_cart_wrapper' => '#ppc-button-googlepay-container-minicart', 'mini_cart_style' => $this->button_styles_for_context( 'mini-cart' ), ), + 'shipping' => $shipping, + 'ajax' => array( + 'update_payment_data' => array( + 'endpoint' => \WC_AJAX::get_endpoint( UpdatePaymentDataEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( UpdatePaymentDataEndpoint::nonce() ), + ), + ), ); } @@ -404,4 +470,12 @@ class Button implements ButtonInterface { return $values; } + /** + * Returns a WC_Countries instance to check shipping + * + * @return WC_Countries + */ + private function wc_countries(): WC_Countries { + return new WC_Countries(); + } } diff --git a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php new file mode 100644 index 000000000..27da5ef48 --- /dev/null +++ b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php @@ -0,0 +1,219 @@ +request_data = $request_data; + $this->logger = $logger; + } + + /** + * Returns the nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws RuntimeException When a validation fails. + */ + public function handle_request(): bool { + try { + $data = $this->request_data->read_request( $this->nonce() ); + + // Validate payment data. + if ( ! isset( $data['paymentData'] ) ) { + throw new RuntimeException( + __( 'No paymentData provided.', 'woocommerce-paypal-payments' ) + ); + } + + $payment_data = $data['paymentData']; + + // Set context as cart. + if ( is_callable( 'wc_maybe_define_constant' ) ) { + wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); + } + + $this->update_addresses( $payment_data ); + $this->update_shipping_method( $payment_data ); + + WC()->cart->calculate_shipping(); + WC()->cart->calculate_fees(); + WC()->cart->calculate_totals(); + + $total = (float) WC()->cart->get_total( 'numeric' ); + + // Shop settings. + $base_location = wc_get_base_location(); + $shop_country_code = $base_location['country']; + $currency_code = get_woocommerce_currency(); + + wp_send_json_success( + array( + 'total' => $total, + 'total_str' => ( new Money( $total, $currency_code ) )->value_str(), + 'currency_code' => $currency_code, + 'country_code' => $shop_country_code, + 'shipping_options' => $this->get_shipping_options(), + ) + ); + + return true; + } catch ( Throwable $error ) { + $this->logger->error( "UpdatePaymentDataEndpoint execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" ); + + wp_send_json_error(); + return false; + } + } + + /** + * Returns the array of available shipping methods. + * + * @return array + */ + public function get_shipping_options(): array { + $shipping_options = array(); + + $calculated_packages = WC()->shipping->calculate_shipping( + WC()->cart->get_shipping_packages() + ); + + if ( ! isset( $calculated_packages[0] ) && ! isset( $calculated_packages[0]['rates'] ) ) { + return array(); + } + + foreach ( $calculated_packages[0]['rates'] as $rate ) { + /** + * The shipping rate. + * + * @var \WC_Shipping_Rate $rate + */ + $shipping_options[] = array( + 'id' => $rate->get_id(), + 'label' => $rate->get_label(), + 'description' => html_entity_decode( + wp_strip_all_tags( + wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) ) + ) + ), + ); + } + + if ( ! isset( $shipping_options[0] ) ) { + return array(); + } + + $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); + + return array( + 'defaultSelectedOptionId' => ( $chosen_shipping_methods[0] ?? null ) ? $chosen_shipping_methods[0] : $shipping_options[0]['id'], + 'shippingOptions' => $shipping_options, + ); + } + + /** + * Update addresses. + * + * @param array $payment_data The payment data. + * @return void + */ + private function update_addresses( array $payment_data ): void { + if ( ! in_array( $payment_data['callbackTrigger'] ?? '', array( 'SHIPPING_ADDRESS', 'INITIALIZE' ), true ) ) { + return; + } + + /** + * The shipping methods. + * + * @var \WC_Customer|null $customer + */ + $customer = WC()->customer; + + if ( ! $customer ) { + return; + } + + $customer->set_billing_postcode( $payment_data['shippingAddress']['postalCode'] ?? '' ); + $customer->set_billing_country( $payment_data['shippingAddress']['countryCode'] ?? '' ); + $customer->set_billing_state( '' ); + $customer->set_billing_city( $payment_data['shippingAddress']['locality'] ?? '' ); + + $customer->set_shipping_postcode( $payment_data['shippingAddress']['postalCode'] ?? '' ); + $customer->set_shipping_country( $payment_data['shippingAddress']['countryCode'] ?? '' ); + $customer->set_shipping_state( '' ); + $customer->set_shipping_city( $payment_data['shippingAddress']['locality'] ?? '' ); + + // Save the data. + $customer->save(); + + WC()->session->set( 'customer', WC()->customer->get_data() ); + } + + /** + * Update shipping method. + * + * @param array $payment_data The payment data. + * @return void + */ + private function update_shipping_method( array $payment_data ): void { + $rate_id = $payment_data['shippingOptionData']['id']; + $calculated_packages = WC()->shipping->calculate_shipping( + WC()->cart->get_shipping_packages() + ); + + if ( $rate_id && isset( $calculated_packages[0]['rates'][ $rate_id ] ) ) { + WC()->session->set( 'chosen_shipping_methods', array( $rate_id ) ); + } + } + +} diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php index 60485fcf1..86c967694 100644 --- a/modules/ppcp-googlepay/src/GooglepayModule.php +++ b/modules/ppcp-googlepay/src/GooglepayModule.php @@ -11,7 +11,9 @@ namespace WooCommerce\PayPalCommerce\Googlepay; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; +use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint; use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus; +use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; @@ -37,18 +39,39 @@ class GooglepayModule implements ModuleInterface { */ public function run( ContainerInterface $c ): void { + // Clears product status when appropriate. + add_action( + 'woocommerce_paypal_payments_clear_apm_product_status', + function( Settings $settings = null ) use ( $c ): void { + $apm_status = $c->get( 'googlepay.helpers.apm-product-status' ); + assert( $apm_status instanceof ApmProductStatus ); + + $apm_status->clear( $settings ); + } + ); + + // Check if the module is applicable, correct country, currency, ... etc. if ( ! $c->get( 'googlepay.eligible' ) ) { return; } + // Load the button handler. $button = $c->get( 'googlepay.button' ); assert( $button instanceof ButtonInterface ); $button->initialize(); - if ( ! $c->get( 'googlepay.available' ) ) { + // Show notice if there are product availability issues. + $availability_notice = $c->get( 'googlepay.availability_notice' ); + assert( $availability_notice instanceof AvailabilityNotice ); + $availability_notice->execute(); + + // Check if this merchant can activate / use the buttons. + // We allow non referral merchants as they can potentially still use GooglePay, we just have no way of checking the capability. + if ( ( ! $c->get( 'googlepay.available' ) ) && $c->get( 'googlepay.is_referral' ) ) { return; } + // Initializes button rendering. add_action( 'wp', static function () use ( $c, $button ) { @@ -59,6 +82,7 @@ class GooglepayModule implements ModuleInterface { } ); + // Enqueue frontend scripts. add_action( 'wp_enqueue_scripts', static function () use ( $c, $button ) { @@ -66,6 +90,23 @@ class GooglepayModule implements ModuleInterface { } ); + // Enqueue backend scripts. + add_action( + 'admin_enqueue_scripts', + static function () use ( $c, $button ) { + if ( ! is_admin() ) { + return; + } + /** + * Should add this to the ButtonInterface. + * + * @psalm-suppress UndefinedInterfaceMethod + */ + $button->enqueue_admin(); + } + ); + + // Registers buttons on blocks pages. add_action( 'woocommerce_blocks_payment_method_type_registration', function( PaymentMethodRegistry $payment_method_registry ) use ( $c, $button ): void { @@ -75,21 +116,26 @@ class GooglepayModule implements ModuleInterface { } ); - // Clear product status handling. + // Adds GooglePay component to the backend button preview settings. add_action( - 'woocommerce_paypal_payments_clear_apm_product_status', - function( Settings $settings = null ) use ( $c ): void { - $apm_status = $c->get( 'googlepay.helpers.apm-product-status' ); - assert( $apm_status instanceof ApmProductStatus ); - - if ( ! $settings instanceof Settings ) { - $settings = null; + 'woocommerce_paypal_payments_admin_gateway_settings', + function( array $settings ) use ( $c, $button ): array { + if ( is_array( $settings['components'] ) ) { + $settings['components'][] = 'googlepay'; } - - $apm_status->clear( $settings ); + return $settings; } ); + // Initialize AJAX endpoints. + add_action( + 'wc_ajax_' . UpdatePaymentDataEndpoint::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'googlepay.endpoint.update-payment-data' ); + assert( $endpoint instanceof UpdatePaymentDataEndpoint ); + $endpoint->handle_request(); + } + ); } /** diff --git a/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php b/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php index b6046e0e5..cf1e6487c 100644 --- a/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php +++ b/modules/ppcp-googlepay/src/Helper/ApmProductStatus.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay\Helper; use Throwable; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -32,6 +33,13 @@ class ApmProductStatus { */ private $current_status = null; + /** + * If there was a request failure. + * + * @var bool + */ + private $has_request_failure = false; + /** * The settings. * @@ -53,21 +61,31 @@ class ApmProductStatus { */ private $onboarding_state; + /** + * The API failure registry + * + * @var FailureRegistry + */ + private $api_failure_registry; + /** * ApmProductStatus constructor. * * @param Settings $settings The Settings. * @param PartnersEndpoint $partners_endpoint The Partner Endpoint. * @param State $onboarding_state The onboarding state. + * @param FailureRegistry $api_failure_registry The API failure registry. */ public function __construct( Settings $settings, PartnersEndpoint $partners_endpoint, - State $onboarding_state + State $onboarding_state, + FailureRegistry $api_failure_registry ) { - $this->settings = $settings; - $this->partners_endpoint = $partners_endpoint; - $this->onboarding_state = $onboarding_state; + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; + $this->onboarding_state = $onboarding_state; + $this->api_failure_registry = $api_failure_registry; } /** @@ -76,33 +94,47 @@ class ApmProductStatus { * @return bool */ public function is_active() : bool { - if ( $this->onboarding_state->current_state() < State::STATE_ONBOARDED ) { + + // If not onboarded then makes no sense to check status. + if ( ! $this->is_onboarded() ) { return false; } + // If status was already checked on this request return the same result. if ( null !== $this->current_status ) { return $this->current_status; } + // Check if status was checked on previous requests. if ( $this->settings->has( self::SETTINGS_KEY ) && ( $this->settings->get( self::SETTINGS_KEY ) ) ) { $this->current_status = wc_string_to_bool( $this->settings->get( self::SETTINGS_KEY ) ); return $this->current_status; } - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( Throwable $error ) { - // It may be a transitory error, don't persist the status. - $this->current_status = false; + // Check API failure registry to prevent multiple failed API requests. + if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) { + $this->has_request_failure = true; + $this->current_status = false; return $this->current_status; } + // Request seller status via PayPal API. + try { + $seller_status = $this->partners_endpoint->seller_status(); + } catch ( Throwable $error ) { + $this->has_request_failure = true; + $this->current_status = false; + return $this->current_status; + } + + // Check the seller status for the intended capability. foreach ( $seller_status->products() as $product ) { if ( $product->name() !== 'PAYMENT_METHODS' ) { continue; } if ( in_array( self::CAPABILITY_NAME, $product->capabilities(), true ) ) { + // Capability found, persist status and return true. $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_ENABLED ); $this->settings->persist(); @@ -111,6 +143,7 @@ class ApmProductStatus { } } + // Capability not found, persist status and return false. $this->settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_DISABLED ); $this->settings->persist(); @@ -118,6 +151,24 @@ class ApmProductStatus { return $this->current_status; } + /** + * Returns if the seller is onboarded. + * + * @return bool + */ + public function is_onboarded(): bool { + return $this->onboarding_state->current_state() >= State::STATE_ONBOARDED; + } + + /** + * Returns if there was a request failure. + * + * @return bool + */ + public function has_request_failure(): bool { + return $this->has_request_failure; + } + /** * Clears the persisted result to force a recheck. * @@ -132,8 +183,10 @@ class ApmProductStatus { $this->current_status = null; - $settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED ); - $settings->persist(); + if ( $settings->has( self::SETTINGS_KEY ) ) { + $settings->set( self::SETTINGS_KEY, self::SETTINGS_VALUE_UNDEFINED ); + $settings->persist(); + } } } diff --git a/modules/ppcp-googlepay/src/Helper/AvailabilityNotice.php b/modules/ppcp-googlepay/src/Helper/AvailabilityNotice.php new file mode 100644 index 000000000..9cadbc91a --- /dev/null +++ b/modules/ppcp-googlepay/src/Helper/AvailabilityNotice.php @@ -0,0 +1,159 @@ +product_status = $product_status; + $this->is_wc_gateways_list_page = $is_wc_gateways_list_page; + $this->is_ppcp_settings_page = $is_ppcp_settings_page; + } + + /** + * Adds availability notice if applicable. + * + * @return void + */ + public function execute(): void { + if ( ! $this->should_display() ) { + return; + } + + // We need to check is active before checking failure requests, otherwise failure status won't be set. + $is_active = $this->product_status->is_active(); + + if ( $this->product_status->has_request_failure() ) { + $this->add_seller_status_failure_notice(); + } elseif ( ! $is_active ) { + $this->add_not_available_notice(); + } + } + + /** + * Whether the message should display. + * + * @return bool + */ + protected function should_display(): bool { + if ( ! $this->product_status->is_onboarded() ) { + return false; + } + if ( ! $this->is_wc_gateways_list_page && ! $this->is_ppcp_settings_page ) { + return false; + } + return true; + } + + /** + * Adds seller status failure notice. + * + * @return void + */ + private function add_seller_status_failure_notice(): void { + add_filter( + Repository::NOTICES_FILTER, + /** + * Adds seller status notice. + * + * @param array $notices The notices. + * @return array + * + * @psalm-suppress MissingClosureParamType + */ + static function ( $notices ): array { + + $message = sprintf( + // translators: %1$s and %2$s are the opening and closing of HTML tag. + __( + '

    Notice: We could not determine your PayPal seller status to list your available features. Disconnect and reconnect your PayPal account through our %1$sonboarding process%2$s to resolve this.

    Don\'t worry if you cannot use the %1$sonboarding process%2$s; most functionalities available to your account should work.

    ', + 'woocommerce-paypal-payments' + ), + '
    ', + '' + ); + + // Name the key so it can be overridden in other modules. + $notices['error_product_status'] = new Message( $message, 'warning', true, 'ppcp-notice-wrapper' ); + return $notices; + } + ); + } + + /** + * Adds not available notice. + * + * @return void + */ + private function add_not_available_notice(): void { + add_filter( + Repository::NOTICES_FILTER, + /** + * Adds GooglePay not available notice. + * + * @param array $notices The notices. + * @return array + * + * @psalm-suppress MissingClosureParamType + */ + static function ( $notices ): array { + + $message = sprintf( + __( + 'Google Pay is not available on your PayPal seller account.', + 'woocommerce-paypal-payments' + ) + ); + + $notices[] = new Message( $message, 'warning', true, 'ppcp-notice-wrapper' ); + return $notices; + } + ); + } + +} diff --git a/modules/ppcp-googlepay/webpack.config.js b/modules/ppcp-googlepay/webpack.config.js index 2d14144fa..6805309e2 100644 --- a/modules/ppcp-googlepay/webpack.config.js +++ b/modules/ppcp-googlepay/webpack.config.js @@ -11,6 +11,7 @@ module.exports = { entry: { 'boot': path.resolve('./resources/js/boot.js'), 'boot-block': path.resolve('./resources/js/boot-block.js'), + 'boot-admin': path.resolve('./resources/js/boot-admin.js'), "styles": path.resolve('./resources/css/styles.scss') }, output: { diff --git a/modules/ppcp-onboarding/resources/js/onboarding.js b/modules/ppcp-onboarding/resources/js/onboarding.js index 46c962e81..57d30161b 100644 --- a/modules/ppcp-onboarding/resources/js/onboarding.js +++ b/modules/ppcp-onboarding/resources/js/onboarding.js @@ -13,84 +13,102 @@ const ppcp_onboarding = { reload: function() { const buttons = document.querySelectorAll(ppcp_onboarding.BUTTON_SELECTOR); - if (0 === buttons.length) { - return; + if (buttons.length > 0) { + // Add event listeners to buttons preventing link clicking if PayPal init failed. + buttons.forEach( + (element) => { + if (element.hasAttribute('data-ppcp-button-initialized')) { + return; + } + + element.addEventListener( + 'click', + (e) => { + if (!element.hasAttribute('data-ppcp-button-initialized') || 'undefined' === typeof window.PAYPAL) { + e.preventDefault(); + } + } + ); + } + ); + + // Clear any previous PayPal scripts. + [ppcp_onboarding.PAYPAL_JS_ID, 'signup-js', 'biz-js'].forEach( + (scriptID) => { + const scriptTag = document.getElementById(scriptID); + + if (scriptTag) { + scriptTag.parentNode.removeChild(scriptTag); + } + + if ('undefined' !== typeof window.PAYPAL) { + delete window.PAYPAL; + } + } + ); + + // Load PayPal scripts. + const paypalScriptTag = document.createElement('script'); + paypalScriptTag.id = ppcp_onboarding.PAYPAL_JS_ID; + paypalScriptTag.src = PayPalCommerceGatewayOnboarding.paypal_js_url; + document.body.appendChild(paypalScriptTag); + + if (ppcp_onboarding._timeout) { + clearTimeout(ppcp_onboarding._timeout); + } + + ppcp_onboarding._timeout = setTimeout( + () => { + buttons.forEach((element) => { element.setAttribute('data-ppcp-button-initialized', 'true'); }); + + if ('undefined' !== window.PAYPAL.apps.Signup) { + window.PAYPAL.apps.Signup.render(); + } + }, + 1000 + ); } - // Add event listeners to buttons preventing link clicking if PayPal init failed. - buttons.forEach( - (element) => { - if (element.hasAttribute('data-ppcp-button-initialized')) { - return; - } + const $onboarding_inputs = function () { + return jQuery('*[data-onboarding-option]'); + }; + const onboarding_options = function () { + let options = {}; + $onboarding_inputs().each((index, el) => { + const opt = jQuery(el).data('onboardingOption'); + options[opt] = el.checked; + }); + return options; + } + const disable_onboarding_options = function () { + $onboarding_inputs().each((index, el) => { + el.setAttribute('disabled', 'disabled'); + }); + } + const enable_onboarding_options = function () { + $onboarding_inputs().each((index, el) => { + el.removeAttribute('disabled'); + }); + } + const update_onboarding_options = function () { + const spinner = ''; - element.addEventListener( - 'click', - (e) => { - if (!element.hasAttribute('data-ppcp-button-initialized') || 'undefined' === typeof window.PAYPAL) { - e.preventDefault(); - } - } - ); - } - ); - - // Clear any previous PayPal scripts. - [ppcp_onboarding.PAYPAL_JS_ID, 'signup-js', 'biz-js'].forEach( - (scriptID) => { - const scriptTag = document.getElementById(scriptID); - - if (scriptTag) { - scriptTag.parentNode.removeChild(scriptTag); - } - - if ('undefined' !== typeof window.PAYPAL) { - delete window.PAYPAL; - } - } - ); - - // Load PayPal scripts. - const paypalScriptTag = document.createElement('script'); - paypalScriptTag.id = ppcp_onboarding.PAYPAL_JS_ID; - paypalScriptTag.src = PayPalCommerceGatewayOnboarding.paypal_js_url; - document.body.appendChild(paypalScriptTag); - - if (ppcp_onboarding._timeout) { - clearTimeout(ppcp_onboarding._timeout); - } - - ppcp_onboarding._timeout = setTimeout( - () => { - buttons.forEach((element) => { element.setAttribute('data-ppcp-button-initialized', 'true'); }); - - if ('undefined' !== window.PAYPAL.apps.Signup) { - window.PAYPAL.apps.Signup.render(); - } - }, - 1000 - ); - - const onboard_pui = document.querySelector('#ppcp-onboarding-pui'); - const spinner = ''; - onboard_pui?.addEventListener('click', (event) => { - event.preventDefault(); - onboard_pui.setAttribute('disabled', 'disabled'); + disable_onboarding_options(); buttons.forEach((element) => { element.removeAttribute('href'); element.setAttribute('disabled', 'disabled'); jQuery(spinner).insertAfter(element); }); - fetch(PayPalCommerceGatewayOnboarding.pui_endpoint, { + fetch(PayPalCommerceGatewayOnboarding.update_signup_links_endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ - nonce: PayPalCommerceGatewayOnboarding.pui_nonce, - checked: onboard_pui.checked + nonce: PayPalCommerceGatewayOnboarding.update_signup_links_nonce, + settings: onboarding_options() }) }).then((res)=>{ return res.json(); @@ -99,7 +117,6 @@ const ppcp_onboarding = { alert('Could not update signup buttons: ' + JSON.stringify(data)); return; } - buttons.forEach((element) => { for (let [key, value] of Object.entries(data.data.signup_links)) { key = 'connect-to' + key.replace(/-/g, ''); @@ -110,9 +127,13 @@ const ppcp_onboarding = { } } }); - onboard_pui.removeAttribute('disabled'); + enable_onboarding_options(); }); - }) + } + $onboarding_inputs().on('click', (event) => { + event.preventDefault(); + update_onboarding_options(); + }); }, loginSeller: function(env, authCode, sharedId) { diff --git a/modules/ppcp-onboarding/resources/js/settings.js b/modules/ppcp-onboarding/resources/js/settings.js index 44fa692ed..56b8ec82b 100644 --- a/modules/ppcp-onboarding/resources/js/settings.js +++ b/modules/ppcp-onboarding/resources/js/settings.js @@ -1,7 +1,7 @@ document.addEventListener( 'DOMContentLoaded', () => { - const payLaterMessagingSelectableLocations = ['product', 'cart', 'checkout']; + const payLaterMessagingSelectableLocations = ['product', 'cart', 'checkout', 'shop', 'home']; const payLaterMessagingAllLocations = payLaterMessagingSelectableLocations.concat('general'); const payLaterMessagingLocationsSelector = '#field-pay_later_messaging_locations'; const payLaterMessagingLocationsSelect = payLaterMessagingLocationsSelector + ' select'; @@ -9,7 +9,7 @@ document.addEventListener( const smartButtonLocationsSelector = '#field-smart_button_locations'; const smartButtonLocationsSelect = smartButtonLocationsSelector + ' select'; - const smartButtonSelectableLocations = payLaterMessagingSelectableLocations.concat('mini-cart'); + const smartButtonSelectableLocations = ['product', 'cart', 'checkout', 'mini-cart']; const groupToggle = (selector, group) => { const toggleElement = document.querySelector(selector); @@ -19,7 +19,10 @@ document.addEventListener( if (! toggleElement.checked) { group.forEach( (elementToHide) => { - document.querySelector(elementToHide).style.display = 'none'; + const element = document.querySelector(elementToHide); + if (element) { + element.style.display = 'none'; + } }) } toggleElement.addEventListener( @@ -27,7 +30,10 @@ document.addEventListener( (event) => { if (! event.target.checked) { group.forEach( (elementToHide) => { - document.querySelector(elementToHide).style.display = 'none'; + const element = document.querySelector(elementToHide); + if (element) { + element.style.display = 'none'; + } }); return; diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index 06f2d27a7..27ab1ad25 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; -use WooCommerce\PayPalCommerce\Onboarding\Endpoint\PayUponInvoiceEndpoint; +use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; @@ -187,8 +187,8 @@ return array( $logger ); }, - 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : PayUponInvoiceEndpoint { - return new PayUponInvoiceEndpoint( + 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : UpdateSignupLinksEndpoint { + return new UpdateSignupLinksEndpoint( $container->get( 'wcgateway.settings' ), $container->get( 'button.request-data' ), $container->get( 'onboarding.signup-link-cache' ), diff --git a/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php b/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php index 5cac972dc..4b0c9c165 100644 --- a/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php +++ b/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Onboarding\Assets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; +use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -145,18 +146,18 @@ class OnboardingAssets { */ public function get_script_data() { return array( - 'endpoint' => \WC_AJAX::get_endpoint( LoginSellerEndpoint::ENDPOINT ), - 'nonce' => wp_create_nonce( $this->login_seller_endpoint::nonce() ), - 'paypal_js_url' => 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js', - 'sandbox_state' => State::get_state_name( $this->state->sandbox_state() ), - 'production_state' => State::get_state_name( $this->state->production_state() ), - 'current_state' => State::get_state_name( $this->state->current_state() ), - 'current_env' => $this->environment->current_environment(), - 'error_messages' => array( + 'endpoint' => \WC_AJAX::get_endpoint( LoginSellerEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( $this->login_seller_endpoint::nonce() ), + 'paypal_js_url' => 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js', + 'sandbox_state' => State::get_state_name( $this->state->sandbox_state() ), + 'production_state' => State::get_state_name( $this->state->production_state() ), + 'current_state' => State::get_state_name( $this->state->current_state() ), + 'current_env' => $this->environment->current_environment(), + 'error_messages' => array( 'no_credentials' => __( 'API credentials must be entered to save the settings.', 'woocommerce-paypal-payments' ), ), - 'pui_endpoint' => \WC_AJAX::get_endpoint( 'ppc-pui' ), - 'pui_nonce' => wp_create_nonce( 'ppc-pui' ), + 'update_signup_links_endpoint' => \WC_AJAX::get_endpoint( UpdateSignupLinksEndpoint::ENDPOINT ), + 'update_signup_links_nonce' => wp_create_nonce( UpdateSignupLinksEndpoint::ENDPOINT ), ); } diff --git a/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php b/modules/ppcp-onboarding/src/Endpoint/UpdateSignupLinksEndpoint.php similarity index 82% rename from modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php rename to modules/ppcp-onboarding/src/Endpoint/UpdateSignupLinksEndpoint.php index bc1607aa4..f26e6dc13 100644 --- a/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php +++ b/modules/ppcp-onboarding/src/Endpoint/UpdateSignupLinksEndpoint.php @@ -14,14 +14,17 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface; use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData; +use WooCommerce\PayPalCommerce\Onboarding\Helper\OnboardingUrl; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; /** - * Class PayUponInvoiceEndpoint + * Class UpdateSignupLinksEndpoint */ -class PayUponInvoiceEndpoint implements EndpointInterface { +class UpdateSignupLinksEndpoint implements EndpointInterface { + + const ENDPOINT = 'ppc-update-signup-links'; /** * The settings. @@ -66,7 +69,7 @@ class PayUponInvoiceEndpoint implements EndpointInterface { protected $logger; /** - * PayUponInvoiceEndpoint constructor. + * UpdateSignupLinksEndpoint constructor. * * @param Settings $settings The settings. * @param RequestData $request_data The request data. @@ -97,7 +100,7 @@ class PayUponInvoiceEndpoint implements EndpointInterface { * @return string */ public static function nonce(): string { - return 'ppc-pui'; + return self::ENDPOINT; } /** @@ -116,13 +119,26 @@ class PayUponInvoiceEndpoint implements EndpointInterface { try { $data = $this->request_data->read_request( $this->nonce() ); - $this->settings->set( 'ppcp-onboarding-pui', $data['checked'] ); + + foreach ( $data['settings'] ?? array() as $field => $value ) { + $option = apply_filters( + 'ppcp_partner_referrals_option', + array( + 'field' => $field, + 'value' => $value, + 'valid' => false, + ) + ); + + if ( $option['valid'] ) { + $this->settings->set( $field, $value ); + } + } + $this->settings->persist(); foreach ( $this->signup_link_ids as $key ) { - if ( $this->signup_link_cache->has( $key ) ) { - $this->signup_link_cache->delete( $key ); - } + ( new OnboardingUrl( $this->signup_link_cache, $key, get_current_user_id() ) )->delete(); } foreach ( $this->signup_link_ids as $key ) { diff --git a/modules/ppcp-onboarding/src/OnboardingModule.php b/modules/ppcp-onboarding/src/OnboardingModule.php index 1f94f0fee..578cda42c 100644 --- a/modules/ppcp-onboarding/src/OnboardingModule.php +++ b/modules/ppcp-onboarding/src/OnboardingModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Onboarding; +use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; @@ -96,7 +97,7 @@ class OnboardingModule implements ModuleInterface { ); add_action( - 'wc_ajax_ppc-pui', + 'wc_ajax_' . UpdateSignupLinksEndpoint::ENDPOINT, static function () use ( $c ) { $endpoint = $c->get( 'onboarding.endpoint.pui' ); $endpoint->handle_request(); diff --git a/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php b/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php index f78c817c6..a5d7628ec 100644 --- a/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php +++ b/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php @@ -95,7 +95,7 @@ class OnboardingOptionsRenderer { $checked = ''; } - return '
  • '; } diff --git a/modules/ppcp-wc-gateway/resources/js/common.js b/modules/ppcp-wc-gateway/resources/js/common.js index e1aad6028..ba763605b 100644 --- a/modules/ppcp-wc-gateway/resources/js/common.js +++ b/modules/ppcp-wc-gateway/resources/js/common.js @@ -16,7 +16,7 @@ document.addEventListener( jQuery( '*[data-ppcp-display]' ).each( (index, el) => { const rules = jQuery(el).data('ppcpDisplay'); - console.log('rules', rules); + // console.log('rules', rules); for (const rule of rules) { displayManager.addRule(rule); diff --git a/modules/ppcp-wc-gateway/resources/js/common/display-manager/DisplayManager.js b/modules/ppcp-wc-gateway/resources/js/common/display-manager/DisplayManager.js index 522238686..2aede73ee 100644 --- a/modules/ppcp-wc-gateway/resources/js/common/display-manager/DisplayManager.js +++ b/modules/ppcp-wc-gateway/resources/js/common/display-manager/DisplayManager.js @@ -14,11 +14,11 @@ class DisplayManager { addRule(ruleConfig) { const updateStatus = () => { this.ruleStatus[ruleConfig.key] = this.rules[ruleConfig.key].status; - console.log('ruleStatus', this.ruleStatus); + //console.log('ruleStatus', this.ruleStatus); } this.rules[ruleConfig.key] = new Rule(ruleConfig, updateStatus.bind(this)); - console.log('Rule', this.rules[ruleConfig.key]); + //console.log('Rule', this.rules[ruleConfig.key]); } register() { diff --git a/modules/ppcp-wc-gateway/resources/js/common/display-manager/Rule.js b/modules/ppcp-wc-gateway/resources/js/common/display-manager/Rule.js index 75e88f141..20581b025 100644 --- a/modules/ppcp-wc-gateway/resources/js/common/display-manager/Rule.js +++ b/modules/ppcp-wc-gateway/resources/js/common/display-manager/Rule.js @@ -15,14 +15,14 @@ class Rule { const condition = ConditionFactory.make(conditionConfig, updateStatus); this.conditions[condition.key] = condition; - console.log('Condition', condition); + //console.log('Condition', condition); } for (const actionConfig of this.config.actions) { const action = ActionFactory.make(actionConfig); this.actions[action.key] = action; - console.log('Action', action); + //console.log('Action', action); } } diff --git a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js index c20432d48..7886131d9 100644 --- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js +++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js @@ -87,6 +87,7 @@ document.addEventListener( try { renderer.render({}); + jQuery(document).trigger('ppcp_paypal_render_preview', settings); } catch (err) { console.error(err); } @@ -113,7 +114,7 @@ document.addEventListener( 'client-id': PayPalCommerceGatewaySettings.client_id, 'currency': PayPalCommerceGatewaySettings.currency, 'integration-date': PayPalCommerceGatewaySettings.integration_date, - 'components': ['buttons', 'funding-eligibility', 'messages'], + 'components': PayPalCommerceGatewaySettings.components, 'enable-funding': ['venmo', 'paylater'], }; @@ -172,11 +173,16 @@ document.addEventListener( function createMessagesPreview(settingsCallback) { const render = (settings) => { - const wrapper = document.querySelector(settings.wrapper); + let wrapper = document.querySelector(settings.wrapper); if (!wrapper) { return; } - wrapper.innerHTML = ''; + // looks like .innerHTML = '' is not enough, PayPal somehow renders with old style + const parent = wrapper.parentElement; + parent.removeChild(wrapper); + wrapper = document.createElement('div'); + wrapper.setAttribute('id', settings.wrapper.replace('#', '')); + parent.appendChild(wrapper); const messageRenderer = new MessageRenderer(settings); @@ -269,7 +275,7 @@ document.addEventListener( }, 1000)); loadPaypalScript(oldScriptSettings, () => { - const payLaterMessagingLocations = ['product', 'cart', 'checkout', 'general']; + const payLaterMessagingLocations = ['product', 'cart', 'checkout', 'shop', 'home', 'general']; const paypalButtonLocations = ['product', 'cart', 'checkout', 'mini-cart', 'general']; paypalButtonLocations.forEach((location) => { diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 0044f7ea1..c5a1b3933 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -1021,7 +1021,8 @@ return array( $partner_endpoint, $container->get( 'dcc.status-cache' ), $container->get( 'api.helpers.dccapplies' ), - $container->get( 'onboarding.state' ) + $container->get( 'onboarding.state' ), + $container->get( 'api.helper.failure-registry' ) ); }, @@ -1101,7 +1102,8 @@ return array( $container->get( 'wcgateway.settings' ), $container->get( 'api.endpoint.partners' ), $container->get( 'pui.status-cache' ), - $container->get( 'onboarding.state' ) + $container->get( 'onboarding.state' ), + $container->get( 'api.helper.failure-registry' ) ); }, 'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice { @@ -1354,7 +1356,13 @@ return array( 'wcgateway.settings.pay-later.messaging-locations' => static function( ContainerInterface $container ): array { $button_locations = $container->get( 'wcgateway.button.locations' ); unset( $button_locations['mini-cart'] ); - return $button_locations; + return array_merge( + $button_locations, + array( + 'shop' => __( 'Shop', 'woocommerce-paypal-payments' ), + 'home' => __( 'Home', 'woocommerce-paypal-payments' ), + ) + ); }, 'wcgateway.button.default-locations' => static function( ContainerInterface $container ): array { return array_keys( $container->get( 'wcgateway.settings.pay-later.messaging-locations' ) ); diff --git a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php index dd7af6ed0..03a9c59b6 100644 --- a/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php +++ b/modules/ppcp-wc-gateway/src/Assets/SettingsPageAssets.php @@ -211,16 +211,20 @@ class SettingsPageAssets { wp_localize_script( 'ppcp-gateway-settings', 'PayPalCommerceGatewaySettings', - array( - 'is_subscriptions_plugin_active' => $this->subscription_helper->plugin_is_active(), - 'client_id' => $this->client_id, - 'currency' => $this->currency, - 'country' => $this->country, - 'environment' => $this->environment->current_environment(), - 'integration_date' => PAYPAL_INTEGRATION_DATE, - 'is_pay_later_button_enabled' => $this->is_pay_later_button_enabled, - 'disabled_sources' => $this->disabled_sources, - 'all_funding_sources' => $this->all_funding_sources, + apply_filters( + 'woocommerce_paypal_payments_admin_gateway_settings', + array( + 'is_subscriptions_plugin_active' => $this->subscription_helper->plugin_is_active(), + 'client_id' => $this->client_id, + 'currency' => $this->currency, + 'country' => $this->country, + 'environment' => $this->environment->current_environment(), + 'integration_date' => PAYPAL_INTEGRATION_DATE, + 'is_pay_later_button_enabled' => $this->is_pay_later_button_enabled, + 'disabled_sources' => $this->disabled_sources, + 'all_funding_sources' => $this->all_funding_sources, + 'components' => array( 'buttons', 'funding-eligibility', 'messages' ), + ) ) ); } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php index 0d0807659..9da44d069 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php @@ -142,6 +142,20 @@ class PayUponInvoice { $this->settings->persist(); } + add_filter( + 'ppcp_partner_referrals_option', + function ( array $option ): array { + if ( $option['valid'] ) { + return $option; + } + if ( $option['field'] === 'ppcp-onboarding-pui' ) { + $option['valid'] = true; + $option['value'] = ( $option['value'] ? '1' : '' ); + } + return $option; + } + ); + add_filter( 'ppcp_partner_referrals_data', function ( array $data ): array { diff --git a/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php b/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php index f8d717f08..772a76e3a 100644 --- a/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php +++ b/modules/ppcp-wc-gateway/src/Helper/DCCProductStatus.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -37,6 +38,14 @@ class DCCProductStatus { * @var bool|null */ private $current_status_cache; + + /** + * If there was a request failure. + * + * @var bool + */ + private $has_request_failure = false; + /** * The settings. * @@ -65,6 +74,13 @@ class DCCProductStatus { */ private $onboarding_state; + /** + * The API failure registry + * + * @var FailureRegistry + */ + private $api_failure_registry; + /** * DccProductStatus constructor. * @@ -73,19 +89,22 @@ class DCCProductStatus { * @param Cache $cache The cache. * @param DccApplies $dcc_applies The dcc applies helper. * @param State $onboarding_state The onboarding state. + * @param FailureRegistry $api_failure_registry The API failure registry. */ public function __construct( Settings $settings, PartnersEndpoint $partners_endpoint, Cache $cache, DccApplies $dcc_applies, - State $onboarding_state + State $onboarding_state, + FailureRegistry $api_failure_registry ) { - $this->settings = $settings; - $this->partners_endpoint = $partners_endpoint; - $this->cache = $cache; - $this->dcc_applies = $dcc_applies; - $this->onboarding_state = $onboarding_state; + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; + $this->cache = $cache; + $this->dcc_applies = $dcc_applies; + $this->onboarding_state = $onboarding_state; + $this->api_failure_registry = $api_failure_registry; } /** @@ -111,9 +130,17 @@ class DCCProductStatus { return true; } + // Check API failure registry to prevent multiple failed API requests. + if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) { + $this->has_request_failure = true; + $this->current_status_cache = false; + return $this->current_status_cache; + } + try { $seller_status = $this->partners_endpoint->seller_status(); } catch ( Throwable $error ) { + $this->has_request_failure = true; $this->current_status_cache = false; return false; } @@ -149,4 +176,14 @@ class DCCProductStatus { $this->current_status_cache = false; return false; } + + /** + * Returns if there was a request failure. + * + * @return bool + */ + public function has_request_failure(): bool { + return $this->has_request_failure; + } + } diff --git a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php index df70c0779..f1a3221eb 100644 --- a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php +++ b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php @@ -13,6 +13,7 @@ use Throwable; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatusProduct; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -36,6 +37,14 @@ class PayUponInvoiceProductStatus { * @var bool|null */ private $current_status_cache; + + /** + * If there was a request failure. + * + * @var bool + */ + private $has_request_failure = false; + /** * The settings. * @@ -57,6 +66,13 @@ class PayUponInvoiceProductStatus { */ private $onboarding_state; + /** + * The API failure registry + * + * @var FailureRegistry + */ + private $api_failure_registry; + /** * PayUponInvoiceProductStatus constructor. * @@ -64,17 +80,20 @@ class PayUponInvoiceProductStatus { * @param PartnersEndpoint $partners_endpoint The Partner Endpoint. * @param Cache $cache The cache. * @param State $onboarding_state The onboarding state. + * @param FailureRegistry $api_failure_registry The API failure registry. */ public function __construct( Settings $settings, PartnersEndpoint $partners_endpoint, Cache $cache, - State $onboarding_state + State $onboarding_state, + FailureRegistry $api_failure_registry ) { - $this->settings = $settings; - $this->partners_endpoint = $partners_endpoint; - $this->cache = $cache; - $this->onboarding_state = $onboarding_state; + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; + $this->cache = $cache; + $this->onboarding_state = $onboarding_state; + $this->api_failure_registry = $api_failure_registry; } /** @@ -99,9 +118,17 @@ class PayUponInvoiceProductStatus { return true; } + // Check API failure registry to prevent multiple failed API requests. + if ( $this->api_failure_registry->has_failure_in_timeframe( FailureRegistry::SELLER_STATUS_KEY, HOUR_IN_SECONDS ) ) { + $this->has_request_failure = true; + $this->current_status_cache = false; + return $this->current_status_cache; + } + try { $seller_status = $this->partners_endpoint->seller_status(); } catch ( Throwable $error ) { + $this->has_request_failure = true; $this->current_status_cache = false; return false; } @@ -136,4 +163,14 @@ class PayUponInvoiceProductStatus { $this->current_status_cache = false; return false; } + + /** + * Returns if there was a request failure. + * + * @return bool + */ + public function has_request_failure(): bool { + return $this->has_request_failure; + } + } diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index d9d4d2596..067acf434 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -456,7 +456,7 @@ return function ( ContainerInterface $container, array $fields ): array { 'description' => __( 'If you use your PayPal account with more than one installation, please use a distinct prefix to separate those installations. Please use only English letters and "-", "_" characters.', 'woocommerce-paypal-payments' ), 'maxlength' => 15, 'custom_attributes' => array( - 'pattern' => '[a-zA-Z_-]+', + 'pattern' => '[a-zA-Z_\\-]+', ), 'default' => ( static function (): string { $site_url = get_site_url( get_current_blog_id() ); diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php index 6428b1389..99a3fd3f0 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php @@ -623,6 +623,254 @@ return function ( ContainerInterface $container, array $fields ): array { 'requirements' => array( 'messages' ), 'gateway' => Settings::PAY_LATER_TAB_ID, ), + + // Shop. + 'pay_later_shop_messaging_heading' => array( + 'heading' => __( 'Pay Later Messaging on the Shop page', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-heading', + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array(), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_layout' => array( + 'title' => __( 'Shop Messaging Layout', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'text', + 'desc_tip' => true, + 'description' => __( 'The layout of the message.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'text' => __( 'Text', 'woocommerce-paypal-payments' ), + 'flex' => __( 'Banner', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_logo' => array( + 'title' => __( 'Shop Messaging Logo', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'inline', + 'desc_tip' => true, + 'description' => __( 'What logo the text message contains. Only applicable, when the layout style Text is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'primary' => __( 'Primary', 'woocommerce-paypal-payments' ), + 'alternative' => __( 'Alternative', 'woocommerce-paypal-payments' ), + 'inline' => __( 'Inline', 'woocommerce-paypal-payments' ), + 'none' => __( 'None', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_position' => array( + 'title' => __( 'Shop Messaging Logo Position', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'left', + 'desc_tip' => true, + 'description' => __( 'The position of the logo. Only applicable, when the layout style Text is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'left' => __( 'Left', 'woocommerce-paypal-payments' ), + 'right' => __( 'Right', 'woocommerce-paypal-payments' ), + 'top' => __( 'Top', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_color' => array( + 'title' => __( 'Shop Messaging Text Color', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'black', + 'desc_tip' => true, + 'description' => __( 'The color of the text. Only applicable, when the layout style Text is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'black' => __( 'Black', 'woocommerce-paypal-payments' ), + 'white' => __( 'White', 'woocommerce-paypal-payments' ), + 'monochrome' => __( 'Monochrome', 'woocommerce-paypal-payments' ), + 'grayscale' => __( 'Grayscale', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_flex_color' => array( + 'title' => __( 'Shop Messaging Color', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => $default_messaging_flex_color, + 'desc_tip' => true, + 'description' => __( 'The color of the text. Only applicable, when the layout style Banner is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'blue' => __( 'Blue', 'woocommerce-paypal-payments' ), + 'black' => __( 'Black', 'woocommerce-paypal-payments' ), + 'white' => __( 'White', 'woocommerce-paypal-payments' ), + 'white-no-border' => __( 'White no border', 'woocommerce-paypal-payments' ), + 'gray' => __( 'Gray', 'woocommerce-paypal-payments' ), + 'monochrome' => __( 'Monochrome', 'woocommerce-paypal-payments' ), + 'grayscale' => __( 'Grayscale', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_flex_ratio' => array( + 'title' => __( 'Shop Messaging Ratio', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => '8x1', + 'desc_tip' => true, + 'description' => __( 'The width/height ratio of the banner. Only applicable, when the layout style Banner is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + '1x1' => __( '1x1', 'woocommerce-paypal-payments' ), + '1x4' => __( '1x4', 'woocommerce-paypal-payments' ), + '8x1' => __( '8x1', 'woocommerce-paypal-payments' ), + '20x1' => __( '20x1', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_shop_message_preview' => array( + 'type' => 'ppcp-text', + 'text' => $render_preview_element( 'ppcpShopMessagePreview', 'message', $messaging_message ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + + // Home. + 'pay_later_home_messaging_heading' => array( + 'heading' => __( 'Pay Later Messaging on the Home page', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-heading', + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array(), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_layout' => array( + 'title' => __( 'Home Messaging Layout', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'text', + 'desc_tip' => true, + 'description' => __( 'The layout of the message.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'text' => __( 'Text', 'woocommerce-paypal-payments' ), + 'flex' => __( 'Banner', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_logo' => array( + 'title' => __( 'Home Messaging Logo', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'inline', + 'desc_tip' => true, + 'description' => __( 'What logo the text message contains. Only applicable, when the layout style Text is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'primary' => __( 'Primary', 'woocommerce-paypal-payments' ), + 'alternative' => __( 'Alternative', 'woocommerce-paypal-payments' ), + 'inline' => __( 'Inline', 'woocommerce-paypal-payments' ), + 'none' => __( 'None', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_position' => array( + 'title' => __( 'Home Messaging Logo Position', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'left', + 'desc_tip' => true, + 'description' => __( 'The position of the logo. Only applicable, when the layout style Text is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'left' => __( 'Left', 'woocommerce-paypal-payments' ), + 'right' => __( 'Right', 'woocommerce-paypal-payments' ), + 'top' => __( 'Top', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_color' => array( + 'title' => __( 'Home Messaging Text Color', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'black', + 'desc_tip' => true, + 'description' => __( 'The color of the text. Only applicable, when the layout style Text is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'black' => __( 'Black', 'woocommerce-paypal-payments' ), + 'white' => __( 'White', 'woocommerce-paypal-payments' ), + 'monochrome' => __( 'Monochrome', 'woocommerce-paypal-payments' ), + 'grayscale' => __( 'Grayscale', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_flex_color' => array( + 'title' => __( 'Home Messaging Color', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => $default_messaging_flex_color, + 'desc_tip' => true, + 'description' => __( 'The color of the text. Only applicable, when the layout style Banner is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'blue' => __( 'Blue', 'woocommerce-paypal-payments' ), + 'black' => __( 'Black', 'woocommerce-paypal-payments' ), + 'white' => __( 'White', 'woocommerce-paypal-payments' ), + 'white-no-border' => __( 'White no border', 'woocommerce-paypal-payments' ), + 'gray' => __( 'Gray', 'woocommerce-paypal-payments' ), + 'monochrome' => __( 'Monochrome', 'woocommerce-paypal-payments' ), + 'grayscale' => __( 'Grayscale', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_flex_ratio' => array( + 'title' => __( 'Home Messaging Ratio', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => '8x1', + 'desc_tip' => true, + 'description' => __( 'The width/height ratio of the banner. Only applicable, when the layout style Banner is used.', 'woocommerce-paypal-payments' ), + 'options' => array( + '1x1' => __( '1x1', 'woocommerce-paypal-payments' ), + '1x4' => __( '1x4', 'woocommerce-paypal-payments' ), + '8x1' => __( '8x1', 'woocommerce-paypal-payments' ), + '20x1' => __( '20x1', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), + 'pay_later_home_message_preview' => array( + 'type' => 'ppcp-text', + 'text' => $render_preview_element( 'ppcpHomeMessagePreview', 'message', $messaging_message ), + 'screens' => array( State::STATE_ONBOARDED ), + 'requirements' => array( 'messages' ), + 'gateway' => Settings::PAY_LATER_TAB_ID, + ), ); return array_merge( $fields, $pay_later_fields ); diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1c735db2f..e96206781 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -264,6 +264,11 @@ DAY_IN_SECONDS + + + DAY_IN_SECONDS + + realpath( __FILE__ )