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 af9720636..aad77d435 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -31,10 +31,16 @@ 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) { - console.log('[ApplePayButton] init', config); + this.log('init', config); if (this.isInitialized) { return; } @@ -53,12 +59,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(); }); @@ -150,7 +156,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); @@ -180,7 +189,7 @@ class ApplepayButton { console.error(error); } const session = this.applePaySession(paymentDataRequest) - console.log("session", session) + this.log("session", session) const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ? new FormValidator( PayPalCommerceGateway.ajax.validate_checkout.endpoint, @@ -242,7 +251,7 @@ class ApplepayButton { //------------------------ onvalidatemerchant(session) { - console.log("onvalidatemerchant") + this.log("onvalidatemerchant") return (applePayValidateMerchantEvent) => { paypal.Applepay().validateMerchant({ validationUrl: applePayValidateMerchantEvent.validationURL @@ -259,7 +268,7 @@ class ApplepayButton { 'woocommerce-process-checkout-nonce': this.nonce, } }) - console.log('validated') + this.log('validated') }) .catch(validateError => { console.error(validateError); @@ -279,7 +288,7 @@ class ApplepayButton { } onshippingmethodselected(session) { const ajax_url = this.buttonConfig.ajax_url - console.log('[ApplePayButton] onshippingmethodselected'); + this.log('onshippingmethodselected'); return (event) => { const data = this.getShippingMethodData(event); jQuery.ajax({ @@ -291,7 +300,7 @@ class ApplepayButton { if (applePayShippingMethodUpdate.success === false) { response.errors = createAppleErrors(response.errors) } - console.log('shipping method update response', response, applePayShippingMethodUpdate) + this.log('shipping method update response', response, applePayShippingMethodUpdate) this.selectedShippingMethod = event.shippingMethod //order the response shipping methods, so that the selected shipping method is the first one let orderedShippingMethods = response.newShippingMethods.sort((a, b) => { @@ -316,10 +325,10 @@ class ApplepayButton { } onshippingcontactselected(session) { const ajax_url = this.buttonConfig.ajax_url - console.log('[ApplePayButton] onshippingcontactselected', ajax_url, session) + this.log('[ApplePayButton] onshippingcontactselected', ajax_url, session) return (event) => { const data = this.getShippingContactData(event); - console.log('shipping contact selected', data, event) + this.log('shipping contact selected', data, event) jQuery.ajax({ url: ajax_url, method: 'POST', @@ -327,7 +336,7 @@ class ApplepayButton { success: (applePayShippingContactUpdate, textStatus, jqXHR) => { let response = applePayShippingContactUpdate.data this.updated_contact_info = event.shippingContact - console.log('shipping contact update response', response, applePayShippingContactUpdate, this.updated_contact_info) + this.log('shipping contact update response', response, applePayShippingContactUpdate, this.updated_contact_info) if (applePayShippingContactUpdate.success === false) { response.errors = createAppleErrors(response.errors) } @@ -421,7 +430,7 @@ class ApplepayButton { let createOrderInPayPal = actionHandler.createOrder() const processInWooAndCapture = async (data) => { try { - console.log('processInWooAndCapture', data) + this.log('processInWooAndCapture', data) const billingContact = data.billing_contact const shippingContact = data.shipping_contact jQuery.ajax({ @@ -443,7 +452,7 @@ class ApplepayButton { complete: (jqXHR, textStatus) => { }, success: (authorizationResult, textStatus, jqXHR) => { - console.log('success authorizationResult', authorizationResult) + this.log('success authorizationResult', authorizationResult) if (authorizationResult.result === "success") { redirectionUrl = authorizationResult.redirect; //session.completePayment(ApplePaySession.STATUS_SUCCESS) @@ -453,18 +462,18 @@ class ApplepayButton { } }, error: (jqXHR, textStatus, errorThrown) => { - console.log('error authorizationResult', errorThrown) + this.log('error authorizationResult', errorThrown) session.completePayment(ApplePaySession.STATUS_FAILURE) console.warn(textStatus, errorThrown) session.abort() }, }) } catch (error) { - console.log(error) // handle error + this.log(error) // handle error } } createOrderInPayPal([], []).then((orderId) => { - console.log('createOrderInPayPal', orderId) + this.log('createOrderInPayPal', orderId) paypal.Applepay().confirmOrder( { orderId: orderId, @@ -488,31 +497,31 @@ class ApplepayButton { } ); }).catch((error) => { - console.log(error) + console.error(error) session.abort() }) };*/ } /* onPaymentAuthorized(paymentData) { - console.log('[ApplePayButton] onPaymentAuthorized', this.context); + this.log('[ApplePayButton] onPaymentAuthorized', this.context); return this.processPayment(paymentData); } async processPayment(paymentData) { - console.log('[ApplePayButton] processPayment', this.context); + this.log('[ApplePayButton] processPayment', this.context); return new Promise(async (resolve, reject) => { try { let id = await this.contextHandler.createOrder(); - console.log('[ApplePayButton] processPayment: createOrder', id, this.context); + this.log('[ApplePayButton] processPayment: createOrder', id, this.context); const confirmOrderResponse = await paypal.Applepay().confirmOrder({ orderId: id, paymentMethodData: paymentData.paymentMethodData }); - console.log('[ApplePayButton] processPayment: confirmOrder', confirmOrderResponse, this.context); + this.log('[ApplePayButton] processPayment: confirmOrder', confirmOrderResponse, this.context); /!** Capture the Order on the Server *!/ if (confirmOrderResponse.status === "APPROVED") { @@ -554,7 +563,7 @@ class ApplepayButton { } } - console.log('[ApplePayButton] processPaymentResponse', response, this.context); + this.log('processPaymentResponse', response, this.context); return response; }*/ diff --git a/modules/ppcp-applepay/resources/js/ApplepayManager.js b/modules/ppcp-applepay/resources/js/ApplepayManager.js index 784bdfabd..03acfcace 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayManager.js +++ b/modules/ppcp-applepay/resources/js/ApplepayManager.js @@ -8,7 +8,7 @@ class ApplepayManager { this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; this.ApplePayConfig = null; -console.log('Applepay manager', ppcpConfig, buttonConfig) + //console.log('Applepay manager', ppcpConfig, buttonConfig) this.buttons = []; buttonModuleWatcher.watchContextBootstrap((bootstrap) => { diff --git a/modules/ppcp-applepay/resources/js/boot-block.js b/modules/ppcp-applepay/resources/js/boot-block.js index de70f7f13..64f83690a 100644 --- a/modules/ppcp-applepay/resources/js/boot-block.js +++ b/modules/ppcp-applepay/resources/js/boot-block.js @@ -14,13 +14,13 @@ if (typeof window.PayPalCommerceGateway === 'undefined') { window.PayPalCommerceGateway = ppcpConfig; } -console.log('ppcpData', ppcpData); -console.log('ppcpConfig', ppcpConfig); -console.log('buttonData', buttonData); -console.log('buttonConfig', buttonConfig); +//console.log('ppcpData', ppcpData); +//console.log('ppcpConfig', ppcpConfig); +//console.log('buttonData', buttonData); +//console.log('buttonConfig', buttonConfig); const ApplePayComponent = () => { - console.log('ApplePayComponent render'); + //console.log('ApplePayComponent render'); const [bootstrapped, setBootstrapped] = useState(false); const [paypalLoaded, setPaypalLoaded] = useState(false); diff --git a/modules/ppcp-applepay/resources/js/boot.js b/modules/ppcp-applepay/resources/js/boot.js index 7b6d569e3..e9b750b2f 100644 --- a/modules/ppcp-applepay/resources/js/boot.js +++ b/modules/ppcp-applepay/resources/js/boot.js @@ -25,7 +25,7 @@ import ApplepayManager from "./ApplepayManager"; } const isMiniCart = ppcpConfig.mini_cart_buttons_enabled; const isButton = jQuery('#' + buttonConfig.button.wrapper).length > 0; - console.log('isbutton' ,isButton, buttonConfig.button.wrapper) + //console.log('isbutton' ,isButton, buttonConfig.button.wrapper) // If button wrapper is not present then there is no need to load the scripts. // minicart loads later? if (!isMiniCart && !isButton) { 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 cee3660a0..7bd4d09a8 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/js/modules/Renderer/WidgetBuilder.js b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js index e4d13844b..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) { @@ -177,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 8ef66f409..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; @@ -1038,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; } @@ -1058,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. * 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-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 7485b3bc3..56b8ec82b 100644 --- a/modules/ppcp-onboarding/resources/js/settings.js +++ b/modules/ppcp-onboarding/resources/js/settings.js @@ -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 e5b71f8d5..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'], }; diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 3cd63a056..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 { 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/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__ )