From 10439ff02b209e7a16b7135924594c1a1a0ef52d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 22 Jul 2024 13:30:20 +0200 Subject: [PATCH 01/22] =?UTF-8?q?=E2=9C=A8=20Add=20empty=20ApplePay=20gate?= =?UTF-8?q?way=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/ApplePayGateway.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 modules/ppcp-applepay/ApplePayGateway.php diff --git a/modules/ppcp-applepay/ApplePayGateway.php b/modules/ppcp-applepay/ApplePayGateway.php new file mode 100644 index 000000000..6073ba859 --- /dev/null +++ b/modules/ppcp-applepay/ApplePayGateway.php @@ -0,0 +1,20 @@ + Date: Mon, 22 Jul 2024 18:29:39 +0200 Subject: [PATCH 02/22] =?UTF-8?q?=F0=9F=9A=A7=20New=20Gateway=20for=20Appl?= =?UTF-8?q?e=20Pay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/ApplePayGateway.php | 20 -- .../ppcp-applepay/resources/css/styles.scss | 5 + .../resources/js/ApplepayButton.js | 200 ++++++++------ .../resources/js/Helper/utils.js | 2 +- modules/ppcp-applepay/services.php | 10 + modules/ppcp-applepay/src/ApplePayGateway.php | 234 +++++++++++++++++ modules/ppcp-applepay/src/ApplepayModule.php | 45 +++- .../src/Assets/ApplePayButton.php | 7 +- .../src/Assets/DataToAppleButtonScripts.php | 244 +++++++----------- .../ContextBootstrap/CheckoutBootstap.js | 11 +- .../js/modules/Helper/CheckoutMethodState.js | 1 + .../ppcp-wc-gateway/src/Settings/Settings.php | 5 +- 12 files changed, 516 insertions(+), 268 deletions(-) delete mode 100644 modules/ppcp-applepay/ApplePayGateway.php create mode 100644 modules/ppcp-applepay/src/ApplePayGateway.php diff --git a/modules/ppcp-applepay/ApplePayGateway.php b/modules/ppcp-applepay/ApplePayGateway.php deleted file mode 100644 index 6073ba859..000000000 --- a/modules/ppcp-applepay/ApplePayGateway.php +++ /dev/null @@ -1,20 +0,0 @@ - { - this.addButton(); - const id_minicart = - '#apple-' + this.buttonConfig.button.mini_cart_wrapper; - const id = '#apple-' + this.buttonConfig.button.wrapper; + const idMinicart = this.buttonConfig.button.mini_cart_wrapper; + const idButton = this.buttonConfig.button.wrapper; - if ( this.context === 'mini-cart' ) { - document - .querySelector( id_minicart ) - ?.addEventListener( 'click', ( evt ) => { - evt.preventDefault(); - this.onButtonClick(); - } ); - } else { - document - .querySelector( id ) - ?.addEventListener( 'click', ( evt ) => { - evt.preventDefault(); - this.onButtonClick(); - } ); - } - } ); - } else { - jQuery( '#' + this.buttonConfig.button.wrapper ).hide(); - jQuery( '#' + this.buttonConfig.button.mini_cart_wrapper ).hide(); + if ( ! this.isEligible ) { + jQuery( '#' + idButton ).hide(); + jQuery( '#' + idMinicart ).hide(); jQuery( '#express-payment-method-ppcp-applepay' ).hide(); + + return; } + + // Add click-handler to the button. + const setupButtonEvents = ( id ) => { + document + .getElementById( id ) + ?.addEventListener( 'click', ( evt ) => { + evt.preventDefault(); + this.onButtonClick(); + } ); + }; + + this.fetchTransactionInfo().then( () => { + this.addButton(); + + if ( this.context === 'mini-cart' ) { + setupButtonEvents( idMinicart ); + } else { + setupButtonEvents( idButton ); + } + } ); } reinit() { @@ -144,11 +169,11 @@ class ApplepayButton { initEventHandlers() { const { wrapper, ppcpButtonWrapper } = this.contextConfig(); - const wrapper_id = '#' + wrapper; + const wrapperId = '#' + wrapper; - if ( wrapper_id === ppcpButtonWrapper ) { + if ( wrapperId === ppcpButtonWrapper ) { throw new Error( - `[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper_id }"` + `[ApplePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapperId }"` ); } @@ -158,9 +183,9 @@ class ApplepayButton { } const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper ); - setVisible( wrapper_id, $ppcpButtonWrapper.is( ':visible' ) ); + setVisible( wrapperId, $ppcpButtonWrapper.is( ':visible' ) ); setEnabled( - wrapper_id, + wrapperId, ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' ) ); }; @@ -192,6 +217,7 @@ class ApplepayButton { session.onshippingcontactselected = this.onShippingContactSelected( session ); } + session.onvalidatemerchant = this.onValidateMerchant( session ); session.onpaymentauthorized = this.onPaymentAuthorized( session ); return session; @@ -211,19 +237,19 @@ class ApplepayButton { const color = this.buttonConfig.button.color; const id = 'apple-' + wrapper; - if ( appleContainer ) { - appleContainer.innerHTML = ``; + if ( ! appleContainer ) { + return; } - const $wrapper = jQuery( '#' + wrapper ); - $wrapper.addClass( 'ppcp-button-' + ppcpStyle.shape ); + appleContainer.innerHTML = ``; + appleContainer.classList.add( 'ppcp-button-' + ppcpStyle.shape ); if ( ppcpStyle.height ) { - $wrapper.css( + appleContainer.style.setProperty( '--apple-pay-button-height', `${ ppcpStyle.height }px` ); - $wrapper.css( 'height', `${ ppcpStyle.height }px` ); + appleContainer.style.height = `${ ppcpStyle.height }px`; } } @@ -239,7 +265,8 @@ class ApplepayButton { const paymentRequest = this.paymentRequest(); - window.ppcpFundingSource = 'apple_pay'; // Do this on another place like on create order endpoint handler. + // Do this on another place like on create order endpoint handler. + window.ppcpFundingSource = 'apple_pay'; // Trigger woocommerce validation if we are in the checkout page. if ( this.context === 'checkout' ) { @@ -323,8 +350,8 @@ class ApplepayButton { } /** - * Indicates how payment completion should be handled if with the context handler default actions. - * Or with ApplePay module specific completion. + * Indicates how payment completion should be handled if with the context handler default + * actions. Or with Apple Pay module specific completion. * * @return {boolean} */ @@ -333,18 +360,17 @@ class ApplepayButton { if ( ! this.contextHandler.shippingAllowed() ) { return true; } + // Use WC form data mode in Checkout. - if ( + return ( this.context === 'checkout' && ! this.shouldUpdateButtonWithFormData() - ) { - return true; - } - return false; + ); } /** - * Updates ApplePay paymentRequest with form data. + * Updates Apple Pay paymentRequest with form data. + * * @param paymentRequest */ updateRequestDataWithForm( paymentRequest ) { @@ -358,8 +384,9 @@ class ApplepayButton { ); // Add custom data. - // "applicationData" is originating a "PayPalApplePayError: An internal server error has occurred" on paypal.Applepay().confirmOrder(). - // paymentRequest.applicationData = this.fillApplicationData(this.formData); + // "applicationData" is originating a "PayPalApplePayError: An internal server error has + // occurred" on paypal.Applepay().confirmOrder(). paymentRequest.applicationData = + // this.fillApplicationData(this.formData); if ( ! this.shouldRequireShippingInButton() ) { return; @@ -425,7 +452,8 @@ class ApplepayButton { 'email', 'phone', ], - requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing email and phone fields. + requiredBillingContactFields: [ 'postalAddress' ], // ApplePay does not implement billing + // email and phone fields. }; if ( ! this.shouldRequireShippingInButton() ) { @@ -453,14 +481,11 @@ class ApplepayButton { } refreshContextData() { - switch ( this.context ) { - case 'product': + if ( 'product' === this.context ) { // Refresh product data that makes the price change. - this.productQuantity = - document.querySelector( 'input.qty' )?.value; + this.productQuantity = document.querySelector( 'input.qty' )?.value; this.products = this.contextHandler.products(); this.log( 'Products updated', this.products ); - break; } } @@ -468,8 +493,35 @@ class ApplepayButton { // Payment process //------------------------ + /** + * Make ajax call to change the verification-status of the current domain. + * + * @param {boolean} isValid + */ + adminValidation( isValid ) { + // eslint-disable-next-line no-unused-vars + const ignored = fetch( this.buttonConfig.ajax_url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams( { + action: 'ppcp_validate', + 'woocommerce-process-checkout-nonce': this.nonce, + validation: isValid, + } ).toString(), + } ); + } + + /** + * Returns an event handler that Apple Pay calls when displaying the payment sheet. + * + * @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778021-onvalidatemerchant + * + * @param session + * @return {(function(*): void)|*} + */ onValidateMerchant( session ) { - this.log( 'onvalidatemerchant', this.buttonConfig.ajax_url ); return ( applePayValidateMerchantEvent ) => { this.log( 'onvalidatemerchant call' ); @@ -479,34 +531,15 @@ class ApplepayButton { validationUrl: applePayValidateMerchantEvent.validationURL, } ) .then( ( validateResult ) => { - this.log( 'onvalidatemerchant ok' ); session.completeMerchantValidation( validateResult.merchantSession ); - //call backend to update validation to true - jQuery.ajax( { - url: this.buttonConfig.ajax_url, - type: 'POST', - data: { - action: 'ppcp_validate', - validation: true, - 'woocommerce-process-checkout-nonce': this.nonce, - }, - } ); + + this.adminValidation( true ); } ) .catch( ( validateError ) => { - this.log( 'onvalidatemerchant error', validateError ); console.error( validateError ); - //call backend to update validation to false - jQuery.ajax( { - url: this.buttonConfig.ajax_url, - type: 'POST', - data: { - action: 'ppcp_validate', - validation: false, - 'woocommerce-process-checkout-nonce': this.nonce, - }, - } ); + this.adminValidation( false ); this.log( 'onvalidatemerchant session abort' ); session.abort(); } ); @@ -537,7 +570,8 @@ class ApplepayButton { } this.selectedShippingMethod = event.shippingMethod; - // Sort the response shipping methods, so that the selected shipping method is the first one. + // Sort the response shipping methods, so that the selected shipping method is + // the first one. response.newShippingMethods = response.newShippingMethods.sort( ( a, b ) => { if ( @@ -680,6 +714,7 @@ class ApplepayButton { function form() { return document.querySelector( 'form.cart' ); } + const processInWooAndCapture = async ( data ) => { return new Promise( ( resolve, reject ) => { try { @@ -781,7 +816,8 @@ class ApplepayButton { if ( this.shouldCompletePaymentWithContextHandler() ) { - // No shipping, expect immediate capture, ex: PayNow, Checkout with form data. + // No shipping, expect immediate capture, ex: PayNow, Checkout with + // form data. let approveFailed = false; await this.contextHandler.approveOrder( @@ -950,4 +986,4 @@ class ApplepayButton { } } -export default ApplepayButton; +export default ApplePayButton; diff --git a/modules/ppcp-applepay/resources/js/Helper/utils.js b/modules/ppcp-applepay/resources/js/Helper/utils.js index 3b8edc2c8..793fe7f42 100644 --- a/modules/ppcp-applepay/resources/js/Helper/utils.js +++ b/modules/ppcp-applepay/resources/js/Helper/utils.js @@ -1,4 +1,4 @@ -export const buttonID = 'applepay-container'; +export const buttonID = 'ppc-button-applepay-container'; export const endpoints = { validation: '_apple_pay_validation', createOrderCart: '_apple_pay_create_order_cart', diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index 6079fcb40..491e5f2aa 100644 --- a/modules/ppcp-applepay/services.php +++ b/modules/ppcp-applepay/services.php @@ -976,5 +976,15 @@ return array( esc_html( $button_text ) ); }, + 'applepay.wc-gateway' => static function ( ContainerInterface $container ): ApplePayGateway { + return new ApplePayGateway( + $container->get( 'wcgateway.order-processor' ), + $container->get( 'api.factory.paypal-checkout-url' ), + $container->get( 'wcgateway.processor.refunds' ), + $container->get( 'wcgateway.transaction-url-provider' ), + $container->get( 'session.handler' ), + $container->get( 'applepay.url' ) + ); + }, ); diff --git a/modules/ppcp-applepay/src/ApplePayGateway.php b/modules/ppcp-applepay/src/ApplePayGateway.php new file mode 100644 index 000000000..327ccae53 --- /dev/null +++ b/modules/ppcp-applepay/src/ApplePayGateway.php @@ -0,0 +1,234 @@ +id = self::ID; + + $this->method_title = __( 'Apple Pay (via PayPal) ', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'The separate payment gateway with the Apple Pay button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' ); + + $this->title = $this->get_option( 'title', __( 'Apple Pay', 'woocommerce-paypal-payments' ) ); + $this->description = $this->get_option( 'description', '' ); + + $this->module_url = $module_url; + $this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.png'; + + $this->init_form_fields(); + $this->init_settings(); + $this->order_processor = $order_processor; + $this->paypal_checkout_url_factory = $paypal_checkout_url_factory; + $this->refund_processor = $refund_processor; + $this->transaction_url_provider = $transaction_url_provider; + $this->session_handler = $session_handler; + + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( + $this, + 'process_admin_options', + ) + ); + } + + /** + * Initialize the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'Enable Apple Pay', 'woocommerce-paypal-payments' ), + 'default' => 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable Apple Pay payment gateway.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + ); + } + + /** + * Process payment for a WooCommerce order. + * + * @param int $order_id The WooCommerce order id. + * + * @return array + */ + public function process_payment( $order_id ) : array { + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $this->handle_payment_failure( + null, + new GatewayGenericException( new Exception( 'WC order was not found.' ) ) + ); + } + + do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order ); + + try { + try { + $this->order_processor->process( $wc_order ); + + do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); + + return $this->handle_payment_success( $wc_order ); + } catch ( PayPalOrderMissingException $exc ) { + $order = $this->order_processor->create_order( $wc_order ); + + return array( + 'result' => 'success', + 'redirect' => ( $this->paypal_checkout_url_factory )( $order->id() ), + ); + } + } catch ( PayPalApiException $error ) { + return $this->handle_payment_failure( + $wc_order, + new Exception( + Messages::generic_payment_error_message() . ' ' . $error->getMessage(), + $error->getCode(), + $error + ) + ); + } catch ( Exception $error ) { + return $this->handle_payment_failure( $wc_order, $error ); + } + } + + /** + * Process refund. + * + * If the gateway declares 'refunds' support, this will allow it to refund. + * a passed in amount. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * + * @return boolean True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) : bool { + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, WC_Order::class ) ) { + return false; + } + + return $this->refund_processor->process( $order, (float) $amount, (string) $reason ); + } + + /** + * Return transaction url for this gateway and given order. + * + * @param WC_Order $order WC order to get transaction url by. + * + * @return string + */ + public function get_transaction_url( $order ) : string { + $this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order ); + + return parent::get_transaction_url( $order ); + } +} diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index 409a9c81e..878d4b322 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Applepay; +use WC_Payment_Gateway; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton; use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; @@ -117,6 +118,48 @@ class ApplepayModule implements ModuleInterface { 100, 2 ); + + add_filter( + 'woocommerce_payment_gateways', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + static function ( $methods ) use ( $c ): array { + if ( ! is_array( $methods ) ) { + return $methods; + } + + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + if ( $settings->has( 'applepay_button_enabled' ) && $settings->get( 'applepay_button_enabled' ) ) { + $applepay_gateway = $c->get( 'applepay.wc-gateway' ); + assert( $applepay_gateway instanceof WC_Payment_Gateway ); + + $methods[] = $applepay_gateway; + } + + return $methods; + } + ); + + add_action( + 'woocommerce_review_order_after_submit', + function () { + // Wrapper ID: #ppc-button-ppcp-applepay. + echo '
'; + } + ); + + add_action( + 'woocommerce_pay_order_after_submit', + function () { + // Wrapper ID: #ppc-button-ppcp-applepay. + echo '
'; + } + ); } /** @@ -306,7 +349,7 @@ class ApplepayModule implements ModuleInterface { * @param bool $is_sandbox The environment for this merchant. * @return string */ - public function validation_string( bool $is_sandbox ) { + public function validation_string( bool $is_sandbox ) : string { $sandbox_string = $this->sandbox_validation_string(); $live_string = $this->live_validation_string(); return $is_sandbox ? $sandbox_string : $live_string; diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 926fc5ce2..b710e75ca 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -20,12 +20,13 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait; +use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; /** * Class ApplePayButton */ class ApplePayButton implements ButtonInterface { - use RequestHandlerTrait; + use RequestHandlerTrait, ContextTrait; /** * The settings. @@ -973,7 +974,7 @@ class ApplePayButton implements ButtonInterface { add_action( $render_placeholder, function () { - echo ''; + echo ''; }, 21 ); @@ -986,7 +987,7 @@ class ApplePayButton implements ButtonInterface { */ protected function applepay_button(): void { ?> -
+
data_for_product_page( - $shop_country_code, - $currency_code, - $total_label - ); + return $this->data_for_product_page(); } - return $this->data_for_cart_page( - $shop_country_code, - $currency_code, - $total_label - ); + return $this->data_for_cart_page(); } /** * Returns the appropriate admin data to send to ApplePay script * * @return array - * @throws NotFoundException When the setting is not found. */ public function apple_pay_script_data_for_admin() : array { + return $this->data_for_admin_page(); + } + + /** + * Returns the full config array for the Apple Pay integration with default values. + * + * @param array $product - Optional. Product details for the payment button. + * + * @return array + */ + private function get_apple_pay_data( array $product = [] ) : array { + // true: Use Apple Pay as distinct gateway. + // false: integrate it with the smart buttons. + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $is_wc_gateway_enabled = isset( $available_gateways[ ApplePayGateway::ID ] ); + + // use_wc: Use WC checkout data + // use_applepay: Use data provided by Apple Pay. + $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) + ? $this->settings->get( 'applepay_checkout_data_mode' ) + : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; + + // Store country, currency and name. $base_location = wc_get_base_location(); $shop_country_code = $base_location['country']; $currency_code = get_woocommerce_currency(); $total_label = get_bloginfo( 'name' ); - return $this->data_for_admin_page( - $shop_country_code, - $currency_code, - $total_label + // Button layout (label, color, language). + $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; + $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; + $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; + $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); + $is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' ); + + return array( + 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'is_admin' => false, + 'is_enabled' => $is_enabled, + 'is_wc_gateway_enabled' => $is_wc_gateway_enabled, + 'preferences' => array( + 'checkout_data_mode' => $checkout_data_mode, + ), + 'button' => array( + 'wrapper' => 'ppc-button-applepay-container', + 'mini_cart_wrapper' => 'ppc-button-applepay-container-minicart', + 'type' => $type, + 'color' => $color, + 'lang' => $lang, + ), + 'product' => $product, + 'shop' => array( + 'countryCode' => $shop_country_code, + 'currencyCode' => $currency_code, + 'totalLabel' => $total_label, + ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), ); } /** * Check if the product needs shipping * - * @param \WC_Product $product The product. - * * @return bool */ - protected function check_if_need_shipping( $product ) { + protected function check_if_need_shipping( WC_Product $product ) : bool { if ( ! wc_shipping_enabled() || 0 === wc_get_shipping_method_count( @@ -104,30 +139,20 @@ class DataToAppleButtonScripts { ) { return false; } - $needs_shipping = false; if ( $product->needs_shipping() ) { - $needs_shipping = true; + return true; } - return $needs_shipping; + return false; } /** * Prepares the data for the product page. * - * @param string $shop_country_code The shop country code. - * @param string $currency_code The currency code. - * @param string $total_label The label for the total amount. - * * @return array - * @throws NotFoundException When the setting is not found. */ - protected function data_for_product_page( - $shop_country_code, - $currency_code, - $total_label - ) { + protected function data_for_product_page() : array { $product = wc_get_product( get_the_id() ); if ( ! $product ) { return array(); @@ -136,146 +161,53 @@ class DataToAppleButtonScripts { if ( $product->get_type() === 'variable' || $product->get_type() === 'variable-subscription' ) { $is_variation = true; } + $product_need_shipping = $this->check_if_need_shipping( $product ); $product_id = get_the_id(); $product_price = $product->get_price(); $product_stock = $product->get_stock_status(); - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; - return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, - 'is_admin' => false, - 'preferences' => array( - 'checkout_data_mode' => $checkout_data_mode, - ), - 'button' => array( - 'wrapper' => 'applepay-container', - 'mini_cart_wrapper' => 'applepay-container-minicart', - 'type' => $type, - 'color' => $color, - 'lang' => $lang, - ), - 'product' => array( - 'needShipping' => $product_need_shipping, - 'id' => $product_id, - 'price' => $product_price, - 'isVariation' => $is_variation, - 'stock' => $product_stock, - ), - 'shop' => array( - 'countryCode' => $shop_country_code, - 'currencyCode' => $currency_code, - 'totalLabel' => $total_label, - ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), - ); + return $this->get_apple_pay_data( array( + 'needShipping' => $product_need_shipping, + 'id' => $product_id, + 'price' => $product_price, + 'isVariation' => $is_variation, + 'stock' => $product_stock, + ) ); } /** * Prepares the data for the cart page. * - * @param string $shop_country_code The shop country code. - * @param string $currency_code The currency code. - * @param string $total_label The label for the total amount. - * * @return array */ - protected function data_for_cart_page( - $shop_country_code, - $currency_code, - $total_label - ) { + protected function data_for_cart_page() : array { $cart = WC()->cart; if ( ! $cart ) { return array(); } - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); - $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; - - return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, - 'is_admin' => false, - 'preferences' => array( - 'checkout_data_mode' => $checkout_data_mode, - ), - 'button' => array( - 'wrapper' => 'applepay-container', - 'mini_cart_wrapper' => 'applepay-container-minicart', - 'type' => $type, - 'color' => $color, - 'lang' => $lang, - ), - 'product' => array( - 'needShipping' => $cart->needs_shipping(), - 'subtotal' => $cart->get_subtotal(), - ), - 'shop' => array( - 'countryCode' => $shop_country_code, - 'currencyCode' => $currency_code, - 'totalLabel' => $total_label, - ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ), - ); + return $this->get_apple_pay_data( array( + 'needShipping' => $cart->needs_shipping(), + 'subtotal' => $cart->get_subtotal(), + ) ); } /** * Prepares the data for the cart page. - * Consider refactoring this method along with data_for_cart_page() and data_for_product_page() methods. - * - * @param string $shop_country_code The shop country code. - * @param string $currency_code The currency code. - * @param string $total_label The label for the total amount. + * Consider refactoring this method along with data_for_cart_page() and data_for_product_page() + * methods. * * @return array */ - protected function data_for_admin_page( - $shop_country_code, - $currency_code, - $total_label - ) { - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); - $checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT; - $is_enabled = $this->settings->has( 'applepay_button_enabled' ) && $this->settings->get( 'applepay_button_enabled' ); + protected function data_for_admin_page() : array { + $data = $this->get_apple_pay_data( array( + 'needShipping' => false, + 'subtotal' => 0, + ) ); - return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, - 'is_admin' => true, - 'is_enabled' => $is_enabled, - 'preferences' => array( - 'checkout_data_mode' => $checkout_data_mode, - ), - 'button' => array( - 'wrapper' => 'applepay-container', - 'mini_cart_wrapper' => 'applepay-container-minicart', - 'type' => $type, - 'color' => $color, - 'lang' => $lang, - ), - 'product' => array( - 'needShipping' => false, - 'subtotal' => 0, - ), - 'shop' => array( - 'countryCode' => $shop_country_code, - 'currencyCode' => $currency_code, - 'totalLabel' => $total_label, - ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - ); + $data['is_admin'] = true; + + return $data; } } diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 848285815..0ec918667 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -1,3 +1,5 @@ +/* global PayPalCommerceGateway */ + import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; import { setVisible, setVisibleByClass } from '../Helper/Hiding'; import { @@ -181,9 +183,14 @@ class CheckoutBootstap { const isSeparateButtonGateway = [ PaymentMethods.CARD_BUTTON ].includes( currentPaymentMethod ); + const isApplePayMethod = + currentPaymentMethod === PaymentMethods.APPLEPAY; const isSavedCard = isCard && isSavedCardSelected(); const isNotOurGateway = - ! isPaypal && ! isCard && ! isSeparateButtonGateway; + ! isPaypal && + ! isCard && + ! isSeparateButtonGateway && + ! isApplePayMethod; const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart; const hasVaultedPaypal = PayPalCommerceGateway.vaulted_paypal_email !== ''; @@ -227,6 +234,8 @@ class CheckoutBootstap { } } + setVisible( '#ppc-button-ppcp-applepay', isApplePayMethod ); + jQuery( document.body ).trigger( 'ppcp_checkout_rendered' ); } diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js index 0ea05f255..6fa3d6c73 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js +++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js @@ -3,6 +3,7 @@ export const PaymentMethods = { CARDS: 'ppcp-credit-card-gateway', OXXO: 'ppcp-oxxo-gateway', CARD_BUTTON: 'ppcp-card-button-gateway', + APPLEPAY: 'ppcp-applepay', }; export const ORDER_BUTTON_SELECTOR = '#place_order'; diff --git a/modules/ppcp-wc-gateway/src/Settings/Settings.php b/modules/ppcp-wc-gateway/src/Settings/Settings.php index 25fcb808d..e91256653 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Settings.php +++ b/modules/ppcp-wc-gateway/src/Settings/Settings.php @@ -117,19 +117,16 @@ class Settings implements ContainerInterface { /** * Stores the settings to the database. */ - public function persist() { - + public function persist() : bool { return update_option( self::KEY, $this->settings ); } - /** * Loads the settings. * * @return bool */ private function load(): bool { - if ( $this->settings ) { return false; } From a473459ddbf377f3f9d01f596365f89a8e4287fd Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 23 Jul 2024 17:36:10 +0200 Subject: [PATCH 03/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20code=20clean?= =?UTF-8?q?up=20and=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 192 +++++++++++------- .../resources/js/ApplepayManager.js | 15 +- .../js/ApplepayManagerBlockEditor.js | 10 +- .../resources/js/Context/BaseHandler.js | 8 - .../resources/js/Context/PreviewHandler.js | 6 +- .../ppcp-applepay/resources/js/boot-admin.js | 4 +- .../ppcp-applepay/resources/js/boot-block.js | 8 +- modules/ppcp-applepay/resources/js/boot.js | 4 +- 8 files changed, 142 insertions(+), 105 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index b703a9565..3a1a13a00 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -10,6 +10,23 @@ import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons'; +/** + * List of valid context values that the button can have. + * + * @type {Object} + */ +const CONTEXT = { + Product: 'product', + Cart: 'cart', + Checkout: 'checkout', + PayNow: 'pay-now', + MiniCart: 'mini-cart', + BlockCart: 'cart-block', + BlockCheckout: 'checkout-block', + Preview: 'preview', + Blocks: [ 'cart-block', 'checkout-block' ], +}; + /** * A payment button for Apple Pay. * @@ -32,6 +49,20 @@ class ApplePayButton { */ context = ''; + externalHandler = null; + buttonConfig = null; + ppcpConfig = null; + paymentsClient = null; + formData = null; + contextHandler = null; + updatedContactInfo = []; + selectedShippingMethod = []; + + /** + * Stores initialization data sent to the button. + */ + initialPaymentRequest = null; + constructor( context, externalHandler, buttonConfig, ppcpConfig ) { apmButtonsInit( ppcpConfig ); @@ -39,8 +70,6 @@ class ApplePayButton { this.externalHandler = externalHandler; this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; - this.paymentsClient = null; - this.formData = null; this.contextHandler = ContextHandlerFactory.create( this.context, @@ -48,18 +77,6 @@ class ApplePayButton { this.ppcpConfig ); - this.updatedContactInfo = []; - this.selectedShippingMethod = []; - this.nonce = - document.getElementById( 'woocommerce-process-checkout-nonce' ) - ?.value || buttonConfig.nonce; - - // Stores initialization data sent to the button. - this.initialPaymentRequest = null; - - // Default eligibility status. - this.isEligible = true; - this.log = function () { if ( this.buttonConfig.is_debug ) { //console.log('[ApplePayButton]', ...arguments); @@ -76,6 +93,39 @@ class ApplePayButton { document.ppcpApplepayButtons[ this.context ] = this; } + /** + * The nonce for ajax requests. + * + * @return {string} The nonce value + */ + get nonce() { + const input = document.getElementById( + 'woocommerce-process-checkout-nonce' + ); + + return input?.value || this.buttonConfig.nonce; + } + + /** + * Whether the current page qualifies to use the Apple Pay button. + * + * In admin, the button is always eligible, to display an accurate preview. + * On front-end, PayPal's response decides if customers can use Apple Pay. + * + * @return {boolean} True, if the button can be displayed. + */ + get isEligible() { + if ( ! this.isInitialized ) { + return true; + } + + if ( this.buttonConfig.is_admin ) { + return true; + } + + return !! ( this.applePayConfig.isEligible && window.ApplePaySession ); + } + init( config ) { if ( this.isInitialized ) { return; @@ -90,9 +140,6 @@ class ApplePayButton { this.isInitialized = true; this.applePayConfig = config; - this.isEligible = - ( this.applePayConfig.isEligible && window.ApplePaySession ) || - this.buttonConfig.is_admin; const idMinicart = this.buttonConfig.button.mini_cart_wrapper; const idButton = this.buttonConfig.button.wrapper; @@ -118,7 +165,7 @@ class ApplePayButton { this.fetchTransactionInfo().then( () => { this.addButton(); - if ( this.context === 'mini-cart' ) { + if ( CONTEXT.MiniCart === this.context ) { setupButtonEvents( idMinicart ); } else { setupButtonEvents( idButton ); @@ -143,23 +190,22 @@ class ApplePayButton { * Returns configurations relative to this button context. */ contextConfig() { - const config = { - wrapper: this.buttonConfig.button.wrapper, - ppcpStyle: this.ppcpConfig.button.style, - buttonStyle: this.buttonConfig.button.style, - ppcpButtonWrapper: this.ppcpConfig.button.wrapper, - }; + const config = {}; - if ( this.context === 'mini-cart' ) { + if ( CONTEXT.MiniCart === this.context ) { config.wrapper = this.buttonConfig.button.mini_cart_wrapper; config.ppcpStyle = this.ppcpConfig.button.mini_cart_style; config.buttonStyle = this.buttonConfig.button.mini_cart_style; config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper; + } else { + config.wrapper = this.buttonConfig.button.wrapper; + config.ppcpStyle = this.ppcpConfig.button.style; + config.buttonStyle = this.buttonConfig.button.style; + config.ppcpButtonWrapper = this.ppcpConfig.button.wrapper; } - if ( - [ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1 - ) { + // Block editor configuration. + if ( CONTEXT.Blocks.includes( this.context ) ) { config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway-paypal'; } @@ -203,8 +249,9 @@ class ApplePayButton { } /** - * Starts an ApplePay session. - * @param paymentRequest + * Starts an Apple Pay session. + * + * @param {Object} paymentRequest The payment request object. */ applePaySession( paymentRequest ) { this.log( 'applePaySession', paymentRequest ); @@ -269,7 +316,7 @@ class ApplePayButton { window.ppcpFundingSource = 'apple_pay'; // Trigger woocommerce validation if we are in the checkout page. - if ( this.context === 'checkout' ) { + if ( CONTEXT.Checkout === this.context ) { const checkoutFormSelector = 'form.woocommerce-checkout'; const errorHandler = new ErrorHandler( PayPalCommerceGateway.labels.error.generic, @@ -323,13 +370,13 @@ class ApplePayButton { /** * If the button should show the shipping fields. * - * @return {false|*} + * @return {boolean} True, if shipping fields should be captured by ApplePay. */ shouldRequireShippingInButton() { return ( this.contextHandler.shippingAllowed() && this.buttonConfig.product.needShipping && - ( this.context !== 'checkout' || + ( CONTEXT.Checkout !== this.context || this.shouldUpdateButtonWithFormData() ) ); } @@ -337,10 +384,10 @@ class ApplePayButton { /** * If the button should be updated with the form addresses. * - * @return {boolean} + * @return {boolean} True, when Apple Pay data should be submitted to WooCommerce. */ shouldUpdateButtonWithFormData() { - if ( this.context !== 'checkout' ) { + if ( CONTEXT.Checkout !== this.context ) { return false; } return ( @@ -353,7 +400,7 @@ class ApplePayButton { * Indicates how payment completion should be handled if with the context handler default * actions. Or with Apple Pay module specific completion. * - * @return {boolean} + * @return {boolean} True, when the Apple Pay data should be submitted to WooCommerce. */ shouldCompletePaymentWithContextHandler() { // Data already handled, ex: PayNow @@ -363,7 +410,7 @@ class ApplePayButton { // Use WC form data mode in Checkout. return ( - this.context === 'checkout' && + CONTEXT.Checkout === this.context && ! this.shouldUpdateButtonWithFormData() ); } @@ -371,7 +418,7 @@ class ApplePayButton { /** * Updates Apple Pay paymentRequest with form data. * - * @param paymentRequest + * @param {Object} paymentRequest Object to extend with form data. */ updateRequestDataWithForm( paymentRequest ) { if ( ! this.shouldUpdateButtonWithFormData() ) { @@ -481,11 +528,11 @@ class ApplePayButton { } refreshContextData() { - if ( 'product' === this.context ) { - // Refresh product data that makes the price change. + if ( CONTEXT.Product === this.context ) { + // Refresh product data that makes the price change. this.productQuantity = document.querySelector( 'input.qty' )?.value; - this.products = this.contextHandler.products(); - this.log( 'Products updated', this.products ); + this.products = this.contextHandler.products(); + this.log( 'Products updated', this.products ); } } @@ -518,8 +565,9 @@ class ApplePayButton { * * @see https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778021-onvalidatemerchant * - * @param session - * @return {(function(*): void)|*} + * @param {Object} session The ApplePaySession object. + * + * @return {(function(*): void)|*} Callback that runs after the merchant validation */ onValidateMerchant( session ) { return ( applePayValidateMerchantEvent ) => { @@ -548,14 +596,14 @@ class ApplePayButton { onShippingMethodSelected( session ) { this.log( 'onshippingmethodselected', this.buttonConfig.ajax_url ); - const ajax_url = this.buttonConfig.ajax_url; + const ajaxUrl = this.buttonConfig.ajax_url; return ( event ) => { this.log( 'onshippingmethodselected call' ); const data = this.getShippingMethodData( event ); jQuery.ajax( { - url: ajax_url, + url: ajaxUrl, method: 'POST', data, success: ( @@ -599,7 +647,7 @@ class ApplePayButton { onShippingContactSelected( session ) { this.log( 'onshippingcontactselected', this.buttonConfig.ajax_url ); - const ajax_url = this.buttonConfig.ajax_url; + const ajaxUrl = this.buttonConfig.ajax_url; return ( event ) => { this.log( 'onshippingcontactselected call' ); @@ -607,7 +655,7 @@ class ApplePayButton { const data = this.getShippingContactData( event ); jQuery.ajax( { - url: ajax_url, + url: ajaxUrl, method: 'POST', data, success: ( @@ -637,15 +685,15 @@ class ApplePayButton { } getShippingContactData( event ) { - const product_id = this.buttonConfig.product.id; + const productId = this.buttonConfig.product.id; this.refreshContextData(); switch ( this.context ) { - case 'product': + case CONTEXT.Product: return { action: 'ppcp_update_shipping_contact', - product_id, + product_id: productId, products: JSON.stringify( this.products ), caller_page: 'productDetail', product_quantity: this.productQuantity, @@ -653,11 +701,12 @@ class ApplePayButton { need_shipping: this.shouldRequireShippingInButton(), 'woocommerce-process-checkout-nonce': this.nonce, }; - case 'cart': - case 'checkout': - case 'cart-block': - case 'checkout-block': - case 'mini-cart': + + case CONTEXT.Cart: + case CONTEXT.Checkout: + case CONTEXT.BlockCart: + case CONTEXT.BlockCheckout: + case CONTEXT.MiniCart: return { action: 'ppcp_update_shipping_contact', simplified_contact: event.shippingContact, @@ -669,12 +718,12 @@ class ApplePayButton { } getShippingMethodData( event ) { - const product_id = this.buttonConfig.product.id; + const productId = this.buttonConfig.product.id; this.refreshContextData(); switch ( this.context ) { - case 'product': + case CONTEXT.Product: return { action: 'ppcp_update_shipping_method', shipping_method: event.shippingMethod, @@ -682,17 +731,18 @@ class ApplePayButton { this.updatedContactInfo || this.initialPaymentRequest.shippingContact || this.initialPaymentRequest.billingContact, - product_id, + product_id: productId, products: JSON.stringify( this.products ), caller_page: 'productDetail', product_quantity: this.productQuantity, 'woocommerce-process-checkout-nonce': this.nonce, }; - case 'cart': - case 'checkout': - case 'cart-block': - case 'checkout-block': - case 'mini-cart': + + case CONTEXT.Cart: + case CONTEXT.Checkout: + case CONTEXT.BlockCart: + case CONTEXT.BlockCheckout: + case CONTEXT.MiniCart: return { action: 'ppcp_update_shipping_method', shipping_method: event.shippingMethod, @@ -711,10 +761,6 @@ class ApplePayButton { return async ( event ) => { this.log( 'onpaymentauthorized call' ); - function form() { - return document.querySelector( 'form.cart' ); - } - const processInWooAndCapture = async ( data ) => { return new Promise( ( resolve, reject ) => { try { @@ -729,7 +775,7 @@ class ApplePayButton { ( this.initialPaymentRequest.shippingMethods || [] )[ 0 ]; - const request_data = { + const requestData = { action: 'ppcp_create_order', caller_page: this.context, product_id: this.buttonConfig.product.id ?? null, @@ -754,7 +800,7 @@ class ApplePayButton { jQuery.ajax( { url: this.buttonConfig.ajax_url, method: 'POST', - data: request_data, + data: requestData, complete: ( jqXHR, textStatus ) => { this.log( 'onpaymentauthorized complete' ); }, @@ -829,15 +875,15 @@ class ApplePayButton { restart: () => new Promise( ( resolve, reject ) => { - approveFailed = true; - resolve(); + approveFailed = true; + resolve(); } ), order: { get: () => new Promise( ( resolve, reject ) => { - resolve( null ); + resolve( null ); } ), }, diff --git a/modules/ppcp-applepay/resources/js/ApplepayManager.js b/modules/ppcp-applepay/resources/js/ApplepayManager.js index 0992d9e7d..264610f28 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayManager.js +++ b/modules/ppcp-applepay/resources/js/ApplepayManager.js @@ -1,7 +1,9 @@ -import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; -import ApplepayButton from './ApplepayButton'; +/* global paypal */ -class ApplepayManager { +import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; +import ApplePayButton from './ApplepayButton'; + +class ApplePayManager { constructor( buttonConfig, ppcpConfig ) { this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; @@ -9,7 +11,7 @@ class ApplepayManager { this.buttons = []; buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => { - const button = new ApplepayButton( + const button = new ApplePayButton( bootstrap.context, bootstrap.handler, buttonConfig, @@ -40,8 +42,7 @@ class ApplepayManager { } /** - * Gets ApplePay configuration of the PayPal merchant. - * @return {Promise} + * Gets Apple Pay configuration of the PayPal merchant. */ async config() { this.ApplePayConfig = await paypal.Applepay().config(); @@ -49,4 +50,4 @@ class ApplepayManager { } } -export default ApplepayManager; +export default ApplePayManager; diff --git a/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js b/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js index 2f4db9d41..1c10bb997 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js +++ b/modules/ppcp-applepay/resources/js/ApplepayManagerBlockEditor.js @@ -1,6 +1,8 @@ -import ApplepayButton from './ApplepayButton'; +/* global paypal */ -class ApplepayManagerBlockEditor { +import ApplePayButton from './ApplepayButton'; + +class ApplePayManagerBlockEditor { constructor( buttonConfig, ppcpConfig ) { this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; @@ -17,7 +19,7 @@ class ApplepayManagerBlockEditor { try { this.applePayConfig = await paypal.Applepay().config(); - const button = new ApplepayButton( + const button = new ApplePayButton( this.ppcpConfig.context, null, this.buttonConfig, @@ -31,4 +33,4 @@ class ApplepayManagerBlockEditor { } } -export default ApplepayManagerBlockEditor; +export default ApplePayManagerBlockEditor; diff --git a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js index f763ac5d9..cc3a3aeb2 100644 --- a/modules/ppcp-applepay/resources/js/Context/BaseHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/BaseHandler.js @@ -1,6 +1,5 @@ import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler'; import CartActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler'; -import { isPayPalSubscription } from '../../../../ppcp-blocks/resources/js/Helper/Subscription'; class BaseHandler { constructor( buttonConfig, ppcpConfig ) { @@ -76,13 +75,6 @@ class BaseHandler { document.querySelector( '.woocommerce-notices-wrapper' ) ); } - - errorHandler() { - return new ErrorHandler( - this.ppcpConfig.labels.error.generic, - document.querySelector( '.woocommerce-notices-wrapper' ) - ); - } } export default BaseHandler; diff --git a/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js index 8740705e9..5febfe0c3 100644 --- a/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/PreviewHandler.js @@ -1,10 +1,6 @@ import BaseHandler from './BaseHandler'; class PreviewHandler extends BaseHandler { - constructor( buttonConfig, ppcpConfig, externalHandler ) { - super( buttonConfig, ppcpConfig, externalHandler ); - } - transactionInfo() { // We need to return something as ApplePay button initialization expects valid data. return { @@ -19,7 +15,7 @@ class PreviewHandler extends BaseHandler { throw new Error( 'Create order fail. This is just a preview.' ); } - approveOrder( data, actions ) { + approveOrder() { throw new Error( 'Approve order fail. This is just a preview.' ); } diff --git a/modules/ppcp-applepay/resources/js/boot-admin.js b/modules/ppcp-applepay/resources/js/boot-admin.js index 080d7c8aa..81d8c0b7b 100644 --- a/modules/ppcp-applepay/resources/js/boot-admin.js +++ b/modules/ppcp-applepay/resources/js/boot-admin.js @@ -1,4 +1,4 @@ -import ApplepayButton from './ApplepayButton'; +import ApplePayButton from './ApplepayButton'; import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton'; import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager'; @@ -86,7 +86,7 @@ class ApplePayPreviewButton extends PreviewButton { } createButton( buttonConfig ) { - const button = new ApplepayButton( + const button = new ApplePayButton( 'preview', null, buttonConfig, diff --git a/modules/ppcp-applepay/resources/js/boot-block.js b/modules/ppcp-applepay/resources/js/boot-block.js index 8445466eb..c447cb064 100644 --- a/modules/ppcp-applepay/resources/js/boot-block.js +++ b/modules/ppcp-applepay/resources/js/boot-block.js @@ -4,8 +4,8 @@ import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Help import { cartHasSubscriptionProducts } from '../../../ppcp-blocks/resources/js/Helper/Subscription'; import { loadCustomScript } from '@paypal/paypal-js'; import CheckoutHandler from './Context/CheckoutHandler'; -import ApplepayManager from './ApplepayManager'; -import ApplepayManagerBlockEditor from './ApplepayManagerBlockEditor'; +import ApplePayManager from './ApplepayManager'; +import ApplePayManagerBlockEditor from './ApplepayManagerBlockEditor'; const ppcpData = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); const ppcpConfig = ppcpData.scriptData; @@ -24,8 +24,8 @@ const ApplePayComponent = ( props ) => { const bootstrap = function () { const ManagerClass = props.isEditing - ? ApplepayManagerBlockEditor - : ApplepayManager; + ? ApplePayManagerBlockEditor + : ApplePayManager; const manager = new ManagerClass( buttonConfig, ppcpConfig ); manager.init(); }; diff --git a/modules/ppcp-applepay/resources/js/boot.js b/modules/ppcp-applepay/resources/js/boot.js index aee735231..8eddafbcb 100644 --- a/modules/ppcp-applepay/resources/js/boot.js +++ b/modules/ppcp-applepay/resources/js/boot.js @@ -1,13 +1,13 @@ import { loadCustomScript } from '@paypal/paypal-js'; import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; -import ApplepayManager from './ApplepayManager'; +import ApplePayManager from './ApplepayManager'; import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper'; ( function ( { buttonConfig, ppcpConfig, jQuery } ) { let manager; const bootstrap = function () { - manager = new ApplepayManager( buttonConfig, ppcpConfig ); + manager = new ApplePayManager( buttonConfig, ppcpConfig ); manager.init(); }; From f4abf7028e1bfbac98de2ec7e3d6199f563a3829 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 14:20:40 +0200 Subject: [PATCH 04/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Chore:=20Make=20isIn?= =?UTF-8?q?itialized=20flag=20private?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reason: The button instance is added to the global document scope, this helps to protect internal component state --- modules/ppcp-applepay/resources/js/ApplepayButton.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 3a1a13a00..d1a56d616 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -40,7 +40,7 @@ class ApplePayButton { * * @type {boolean} */ - isInitialized = false; + #isInitialized = false; /** * Context describes the button's location on the website and what details it submits. @@ -115,7 +115,7 @@ class ApplePayButton { * @return {boolean} True, if the button can be displayed. */ get isEligible() { - if ( ! this.isInitialized ) { + if ( ! this.#isInitialized ) { return true; } @@ -127,7 +127,7 @@ class ApplePayButton { } init( config ) { - if ( this.isInitialized ) { + if ( this.#isInitialized ) { return; } @@ -138,7 +138,7 @@ class ApplePayButton { this.log( 'Init', this.context ); this.initEventHandlers(); - this.isInitialized = true; + this.#isInitialized = true; this.applePayConfig = config; const idMinicart = this.buttonConfig.button.mini_cart_wrapper; @@ -178,7 +178,7 @@ class ApplePayButton { return; } - this.isInitialized = false; + this.#isInitialized = false; this.init( this.applePayConfig ); } From f75c3610d813522c6a2e35608e244f855ee7438f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 14:21:26 +0200 Subject: [PATCH 05/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Chore:=20Slightly=20?= =?UTF-8?q?improve=20the=20debug-log=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index d1a56d616..63368e305 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -77,20 +77,24 @@ class ApplePayButton { this.ppcpConfig ); - this.log = function () { - if ( this.buttonConfig.is_debug ) { - //console.log('[ApplePayButton]', ...arguments); - } - }; - this.refreshContextData(); // Debug helpers - jQuery( document ).on( 'ppcp-applepay-debug', () => { - console.log( 'ApplePayButton', this.context, this ); - } ); document.ppcpApplepayButtons = document.ppcpApplepayButtons || {}; document.ppcpApplepayButtons[ this.context ] = this; + + this.log = function () { + if ( ! this.buttonConfig.is_debug ) { + return; + } + console.log( `[ApplePayButton | ${ this.context }]`, ...arguments ); + }; + + if ( this.buttonConfig.is_debug ) { + jQuery( document ).on( 'ppcp-applepay-debug', () => { + this.log( this ); + } ); + } } /** @@ -135,7 +139,7 @@ class ApplePayButton { return; } - this.log( 'Init', this.context ); + this.log( 'Init' ); this.initEventHandlers(); this.#isInitialized = true; @@ -274,7 +278,7 @@ class ApplePayButton { * Adds an Apple Pay purchase button. */ addButton() { - this.log( 'addButton', this.context ); + this.log( 'addButton' ); const { wrapper, ppcpStyle } = this.contextConfig(); @@ -308,7 +312,7 @@ class ApplePayButton { * Show Apple Pay payment sheet when Apple Pay payment button is clicked */ async onButtonClick() { - this.log( 'onButtonClick', this.context ); + this.log( 'onButtonClick' ); const paymentRequest = this.paymentRequest(); From 43d7e0788fe4e5eeb58cd22546644ebb0e8d093c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 15:19:35 +0200 Subject: [PATCH 06/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Chore:=20Minor=20cod?= =?UTF-8?q?e=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 1 + modules/ppcp-applepay/src/ApplePayGateway.php | 13 +++--- .../src/Assets/ApplePayButton.php | 43 ++++++++----------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 63368e305..4f09d28d2 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -1,3 +1,4 @@ +/* global jQuery */ /* global ApplePaySession */ /* global PayPalCommerceGateway */ diff --git a/modules/ppcp-applepay/src/ApplePayGateway.php b/modules/ppcp-applepay/src/ApplePayGateway.php index 327ccae53..8b46dda64 100644 --- a/modules/ppcp-applepay/src/ApplePayGateway.php +++ b/modules/ppcp-applepay/src/ApplePayGateway.php @@ -9,17 +9,17 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Applepay; +use Exception; +use WC_Order; use WC_Payment_Gateway; +use WooCommerce\PayPalCommerce\Session\SessionHandler; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; -use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WC_Order; use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; -use Exception; use WooCommerce\PayPalCommerce\WcGateway\Exception\PayPalOrderMissingException; -use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\Messages; /** @@ -114,10 +114,7 @@ class ApplePayGateway extends WC_Payment_Gateway { add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, - array( - $this, - 'process_admin_options', - ) + array( $this, 'process_admin_options' ) ); } diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index b710e75ca..d9a9d5393 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -341,7 +341,7 @@ class ApplePayButton implements ButtonInterface { } $response = $this->response_templates->apple_formatted_response( $payment_details ); $this->response_templates->response_success( $response ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { $this->response_templates->response_with_data_errors( array( array( @@ -383,7 +383,7 @@ class ApplePayButton implements ButtonInterface { } $response = $this->response_templates->apple_formatted_response( $payment_details ); $this->response_templates->response_success( $response ); - } catch ( \Exception $e ) { + } catch ( Exception $e ) { $this->response_templates->response_with_data_errors( array( array( @@ -400,7 +400,7 @@ class ApplePayButton implements ButtonInterface { * On error returns an array of errors to be handled by the script * On success returns the new order data * - * @throws \Exception When validation fails. + * @throws Exception When validation fails. */ public function create_wc_order(): void { $applepay_request_data_object = $this->applepay_data_object_http(); @@ -421,15 +421,18 @@ class ApplePayButton implements ButtonInterface { $applepay_request_data_object->order_data( $context ); $this->update_posted_data( $applepay_request_data_object ); + if ( $context === 'product' ) { $cart_item_key = $this->prepare_cart( $applepay_request_data_object ); $cart = WC()->cart; $address = $applepay_request_data_object->shipping_address(); + $this->calculate_totals_single_product( $cart, $address, $applepay_request_data_object->shipping_method() ); + if ( ! $cart_item_key ) { $this->response_templates->response_with_data_errors( array( @@ -439,20 +442,17 @@ class ApplePayButton implements ButtonInterface { ), ) ); - return; - } + } else { add_filter( 'woocommerce_payment_successful_result', function ( array $result ) use ( $cart, $cart_item_key ) : array { - if ( ! is_string( $cart_item_key ) ) { - return $result; - } $this->clear_current_cart( $cart, $cart_item_key ); $this->reload_cart( $cart ); return $result; } ); } + } WC()->checkout()->process_checkout(); } @@ -461,17 +461,20 @@ class ApplePayButton implements ButtonInterface { /** * Checks if the nonce in the data object is valid * - * @return bool|int + * @return bool */ protected function is_nonce_valid(): bool { $nonce = filter_input( INPUT_POST, 'woocommerce-process-checkout-nonce', FILTER_SANITIZE_SPECIAL_CHARS ); if ( ! $nonce ) { return false; } - return wp_verify_nonce( + + // Return value 1 indicates "valid nonce, generated in past 12 hours". + // Return value 2 also indicated valid nonce, but older than 12 hours. + return 1 === wp_verify_nonce( $nonce, 'woocommerce-process_checkout' - ) === 1; + ); } /** @@ -512,7 +515,7 @@ class ApplePayButton implements ButtonInterface { $address, $applepay_request_data_object->shipping_method() ); - if ( is_string( $cart_item_key ) ) { + if ( $cart_item_key ) { $this->clear_current_cart( $cart, $cart_item_key ); $this->reload_cart( $cart ); } @@ -820,9 +823,9 @@ class ApplePayButton implements ButtonInterface { /** * Removes the old cart, saves it, and creates a new one * + * @throws Exception If it cannot be added to cart. * @param ApplePayDataObjectHttp $applepay_request_data_object The request data object. - * @return bool | string The cart item key after adding to the new cart. - * @throws \Exception If it cannot be added to cart. + * @return string The cart item key after adding to the new cart. */ public function prepare_cart( ApplePayDataObjectHttp $applepay_request_data_object ): string { $this->save_old_cart(); @@ -839,7 +842,7 @@ class ApplePayButton implements ButtonInterface { ); $this->cart_products->add_products( array( $product ) ); - return $this->cart_products->cart_item_keys()[0]; + return $this->cart_products->cart_item_keys()[0] ?? ''; } /** @@ -982,6 +985,7 @@ class ApplePayButton implements ButtonInterface { return true; } + /** * ApplePay button markup */ @@ -993,15 +997,6 @@ class ApplePayButton implements ButtonInterface { Date: Wed, 24 Jul 2024 15:21:09 +0200 Subject: [PATCH 07/22] =?UTF-8?q?=E2=9C=A8=20Hide=20Apple=20Pay=20gateway?= =?UTF-8?q?=20on=20ineligible=20devices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 24 ++++++++++++-- .../src/Assets/ApplePayButton.php | 32 +++++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 4f09d28d2..21b410427 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -150,13 +150,31 @@ class ApplePayButton { const idButton = this.buttonConfig.button.wrapper; if ( ! this.isEligible ) { - jQuery( '#' + idButton ).hide(); - jQuery( '#' + idMinicart ).hide(); - jQuery( '#express-payment-method-ppcp-applepay' ).hide(); + const hideContainers = [ + // Payment button (Pay now, smart button block) + `#${ idButton }`, + // Mini Cart button + `#${ idMinicart }`, + // Block Checkout: Express checkout button. + '#express-payment-method-ppcp-applepay', + ]; + + hideContainers.forEach( ( selector ) => { + const elements = document.querySelectorAll( selector ); + + elements.forEach( ( element ) => { + element.style.display = 'none'; + } ); + } ); return; } + // Classic Checkout: Make the Apple Pay gateway visible. + document + .querySelectorAll( 'style#ppcp-hide-apple-pay' ) + .forEach( ( el ) => el.remove() ); + // Add click-handler to the button. const setupButtonEvents = ( id ) => { document diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index d9a9d5393..22adce359 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -443,15 +443,15 @@ class ApplePayButton implements ButtonInterface { ) ); } else { - add_filter( - 'woocommerce_payment_successful_result', - function ( array $result ) use ( $cart, $cart_item_key ) : array { - $this->clear_current_cart( $cart, $cart_item_key ); - $this->reload_cart( $cart ); - return $result; - } - ); - } + add_filter( + 'woocommerce_payment_successful_result', + function ( array $result ) use ( $cart, $cart_item_key ) : array { + $this->clear_current_cart( $cart, $cart_item_key ); + $this->reload_cart( $cart ); + return $result; + } + ); + } } WC()->checkout()->process_checkout(); @@ -953,6 +953,7 @@ class ApplePayButton implements ButtonInterface { $render_placeholder, function () { $this->applepay_button(); + $this->hide_gateway_until_eligible(); }, 21 ); @@ -997,6 +998,19 @@ class ApplePayButton implements ButtonInterface { + + Date: Wed, 24 Jul 2024 18:27:04 +0200 Subject: [PATCH 08/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Entangle=20code=20in?= =?UTF-8?q?to=20getters=20and=20atomic=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 290 +++++++++++++----- 1 file changed, 206 insertions(+), 84 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 21b410427..28ab2e990 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -1,4 +1,6 @@ +/* eslint-env browser */ /* global jQuery */ + /* global ApplePaySession */ /* global PayPalCommerceGateway */ @@ -11,6 +13,25 @@ import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons'; +/** + * Plugin-specific styling. + * + * Note that most properties of this object do not apply to the Apple Pay button. + * + * @typedef {Object} PPCPStyle + * @property {string} shape - Outline shape. + * @property {?number} height - Button height in pixel. + */ + +/** + * Style options that are defined by the Apple Pay SDK and are required to render the button. + * + * @typedef {Object} ApplePayStyle + * @property {string} type - Defines the button label. + * @property {string} color - Button color + * @property {string} lang - The locale; an empty string will apply the user-agent's language. + */ + /** * List of valid context values that the button can have. * @@ -43,6 +64,9 @@ class ApplePayButton { */ #isInitialized = false; + #wrapperId = ''; + #ppcpButtonWrapperId = ''; + /** * Context describes the button's location on the website and what details it submits. * @@ -92,9 +116,9 @@ class ApplePayButton { }; if ( this.buttonConfig.is_debug ) { - jQuery( document ).on( 'ppcp-applepay-debug', () => { + jQuery( document ).on( 'ppcp-applepay-debug', () => { this.log( this ); - } ); + } ); } } @@ -131,6 +155,127 @@ class ApplePayButton { return !! ( this.applePayConfig.isEligible && window.ApplePaySession ); } + /** + * Returns the wrapper ID for the current button context. + * The ID varies for the MiniCart context. + * + * @return {string} The wrapper-element's ID (without the `#` prefix). + */ + get wrapperId() { + if ( ! this.#wrapperId ) { + let id; + + if ( CONTEXT.MiniCart === this.context ) { + id = this.buttonConfig.button.mini_cart_wrapper; + } else { + id = this.buttonConfig.button.wrapper; + } + + this.#wrapperId = id.replace( /^#/, '' ); + } + + return this.#wrapperId; + } + + /** + * Returns the wrapper ID for the ppcpButton + * + * @return {string} The wrapper-element's ID (without the `#` prefix). + */ + get ppcpButtonWrapperId() { + if ( ! this.#ppcpButtonWrapperId ) { + let id; + + if ( CONTEXT.MiniCart === this.context ) { + id = this.ppcpConfig.button.mini_cart_wrapper; + } else if ( CONTEXT.Blocks.includes( this.context ) ) { + id = '#express-payment-method-ppcp-gateway-paypal'; + } else { + id = this.ppcpConfig.button.wrapper; + } + + this.#ppcpButtonWrapperId = id.replace( /^#/, '' ); + } + + return this.#ppcpButtonWrapperId; + } + + /** + * Returns the context-relevant PPCP style object. + * The style for the MiniCart context can be different. + * + * The PPCP style are custom style options, that are provided by this plugin. + * + * @return {PPCPStyle} The style object. + */ + get ppcpStyle() { + if ( CONTEXT.MiniCart === this.context ) { + return this.ppcpConfig.button.mini_cart_style; + } + + return this.ppcpConfig.button.style; + } + + /** + * Returns default style options that are propagated to and rendered by the Apple Pay button. + * + * These styles are the official style options provided by the Apple Pay SDK. + * + * @return {ApplePayStyle} The style object. + */ + get buttonStyle() { + return { + type: this.buttonConfig.button.type, + lang: this.buttonConfig.button.lang, + color: this.buttonConfig.button.color, + }; + } + + /** + * Returns the HTML element that wraps the current button + * + * @return {HTMLElement|null} The wrapper element, or null. + */ + get wrapperElement() { + return document.getElementById( this.wrapperId ); + } + + /** + * Returns an array of HTMLElements that belong to the payment button. + * + * @return {HTMLElement[]} List of payment button wrapper elements. + */ + get allElements() { + const selectors = []; + + // Payment button (Pay now, smart button block) + selectors.push( `#${ this.wrapperId }` ); + + // Block Checkout: Express checkout button. + if ( CONTEXT.Blocks.includes( this.context ) ) { + selectors.push( '#express-payment-method-ppcp-applepay' ); + } + + // Classic Checkout: Apple Pay gateway. + if ( CONTEXT.Checkout === this.context ) { + selectors.push( '.wc_payment_method.payment_method_ppcp-applepay' ); + } + + this.log( 'Wrapper Elements:', selectors ); + return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) => + Array.from( document.querySelectorAll( selector ) ) + ); + } + + /** + * Checks whether the main button-wrapper is present in the current DOM. + * + * @return {boolean} True, if the button context (wrapper element) is found. + */ + get isPresent() { + return this.wrapperElement instanceof HTMLElement; + } + init( config ) { if ( this.#isInitialized ) { return; @@ -146,54 +291,24 @@ class ApplePayButton { this.#isInitialized = true; this.applePayConfig = config; - const idMinicart = this.buttonConfig.button.mini_cart_wrapper; - const idButton = this.buttonConfig.button.wrapper; - if ( ! this.isEligible ) { - const hideContainers = [ - // Payment button (Pay now, smart button block) - `#${ idButton }`, - // Mini Cart button - `#${ idMinicart }`, - // Block Checkout: Express checkout button. - '#express-payment-method-ppcp-applepay', - ]; + this.hide(); + } else { + this.show(); - hideContainers.forEach( ( selector ) => { - const elements = document.querySelectorAll( selector ); + this.fetchTransactionInfo().then( () => { + const button = this.addButton(); - elements.forEach( ( element ) => { - element.style.display = 'none'; - } ); - } ); + if ( ! button ) { + return; + } - return; - } - - // Classic Checkout: Make the Apple Pay gateway visible. - document - .querySelectorAll( 'style#ppcp-hide-apple-pay' ) - .forEach( ( el ) => el.remove() ); - - // Add click-handler to the button. - const setupButtonEvents = ( id ) => { - document - .getElementById( id ) - ?.addEventListener( 'click', ( evt ) => { + button.addEventListener( 'click', ( evt ) => { evt.preventDefault(); this.onButtonClick(); } ); - }; - - this.fetchTransactionInfo().then( () => { - this.addButton(); - - if ( CONTEXT.MiniCart === this.context ) { - setupButtonEvents( idMinicart ); - } else { - setupButtonEvents( idButton ); - } - } ); + } ); + } } reinit() { @@ -205,40 +320,43 @@ class ApplePayButton { this.init( this.applePayConfig ); } + /** + * Hides all wrappers that belong to this ApplePayButton instance. + */ + hide() { + this.allElements.forEach( ( element ) => { + element.style.display = 'none'; + } ); + } + + /** + * Ensures all wrapper elements of this ApplePayButton instance are visible. + */ + show() { + if ( ! this.isPresent ) { + this.log( 'Cannot show button, wrapper is not present' ); + return; + } + + // Classic Checkout: Make the Apple Pay gateway visible after page load. + if ( CONTEXT.Checkout === this.context ) { + document + .querySelectorAll( 'style#ppcp-hide-apple-pay' ) + .forEach( ( el ) => el.remove() ); + } + + this.allElements.forEach( ( element ) => { + element.style.display = ''; + } ); + } + async fetchTransactionInfo() { this.transactionInfo = await this.contextHandler.transactionInfo(); } - /** - * Returns configurations relative to this button context. - */ - contextConfig() { - const config = {}; - - if ( CONTEXT.MiniCart === this.context ) { - config.wrapper = this.buttonConfig.button.mini_cart_wrapper; - config.ppcpStyle = this.ppcpConfig.button.mini_cart_style; - config.buttonStyle = this.buttonConfig.button.mini_cart_style; - config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper; - } else { - config.wrapper = this.buttonConfig.button.wrapper; - config.ppcpStyle = this.ppcpConfig.button.style; - config.buttonStyle = this.buttonConfig.button.style; - config.ppcpButtonWrapper = this.ppcpConfig.button.wrapper; - } - - // Block editor configuration. - if ( CONTEXT.Blocks.includes( this.context ) ) { - config.ppcpButtonWrapper = - '#express-payment-method-ppcp-gateway-paypal'; - } - - return config; - } - initEventHandlers() { - const { wrapper, ppcpButtonWrapper } = this.contextConfig(); - const wrapperId = '#' + wrapper; + const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`; + const wrapperId = `#${ this.wrapperId }`; if ( wrapperId === ppcpButtonWrapper ) { throw new Error( @@ -295,23 +413,23 @@ class ApplePayButton { /** * Adds an Apple Pay purchase button. + * + * @return {HTMLElement|null} The newly created `` element. Null on failure. */ addButton() { this.log( 'addButton' ); - const { wrapper, ppcpStyle } = this.contextConfig(); - - const appleContainer = document.getElementById( wrapper ); - const type = this.buttonConfig.button.type; - const language = this.buttonConfig.button.lang; - const color = this.buttonConfig.button.color; - const id = 'apple-' + wrapper; + const appleContainer = document.getElementById( this.wrapperId ); + const style = this.buttonStyle; + const id = 'apple-' + this.wrapperId; if ( ! appleContainer ) { - return; + return null; } - appleContainer.innerHTML = ``; + const ppcpStyle = this.ppcpStyle; + + appleContainer.innerHTML = ``; appleContainer.classList.add( 'ppcp-button-' + ppcpStyle.shape ); if ( ppcpStyle.height ) { @@ -321,6 +439,8 @@ class ApplePayButton { ); appleContainer.style.height = `${ ppcpStyle.height }px`; } + + return appleContainer.querySelector( 'apple-pay-button' ); } //------------------------ @@ -345,6 +465,7 @@ class ApplePayButton { PayPalCommerceGateway.labels.error.generic, document.querySelector( '.woocommerce-notices-wrapper' ) ); + try { const formData = new FormData( document.querySelector( checkoutFormSelector ) @@ -366,6 +487,7 @@ class ApplePayButton { PayPalCommerceGateway.ajax.validate_checkout.nonce ) : null; + if ( formValidator ) { try { const errors = await formValidator.validate( @@ -898,15 +1020,15 @@ class ApplePayButton { restart: () => new Promise( ( resolve, reject ) => { - approveFailed = true; - resolve(); + approveFailed = true; + resolve(); } ), order: { get: () => new Promise( ( resolve, reject ) => { - resolve( null ); + resolve( null ); } ), }, From 4a0e410f52487afb31d76816e1a96af2565a153d Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 18:55:22 +0200 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=94=A7=20Add=20missing=20config=20t?= =?UTF-8?q?o=20eslintrc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - env.browser: Linter recognizes browser elements, like `HTMLElement` - globals.jQuery: Library is present on all pages --- .eslintrc | 6 +++++- modules/ppcp-applepay/resources/js/ApplepayButton.js | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.eslintrc b/.eslintrc index 7a97d815d..947e3acbe 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,11 @@ { "extends": [ "plugin:@wordpress/eslint-plugin/recommended" ], + "env": { + "browser": true + }, "globals": { - "wc": true + "wc": true, + "jQuery": "readonly" }, "rules": { "no-console": ["error", { "allow": ["warn", "error"] }] diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 28ab2e990..3c9f0acf7 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -1,6 +1,3 @@ -/* eslint-env browser */ -/* global jQuery */ - /* global ApplePaySession */ /* global PayPalCommerceGateway */ From 5df93b2dd48d5f90c6e7d61bb778eda4182ec359 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 19:42:37 +0200 Subject: [PATCH 10/22] =?UTF-8?q?=F0=9F=92=AC=20Improve=20the=20gateway=20?= =?UTF-8?q?description=20for=20admins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/src/ApplePayGateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-applepay/src/ApplePayGateway.php b/modules/ppcp-applepay/src/ApplePayGateway.php index 8b46dda64..1cccda0f2 100644 --- a/modules/ppcp-applepay/src/ApplePayGateway.php +++ b/modules/ppcp-applepay/src/ApplePayGateway.php @@ -96,7 +96,7 @@ class ApplePayGateway extends WC_Payment_Gateway { $this->id = self::ID; $this->method_title = __( 'Apple Pay (via PayPal) ', 'woocommerce-paypal-payments' ); - $this->method_description = __( 'The separate payment gateway with the Apple Pay button. If disabled, the button is included in the PayPal gateway.', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'Display Apple Pay as a standalone payment option instead of bundling it with PayPal.', 'woocommerce-paypal-payments' ); $this->title = $this->get_option( 'title', __( 'Apple Pay', 'woocommerce-paypal-payments' ) ); $this->description = $this->get_option( 'description', '' ); From f1c5b70877f79880c17356ecaa6f3a81a293a599 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 20:02:27 +0200 Subject: [PATCH 11/22] =?UTF-8?q?=E2=9C=A8=20Extract=20payment=20button=20?= =?UTF-8?q?to=20own=20gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 3c9f0acf7..024782a1d 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -43,7 +43,10 @@ const CONTEXT = { BlockCart: 'cart-block', BlockCheckout: 'checkout-block', Preview: 'preview', + // Block editor contexts. Blocks: [ 'cart-block', 'checkout-block' ], + // Custom gateway contexts. + Gateways: [ 'checkout', 'pay-now' ], }; /** @@ -152,6 +155,22 @@ class ApplePayButton { return !! ( this.applePayConfig.isEligible && window.ApplePaySession ); } + /** + * Determines if the current payment button should be rendered as a stand-alone gateway. + * The return value `false` usually means, that the payment button is bundled with all available + * payment buttons. + * + * The decision depends on the button context (placement) and the plugin settings. + * + * @return {boolean} True, if the current button represents a stand-alone gateway. + */ + get isSeparateGateway() { + return ( + this.buttonConfig.is_wc_gateway_enabled && + CONTEXT.Gateways.includes( this.context ) + ); + } + /** * Returns the wrapper ID for the current button context. * The ID varies for the MiniCart context. @@ -164,6 +183,8 @@ class ApplePayButton { if ( CONTEXT.MiniCart === this.context ) { id = this.buttonConfig.button.mini_cart_wrapper; + } else if ( this.isSeparateGateway ) { + id = 'ppc-button-ppcp-applepay'; } else { id = this.buttonConfig.button.wrapper; } From cffaa7a92f7a14462753536cba8889cd58c145bd Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 20:03:23 +0200 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=92=84=20Fix=20button=20size=20in?= =?UTF-8?q?=20stand-alone=20gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 024782a1d..4d135bce5 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -437,28 +437,32 @@ class ApplePayButton { addButton() { this.log( 'addButton' ); - const appleContainer = document.getElementById( this.wrapperId ); + const wrapper = this.wrapperElement; const style = this.buttonStyle; const id = 'apple-' + this.wrapperId; - if ( ! appleContainer ) { + if ( ! wrapper ) { return null; } const ppcpStyle = this.ppcpStyle; - appleContainer.innerHTML = ``; - appleContainer.classList.add( 'ppcp-button-' + ppcpStyle.shape ); + wrapper.innerHTML = ``; + wrapper.classList.add( + `ppcp-button-${ ppcpStyle.shape }`, + 'ppcp-button-apm', + 'ppcp-button-applepay' + ); if ( ppcpStyle.height ) { - appleContainer.style.setProperty( + wrapper.style.setProperty( '--apple-pay-button-height', `${ ppcpStyle.height }px` ); - appleContainer.style.height = `${ ppcpStyle.height }px`; + wrapper.style.height = `${ ppcpStyle.height }px`; } - return appleContainer.querySelector( 'apple-pay-button' ); + return wrapper.querySelector( 'apple-pay-button' ); } //------------------------ From 354a9ff175ce72105034612af2bee7aa917db618 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 20:13:14 +0200 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=92=A1=20Better=20debug=20logs=20fo?= =?UTF-8?q?r=20show/hide=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/resources/js/ApplepayButton.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 4d135bce5..68edf2e6a 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -312,6 +312,12 @@ class ApplePayButton { if ( ! this.isEligible ) { this.hide(); } else { + // Bail if the button wrapper is not present; handles mini-cart logic on checkout page. + if ( ! this.isPresent ) { + this.log( 'Abort init (no wrapper found)' ); + return; + } + this.show(); this.fetchTransactionInfo().then( () => { @@ -342,6 +348,7 @@ class ApplePayButton { * Hides all wrappers that belong to this ApplePayButton instance. */ hide() { + this.log( 'Hide button' ); this.allElements.forEach( ( element ) => { element.style.display = 'none'; } ); @@ -351,8 +358,9 @@ class ApplePayButton { * Ensures all wrapper elements of this ApplePayButton instance are visible. */ show() { + this.log( 'Show button' ); if ( ! this.isPresent ) { - this.log( 'Cannot show button, wrapper is not present' ); + this.log( '!! Cannot show button, wrapper is not present' ); return; } From a4d3af848c378ae296e2da950e42433227a7f535 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 20:13:30 +0200 Subject: [PATCH 14/22] =?UTF-8?q?=F0=9F=A9=B9=20Hide=20gateway=20on=20PayN?= =?UTF-8?q?ow=20page=20when=20ineligible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/resources/js/ApplepayButton.js | 12 +++++------- modules/ppcp-applepay/src/Assets/ApplePayButton.php | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 68edf2e6a..227f1533b 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -275,7 +275,7 @@ class ApplePayButton { } // Classic Checkout: Apple Pay gateway. - if ( CONTEXT.Checkout === this.context ) { + if ( CONTEXT.Gateways.includes( this.context ) ) { selectors.push( '.wc_payment_method.payment_method_ppcp-applepay' ); } @@ -364,12 +364,10 @@ class ApplePayButton { return; } - // Classic Checkout: Make the Apple Pay gateway visible after page load. - if ( CONTEXT.Checkout === this.context ) { - document - .querySelectorAll( 'style#ppcp-hide-apple-pay' ) - .forEach( ( el ) => el.remove() ); - } + // Classic Checkout/PayNow: Make the Apple Pay gateway visible after page load. + document + .querySelectorAll( 'style#ppcp-hide-apple-pay' ) + .forEach( ( el ) => el.remove() ); this.allElements.forEach( ( element ) => { element.style.display = ''; diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 22adce359..bfcc88133 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -966,6 +966,7 @@ class ApplePayButton implements ButtonInterface { $render_placeholder, function () { $this->applepay_button(); + $this->hide_gateway_until_eligible(); }, 21 ); From b9b13d7b05718e9cc55ba76618e6b4a8d2f64a8e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 20:42:57 +0200 Subject: [PATCH 15/22] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Remove=20unnecessary?= =?UTF-8?q?=20styles=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/resources/css/styles.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/ppcp-applepay/resources/css/styles.scss b/modules/ppcp-applepay/resources/css/styles.scss index 26c43b63e..3818b8db5 100644 --- a/modules/ppcp-applepay/resources/css/styles.scss +++ b/modules/ppcp-applepay/resources/css/styles.scss @@ -52,8 +52,3 @@ } } } - -// Initially hide the APM button until it's explicitly activated. -#ppc-button-ppcp-applepay { - display: none; -} From 75f4a6f94a154fdc66fbc5ba586c237c92e235ac Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 24 Jul 2024 20:47:59 +0200 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=8E=A8=20Fully=20disable=20debug=20?= =?UTF-8?q?logic=20via=20WP=5FDEBUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 227f1533b..ce998f04c 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -104,18 +104,20 @@ class ApplePayButton { this.refreshContextData(); + this.log = () => {}; + // Debug helpers - document.ppcpApplepayButtons = document.ppcpApplepayButtons || {}; - document.ppcpApplepayButtons[ this.context ] = this; - - this.log = function () { - if ( ! this.buttonConfig.is_debug ) { - return; - } - console.log( `[ApplePayButton | ${ this.context }]`, ...arguments ); - }; - if ( this.buttonConfig.is_debug ) { + document.ppcpApplepayButtons = document.ppcpApplepayButtons || {}; + document.ppcpApplepayButtons[ this.context ] = this; + + this.log = function () { + console.log( + `[ApplePayButton | ${ this.context }]`, + ...arguments + ); + }; + jQuery( document ).on( 'ppcp-applepay-debug', () => { this.log( this ); } ); From 64fae60da44ee071c27c14733acfdbb5f5b262a3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 25 Jul 2024 15:48:35 +0200 Subject: [PATCH 17/22] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Undo=20a=20type-hint?= =?UTF-8?q?=20that=20causes=20CI=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our Stub object has a void return value; CI fails with this type-hint, as the Stub is not compatible with the actual object. --- modules/ppcp-wc-gateway/src/Settings/Settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/Settings.php b/modules/ppcp-wc-gateway/src/Settings/Settings.php index e91256653..e083a91d1 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Settings.php +++ b/modules/ppcp-wc-gateway/src/Settings/Settings.php @@ -117,7 +117,7 @@ class Settings implements ContainerInterface { /** * Stores the settings to the database. */ - public function persist() : bool { + public function persist() { return update_option( self::KEY, $this->settings ); } From a14276bf528f89fe8ddc7bbf2530b7b365986ee9 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Thu, 25 Jul 2024 16:50:00 +0200 Subject: [PATCH 18/22] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fix=20all=20reported?= =?UTF-8?q?=20phpcs=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Assets/DataToAppleButtonScripts.php | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php index 004500355..52663a4f0 100644 --- a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php +++ b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php @@ -73,7 +73,7 @@ class DataToAppleButtonScripts { * * @return array */ - private function get_apple_pay_data( array $product = [] ) : array { + private function get_apple_pay_data( array $product = array() ) : array { // true: Use Apple Pay as distinct gateway. // false: integrate it with the smart buttons. $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); @@ -128,6 +128,8 @@ class DataToAppleButtonScripts { /** * Check if the product needs shipping * + * @param WC_Product $product Product to check. + * * @return bool */ protected function check_if_need_shipping( WC_Product $product ) : bool { @@ -167,13 +169,15 @@ class DataToAppleButtonScripts { $product_price = $product->get_price(); $product_stock = $product->get_stock_status(); - return $this->get_apple_pay_data( array( - 'needShipping' => $product_need_shipping, - 'id' => $product_id, - 'price' => $product_price, - 'isVariation' => $is_variation, - 'stock' => $product_stock, - ) ); + return $this->get_apple_pay_data( + array( + 'needShipping' => $product_need_shipping, + 'id' => $product_id, + 'price' => $product_price, + 'isVariation' => $is_variation, + 'stock' => $product_stock, + ) + ); } /** @@ -187,10 +191,12 @@ class DataToAppleButtonScripts { return array(); } - return $this->get_apple_pay_data( array( - 'needShipping' => $cart->needs_shipping(), - 'subtotal' => $cart->get_subtotal(), - ) ); + return $this->get_apple_pay_data( + array( + 'needShipping' => $cart->needs_shipping(), + 'subtotal' => $cart->get_subtotal(), + ) + ); } /** @@ -201,10 +207,12 @@ class DataToAppleButtonScripts { * @return array */ protected function data_for_admin_page() : array { - $data = $this->get_apple_pay_data( array( - 'needShipping' => false, - 'subtotal' => 0, - ) ); + $data = $this->get_apple_pay_data( + array( + 'needShipping' => false, + 'subtotal' => 0, + ) + ); $data['is_admin'] = true; From cbf9bff80836946b28b8f31027668e82f9ec3399 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 26 Jul 2024 17:06:42 +0200 Subject: [PATCH 19/22] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20with=20undefin?= =?UTF-8?q?ed=20log()=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/ApplepayButton.js | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index ce998f04c..d84870fd9 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -43,8 +43,10 @@ const CONTEXT = { BlockCart: 'cart-block', BlockCheckout: 'checkout-block', Preview: 'preview', + // Block editor contexts. Blocks: [ 'cart-block', 'checkout-block' ], + // Custom gateway contexts. Gateways: [ 'checkout', 'pay-now' ], }; @@ -89,6 +91,8 @@ class ApplePayButton { initialPaymentRequest = null; constructor( context, externalHandler, buttonConfig, ppcpConfig ) { + this._initDebug( !! buttonConfig?.is_debug ); + apmButtonsInit( ppcpConfig ); this.context = context; @@ -103,25 +107,34 @@ class ApplePayButton { ); this.refreshContextData(); + } - this.log = () => {}; + /** + * NOOP log function to avoid errors when debugging is disabled. + */ + log() {} - // Debug helpers - if ( this.buttonConfig.is_debug ) { - document.ppcpApplepayButtons = document.ppcpApplepayButtons || {}; - document.ppcpApplepayButtons[ this.context ] = this; - - this.log = function () { - console.log( - `[ApplePayButton | ${ this.context }]`, - ...arguments - ); - }; - - jQuery( document ).on( 'ppcp-applepay-debug', () => { - this.log( this ); - } ); + /** + * Enables debugging tools, when the button's is_debug flag is set. + * + * @param {boolean} enableDebugging If debugging features should be enabled for this instance. + * @private + */ + _initDebug( enableDebugging ) { + if ( ! enableDebugging || this.#isInitialized ) { + return; } + + document.ppcpApplepayButtons = document.ppcpApplepayButtons || {}; + document.ppcpApplepayButtons[ this.context ] = this; + + this.log = ( ...args ) => { + console.log( `[ApplePayButton | ${ this.context }]`, ...args ); + }; + + jQuery( document ).on( 'ppcp-applepay-debug', () => { + this.log( this ); + } ); } /** From 09308f15b65183f145b06461db309be3aed2d773 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 26 Jul 2024 17:20:48 +0200 Subject: [PATCH 20/22] =?UTF-8?q?=F0=9F=90=9B=20Hide=20Apple=20Pay=20butto?= =?UTF-8?q?n=20until=20gateway=20is=20selected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-applepay/resources/css/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/ppcp-applepay/resources/css/styles.scss b/modules/ppcp-applepay/resources/css/styles.scss index 3818b8db5..3402b1a20 100644 --- a/modules/ppcp-applepay/resources/css/styles.scss +++ b/modules/ppcp-applepay/resources/css/styles.scss @@ -52,3 +52,7 @@ } } } + +#ppc-button-ppcp-applepay { + display: none; +} From c15715a02f5e2fb30502479b90894ab291998130 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 26 Jul 2024 17:28:30 +0200 Subject: [PATCH 21/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20More=20reliable=20el?= =?UTF-8?q?igibility-check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eligibility better aligns with the ApplePay SDK logic and will not insert a hidden button anymore. --- .../resources/js/ApplepayButton.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index d84870fd9..fb853182c 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -163,11 +163,26 @@ class ApplePayButton { return true; } - if ( this.buttonConfig.is_admin ) { + if ( CONTEXT.Preview === this.context ) { return true; } - return !! ( this.applePayConfig.isEligible && window.ApplePaySession ); + /** + * Ensure the ApplePaySession is available and accepts payments + * This check is required when using Apple Pay SDK v1; canMakePayments() returns false + * if the current device is not liked to iCloud or the Apple Wallet is not available + * for a different reason. + */ + try { + if ( ! window.ApplePaySession?.canMakePayments() ) { + return false; + } + } catch ( error ) { + console.warn( error ); + return false; + } + + return !! this.applePayConfig.isEligible; } /** From 1ac9b460b6aae0ee35dad718411628194c42f905 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Fri, 26 Jul 2024 18:32:33 +0200 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=90=9B=20Display=20the=20PayPal=20p?= =?UTF-8?q?ayment=20tabs=20in=20Gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-wc-gateway/services.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index ce41619ca..92acc6b0c 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -71,6 +71,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; return array( 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { @@ -198,6 +199,7 @@ return array( Settings::PAY_LATER_TAB_ID, AxoGateway::ID, GooglePayGateway::ID, + ApplePayGateway::ID, ), true ); @@ -220,6 +222,7 @@ return array( Settings::PAY_LATER_TAB_ID, Settings::CONNECTION_TAB_ID, GooglePayGateway::ID, + ApplePayGateway::ID, ), true );