From 8e7350ec88b665a4fb036f21c822c64942f45fd2 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Tue, 10 Sep 2024 10:26:26 +0300 Subject: [PATCH 01/30] Set email when creating order for express payment --- .../src/Helper/WooCommerceOrderCreator.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index 9d3326010..b2e351103 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper; use Exception; use RuntimeException; use WC_Cart; +use WC_Customer; use WC_Data_Exception; use WC_Order; use WC_Order_Item_Product; @@ -190,6 +191,7 @@ class WooCommerceOrderCreator { * @param WC_Cart $wc_cart The Cart. * @return void * @throws WC_Data_Exception|RuntimeException When failing to configure shipping. + * @psalm-suppress RedundantConditionGivenDocblockType */ protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void { $shipping_address = null; @@ -200,7 +202,16 @@ class WooCommerceOrderCreator { $address = $payer->address(); $payer_name = $payer->name(); + $wc_email = null; + $wc_customer = WC()->customer; + if ( $wc_customer instanceof WC_Customer ) { + $wc_email = $wc_customer->get_email(); + } + + $email = $wc_email ?: $payer->email_address(); + $billing_address = array( + 'email' => $email ? $email : '', 'first_name' => $payer_name ? $payer_name->given_name() : '', 'last_name' => $payer_name ? $payer_name->surname() : '', 'address_1' => $address ? $address->address_line_1() : '', From e99159a35b8f71ef50ac369cbb6a072f83331b65 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Tue, 10 Sep 2024 10:26:50 +0300 Subject: [PATCH 02/30] Fix function name --- modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index b2e351103..6cd3db05b 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -95,7 +95,7 @@ class WooCommerceOrderCreator { $this->configure_payment_source( $wc_order ); $this->configure_customer( $wc_order ); $this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping ); - $this->configure_shipping( $wc_order, $payer, $shipping, $wc_cart ); + $this->configure_addresses( $wc_order, $payer, $shipping, $wc_cart ); $this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() ); $wc_order->calculate_totals(); @@ -163,7 +163,7 @@ class WooCommerceOrderCreator { $item->set_total( $subscription_total ); $subscription->add_product( $product ); - $this->configure_shipping( $subscription, $payer, $shipping, $wc_cart ); + $this->configure_addresses( $subscription, $payer, $shipping, $wc_cart ); $this->configure_payment_source( $subscription ); $this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() ); @@ -193,7 +193,7 @@ class WooCommerceOrderCreator { * @throws WC_Data_Exception|RuntimeException When failing to configure shipping. * @psalm-suppress RedundantConditionGivenDocblockType */ - protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void { + protected function configure_addresses( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void { $shipping_address = null; $billing_address = null; $shipping_options = null; From b9118616d01232f450ccb1113771f20e266c1d95 Mon Sep 17 00:00:00 2001 From: "Alex P." Date: Tue, 10 Sep 2024 15:11:17 +0300 Subject: [PATCH 03/30] Use ?: --- modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php index 6cd3db05b..8341aa29a 100644 --- a/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php +++ b/modules/ppcp-button/src/Helper/WooCommerceOrderCreator.php @@ -211,7 +211,7 @@ class WooCommerceOrderCreator { $email = $wc_email ?: $payer->email_address(); $billing_address = array( - 'email' => $email ? $email : '', + 'email' => $email ?: '', 'first_name' => $payer_name ? $payer_name->given_name() : '', 'last_name' => $payer_name ? $payer_name->surname() : '', 'address_1' => $address ? $address->address_line_1() : '', From 368ad49d16ae623d2ec42c3deb49f8dd717295a3 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 18 Sep 2024 17:33:27 +0200 Subject: [PATCH 04/30] =?UTF-8?q?=F0=9F=8E=A8=20Minor=20changes=20in=20com?= =?UTF-8?q?ment=20linebreaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/GooglepayButton.js | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index d4d9df55f..97bfa6d2c 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -5,7 +5,6 @@ import { import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton'; import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import UpdatePaymentData from './Helper/UpdatePaymentData'; -import TransactionInfo from './Helper/TransactionInfo'; import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData'; import moduleStorage from './Helper/GooglePayStorage'; @@ -42,17 +41,11 @@ import moduleStorage from './Helper/GooglePayStorage'; * * @see https://developers.google.com/pay/api/web/reference/client * @typedef {Object} PaymentsClient - * @property {Function} createButton - The convenience method is used to - * generate a Google Pay payment button styled with the latest Google Pay branding for - * insertion into a webpage. - * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) - * method to determine a user's ability to return a form of payment from the Google Pay API. - * @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment - * sheet that allows selection of a payment method and optionally configured parameters - * @property {Function} onPaymentAuthorized - This method is called when a payment is - * authorized in the payment sheet. - * @property {Function} onPaymentDataChanged - This method handles payment data changes - * in the payment sheet such as shipping address and shipping options. + * @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage. + * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API. + * @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters + * @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet. + * @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options. */ /** @@ -62,18 +55,12 @@ import moduleStorage from './Helper/GooglePayStorage'; * @typedef {Object} TransactionInfo * @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code. * @property {string} countryCode - Optional. required for EEA countries, - * @property {string} transactionId - Optional. A unique ID that identifies a facilitation - * attempt. Highly encouraged for troubleshooting. - * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price - * used: - * @property {string} totalPrice - Required. Total monetary value of the transaction with an - * optional decimal precision of two decimal places. - * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet - * (e.g. subtotals, sales taxes, shipping charges, discounts etc.). - * @property {string} totalPriceLabel - Optional. Custom label for the total price within the - * display items. - * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the - * Google Pay payment sheet. + * @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting. + * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used. + * @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places. + * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.). + * @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items. + * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet. */ function payerDataFromPaymentResponse( response ) { From 643a23c6e02f2a13a9b6dbd7a8181c36d0875ca2 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Wed, 18 Sep 2024 17:34:20 +0200 Subject: [PATCH 05/30] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=200-amount=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When shipping is disabled, the shippingFee is `undefined` which also resulted in the amount to become `NaN` --- modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js index 9216ad7c9..de62926ad 100644 --- a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js +++ b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js @@ -8,6 +8,8 @@ export default class TransactionInfo { this.#country = country; this.#currency = currency; + shippingFee = this.toAmount( shippingFee ); + total = this.toAmount( total ); this.shippingFee = shippingFee; this.amount = total - shippingFee; } From 5891d80f75b143d98980895d36d32429a87c4523 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 23 Sep 2024 13:50:44 +0200 Subject: [PATCH 06/30] =?UTF-8?q?=F0=9F=8E=A8=20Code=20style=20changes=20&?= =?UTF-8?q?=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused imports - Use state instead of global window-variable (continuationFilled) - Apply codestyle rules (spaces, change one let to const) --- .../resources/js/checkout-block.js | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index d1b6ad990..4bec20aa6 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -19,8 +19,6 @@ import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap'; -import { keysToCamelCase } from '../../../ppcp-button/resources/js/modules/Helper/Utils'; -import { handleShippingOptionsChange } from '../../../ppcp-button/resources/js/modules/Helper/ShippingHandler'; const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); window.ppcpFundingSource = config.fundingSource; @@ -46,6 +44,7 @@ const PayPalComponent = ( { const { responseTypes } = emitResponse; const [ paypalOrder, setPaypalOrder ] = useState( null ); + const [ continuationFilled, setContinuationFilled ] = useState( false ); const [ gotoContinuationOnError, setGotoContinuationOnError ] = useState( false ); @@ -65,13 +64,10 @@ const PayPalComponent = ( { useEffect( () => { // fill the form if in continuation (for product or mini-cart buttons) - if ( - ! config.scriptData.continuation || - ! config.scriptData.continuation.order || - window.ppcpContinuationFilled - ) { + if ( continuationFilled || ! config.scriptData.continuation?.order ) { return; } + try { const paypalAddresses = paypalOrderToWcAddresses( config.scriptData.continuation.order @@ -80,9 +76,11 @@ const PayPalComponent = ( { .select( 'wc/store/cart' ) .getCustomerData(); const addresses = mergeWcAddress( wcAddresses, paypalAddresses ); + wp.data .dispatch( 'wc/store/cart' ) .setBillingAddress( addresses.billingAddress ); + if ( shippingData.needsShipping ) { wp.data .dispatch( 'wc/store/cart' ) @@ -92,9 +90,10 @@ const PayPalComponent = ( { // sometimes the PayPal address is missing, skip in this case. console.log( err ); } + // this useEffect should run only once, but adding this in case of some kind of full re-rendering - window.ppcpContinuationFilled = true; - }, [] ); + setContinuationFilled( true ); + }, [ shippingData, continuationFilled ] ); const createOrder = async ( data, actions ) => { try { @@ -364,19 +363,19 @@ const PayPalComponent = ( { }; const shouldHandleShippingInPayPal = () => { - return shouldskipFinalConfirmation() && config.needShipping + return shouldskipFinalConfirmation() && config.needShipping; }; - const shouldskipFinalConfirmation = () => { - if ( config.finalReviewEnabled ) { - return false; - } + const shouldskipFinalConfirmation = () => { + if ( config.finalReviewEnabled ) { + return false; + } - return ( - window.ppcpFundingSource !== 'venmo' || - ! config.scriptData.vaultingEnabled - ); - }; + return ( + window.ppcpFundingSource !== 'venmo' || + ! config.scriptData.vaultingEnabled + ); + }; let handleShippingOptionsChange = null; let handleShippingAddressChange = null; @@ -610,11 +609,11 @@ const PayPalComponent = ( { } return ( data, actions ) => { - let shippingAddressChange = shouldHandleShippingInPayPal() + const shippingAddressChange = shouldHandleShippingInPayPal() ? handleShippingAddressChange( data, actions ) : null; - return shippingAddressChange; + return shippingAddressChange; }; }; From c852bc92b50b38850ea6d0484e26b9ef797bd515 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 23 Sep 2024 17:14:28 +0200 Subject: [PATCH 07/30] =?UTF-8?q?=F0=9F=90=9B=20Fix=20express=20payment=20?= =?UTF-8?q?error=20in=20block=20cart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/checkout-block.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 4bec20aa6..21d959a4f 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -24,9 +24,10 @@ const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); window.ppcpFundingSource = config.fundingSource; let registeredContext = false; - let paypalScriptPromise = null; +const PAYPAL_GATEWAY_ID = 'ppcp-gateway'; + const PayPalComponent = ( { onClick, onClose, @@ -62,6 +63,27 @@ const PayPalComponent = ( { ? `${ config.id }-${ fundingSource }` : config.id; + /** + * The block cart displays express checkout buttons. Those buttons are handled by the + * PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons"). + * + * A possible bug in WooCommerce does not use the correct payment method ID for the express + * payment buttons inside the cart, but sends the ID of the _first_ active payment method. + * + * This function uses an internal WooCommerce dispatcher method to set the correct method ID. + */ + const enforcePaymentMethodForCart = () => { + // Do nothing, unless we're handling block cart express payment buttons. + if ( 'cart-block' !== config.scriptData.context ) { + return; + } + + // Set the active payment method to PAYPAL_GATEWAY_ID. + wp.data + .dispatch( 'wc/store/payment' ) + .__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} ); + }; + useEffect( () => { // fill the form if in continuation (for product or mini-cart buttons) if ( continuationFilled || ! config.scriptData.continuation?.order ) { @@ -230,6 +252,7 @@ const PayPalComponent = ( { location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError( true ); + enforcePaymentMethodForCart(); onSubmit(); } } catch ( err ) { @@ -321,6 +344,7 @@ const PayPalComponent = ( { location.href = getCheckoutRedirectUrl(); } else { setGotoContinuationOnError( true ); + enforcePaymentMethodForCart(); onSubmit(); } } catch ( err ) { From 8b6cb86dbb08a3871fd8d3fb7bb7d916c98933ff Mon Sep 17 00:00:00 2001 From: Narek Zakarian Date: Fri, 27 Sep 2024 19:11:35 +0400 Subject: [PATCH 08/30] Add Custom Placeholder Handling when rendering the card fields --- modules/ppcp-card-fields/resources/js/Render.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js index 146396288..a0efd78bd 100644 --- a/modules/ppcp-card-fields/resources/js/Render.js +++ b/modules/ppcp-card-fields/resources/js/Render.js @@ -8,11 +8,17 @@ function renderField( cardField, inputField ) { // Insert the PayPal card field after the original input field. const styles = cardFieldStyles( inputField ); - cardField( { style: { input: styles } } ).render( inputField.parentNode ); + const fieldOptions = {style: { input: styles },}; - // Hide the original input field. - hide( inputField, true ); - inputField.hidden = true; + if ( inputField.getAttribute( 'placeholder' ) ) { + fieldOptions.placeholder = inputField.getAttribute( 'placeholder' ); + } + + cardField( fieldOptions ).render( inputField.parentNode ); + + // Hide the original input field. + hide( inputField, true ); + inputField.hidden = true; } export function renderFields( cardFields ) { From 8b0e12c06d4be992b0ac4e99b3b29872637d382f Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 30 Sep 2024 18:29:02 +0200 Subject: [PATCH 09/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20code=20clean?= =?UTF-8?q?up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define class properties - Change visibility of properties - Add some comments --- .../resources/js/Components/FormFieldGroup.js | 71 ++++++++++++------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index b4318a43d..77ca2317b 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -1,30 +1,36 @@ class FormFieldGroup { + #stored; + #data = {}; + #active = false; + #baseSelector; + #contentSelector; + #fields = {}; + #template; + constructor( config ) { - this.data = {}; + this.#baseSelector = config.baseSelector; + this.#contentSelector = config.contentSelector; + this.#fields = config.fields || {}; + this.#template = config.template; + this.#stored = new Map(); - this.baseSelector = config.baseSelector; - this.contentSelector = config.contentSelector; - this.fields = config.fields || {}; - this.template = config.template; - - this.active = false; } setData( data ) { - this.data = data; + this.#data = data; this.refresh(); } dataValue( fieldKey ) { - if ( ! fieldKey || ! this.fields[ fieldKey ] ) { + if ( ! fieldKey || ! this.#fields[ fieldKey ] ) { return ''; } - if ( typeof this.fields[ fieldKey ].valueCallback === 'function' ) { - return this.fields[ fieldKey ].valueCallback( this.data ); + if ( typeof this.#fields[ fieldKey ].valueCallback === 'function' ) { + return this.#fields[ fieldKey ].valueCallback( this.#data ); } - const path = this.fields[ fieldKey ].valuePath; + const path = this.#fields[ fieldKey ].valuePath; if ( ! path ) { return ''; @@ -35,27 +41,42 @@ class FormFieldGroup { .reduce( ( acc, key ) => acc && acc[ key ] !== undefined ? acc[ key ] : undefined, - this.data + this.#data ); return value ? value : ''; } + /** + * Activate form group: Render a custom Fastlane UI to replace the WooCommerce form. + * + * Indicates: Ryan flow. + */ activate() { - this.active = true; + this.#active = true; this.refresh(); } + /** + * Deactivate form group: Remove the custom Fastlane UI - either display the default + * WooCommerce checkout form or no form at all (when no email was provided yet). + * + * Indicates: Gary flow / no email provided / not using Fastlane. + */ deactivate() { - this.active = false; + this.#active = false; this.refresh(); } toggle() { - this.active ? this.deactivate() : this.activate(); + if ( this.#active ) { + this.deactivate(); + } else { + this.activate(); + } } refresh() { - const content = document.querySelector( this.contentSelector ); + const content = document.querySelector( this.#contentSelector ); if ( ! content ) { return; @@ -63,10 +84,10 @@ class FormFieldGroup { content.innerHTML = ''; - if ( ! this.active ) { - this.hideField( this.contentSelector ); + if ( ! this.#active ) { + this.hideField( this.#contentSelector ); } else { - this.showField( this.contentSelector ); + this.showField( this.#contentSelector ); } Object.keys( this.fields ).forEach( ( key ) => { @@ -79,8 +100,8 @@ class FormFieldGroup { } } ); - if ( typeof this.template === 'function' ) { - content.innerHTML = this.template( { + if ( typeof this.#template === 'function' ) { + content.innerHTML = this.#template( { value: ( fieldKey ) => { return this.dataValue( fieldKey ); }, @@ -100,7 +121,7 @@ class FormFieldGroup { showField( selector ) { const field = document.querySelector( - this.baseSelector + ' ' + selector + this.#baseSelector + ' ' + selector ); if ( field ) { field.classList.remove( 'ppcp-axo-field-hidden' ); @@ -109,7 +130,7 @@ class FormFieldGroup { hideField( selector ) { const field = document.querySelector( - this.baseSelector + ' ' + selector + this.#baseSelector + ' ' + selector ); if ( field ) { field.classList.add( 'ppcp-axo-field-hidden' ); @@ -117,7 +138,7 @@ class FormFieldGroup { } inputElement( name ) { - const baseSelector = this.fields[ name ].selector; + const baseSelector = this.#fields[ name ].selector; const select = document.querySelector( baseSelector + ' select' ); if ( select ) { From 89fa0f1fa7768a343b435563eef3e55570df783c Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 30 Sep 2024 18:57:00 +0200 Subject: [PATCH 10/30] =?UTF-8?q?=E2=9C=A8=20New=20utility=20function=20to?= =?UTF-8?q?=20loop=20all=20group-fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/FormFieldGroup.js | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index 77ca2317b..e7394e731 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -90,13 +90,11 @@ class FormFieldGroup { this.showField( this.#contentSelector ); } - Object.keys( this.fields ).forEach( ( key ) => { - const field = this.fields[ key ]; - - if ( this.active && ! field.showInput ) { - this.hideField( field.selector ); + this.loopFields( ( { selector } ) => { + if ( this.#active /* && ! field.showInput */ ) { + this.hideField( selector ); } else { - this.showField( field.selector ); + this.showField( selector ); } } ); @@ -107,18 +105,39 @@ class FormFieldGroup { }, isEmpty: () => { let isEmpty = true; - Object.keys( this.fields ).forEach( ( fieldKey ) => { + + this.loopFields( ( field, fieldKey ) => { if ( this.dataValue( fieldKey ) ) { isEmpty = false; return false; } } ); + return isEmpty; }, } ); } } + /** + * Invoke a callback on every field in the current group. + * + * @param {(field: object, key: string) => void} callback + */ + loopFields( callback ) { + Object.keys( this.#fields ).forEach( ( key ) => { + const field = this.#fields[ key ]; + const fieldSelector = `${ this.#baseSelector } ${ field.selector }`; + + callback( + { + ...field, + }, + key + ); + } ); + } + showField( selector ) { const field = document.querySelector( this.#baseSelector + ' ' + selector @@ -159,9 +178,7 @@ class FormFieldGroup { } toSubmitData( data ) { - Object.keys( this.fields ).forEach( ( fieldKey ) => { - const field = this.fields[ fieldKey ]; - + this.loopFields( ( field, fieldKey ) => { if ( ! field.valuePath || ! field.selector ) { return true; } From 0abb2e0f8dc16df6f175b185a5d3f997cfaa4504 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 30 Sep 2024 18:59:13 +0200 Subject: [PATCH 11/30] =?UTF-8?q?=E2=9C=A8=20Store=20&=20restore=20the=20W?= =?UTF-8?q?oo=20form=20as=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Ryan flow starts, the values of the Woo-form are saved in an internal Map, and restored once the Ryan flow is disabled again (change email or select other payment gateway) --- .../resources/js/Components/FormFieldGroup.js | 55 +++++++++++++++++++ .../resources/js/Views/BillingView.js | 10 ++++ .../resources/js/Views/ShippingView.js | 10 ++++ 3 files changed, 75 insertions(+) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index e7394e731..ec7f69736 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -53,6 +53,7 @@ class FormFieldGroup { */ activate() { this.#active = true; + this.storeFormData(); this.refresh(); } @@ -64,6 +65,7 @@ class FormFieldGroup { */ deactivate() { this.#active = false; + this.restoreFormData(); this.refresh(); } @@ -131,6 +133,9 @@ class FormFieldGroup { callback( { + inputSelector: field.inputName + ? `${ fieldSelector } [name="${ field.inputName }"]` + : '', ...field, }, key @@ -138,6 +143,56 @@ class FormFieldGroup { } ); } + /** + * Stores the current form data in an internal storage. + * This allows the original form to be restored later. + */ + storeFormData() { + const storeValue = ( field, name ) => { + if ( 'checkbox' === field.type || 'radio' === field.type ) { + this.#stored.set( name, field.checked ); + } else { + this.#stored.set( name, field.value ); + } + }; + + this.loopFields( ( { inputSelector }, fieldKey ) => { + if ( inputSelector && ! this.#stored.has( fieldKey ) ) { + const elInput = document.querySelector( inputSelector ); + + if ( elInput ) { + storeValue( elInput, fieldKey ); + } + } + } ); + } + + /** + * Restores the form data to its initial state before the form group was activated. + * This function iterates through the stored form fields and resets their values or states. + */ + restoreFormData() { + const restoreValue = ( field, name ) => { + if ( 'checkbox' === field.type || 'radio' === field.type ) { + field.checked = this.#stored.get( name ); + } else { + field.value = this.#stored.get( name ); + } + }; + + this.loopFields( ( { inputSelector }, fieldKey ) => { + if ( inputSelector && this.#stored.has( fieldKey ) ) { + const elInput = document.querySelector( inputSelector ); + + if ( elInput ) { + restoreValue( elInput, fieldKey ); + } + + this.#stored.delete( fieldKey ); + } + } ); + } + showField( selector ) { const field = document.querySelector( this.#baseSelector + ' ' + selector diff --git a/modules/ppcp-axo/resources/js/Views/BillingView.js b/modules/ppcp-axo/resources/js/Views/BillingView.js index c9047f417..7f62f7d64 100644 --- a/modules/ppcp-axo/resources/js/Views/BillingView.js +++ b/modules/ppcp-axo/resources/js/Views/BillingView.js @@ -45,42 +45,52 @@ class BillingView { firstName: { selector: '#billing_first_name_field', valuePath: null, + inputName: 'billing_first_name', }, lastName: { selector: '#billing_last_name_field', valuePath: null, + inputName: 'billing_last_name', }, street1: { selector: '#billing_address_1_field', valuePath: 'billing.address.addressLine1', + inputName: 'billing_address_1', }, street2: { selector: '#billing_address_2_field', valuePath: null, + inputName: 'billing_address_2', }, postCode: { selector: '#billing_postcode_field', valuePath: 'billing.address.postalCode', + inputName: 'billing_postcode', }, city: { selector: '#billing_city_field', valuePath: 'billing.address.adminArea2', + inputName: 'billing_city', }, stateCode: { selector: '#billing_state_field', valuePath: 'billing.address.adminArea1', + inputName: 'billing_state', }, countryCode: { selector: '#billing_country_field', valuePath: 'billing.address.countryCode', + inputName: 'billing_country', }, company: { selector: '#billing_company_field', valuePath: null, + inputName: 'billing_company', }, phone: { selector: '#billing_phone_field', valuePath: 'billing.phoneNumber', + inputName: 'billing_phone', }, }, } ); diff --git a/modules/ppcp-axo/resources/js/Views/ShippingView.js b/modules/ppcp-axo/resources/js/Views/ShippingView.js index ba7ffb408..c656a010e 100644 --- a/modules/ppcp-axo/resources/js/Views/ShippingView.js +++ b/modules/ppcp-axo/resources/js/Views/ShippingView.js @@ -90,42 +90,52 @@ class ShippingView { key: 'firstName', selector: '#shipping_first_name_field', valuePath: 'shipping.name.firstName', + inputName: 'shipping_first_name', }, lastName: { selector: '#shipping_last_name_field', valuePath: 'shipping.name.lastName', + inputName: 'shipping_last_name', }, street1: { selector: '#shipping_address_1_field', valuePath: 'shipping.address.addressLine1', + inputName: 'shipping_address_1', }, street2: { selector: '#shipping_address_2_field', valuePath: null, + inputName: 'shipping_address_2', }, postCode: { selector: '#shipping_postcode_field', valuePath: 'shipping.address.postalCode', + inputName: 'shipping_postcode', }, city: { selector: '#shipping_city_field', valuePath: 'shipping.address.adminArea2', + inputName: 'shipping_city', }, stateCode: { selector: '#shipping_state_field', valuePath: 'shipping.address.adminArea1', + inputName: 'shipping_state', }, countryCode: { selector: '#shipping_country_field', valuePath: 'shipping.address.countryCode', + inputName: 'shipping_country', }, company: { selector: '#shipping_company_field', valuePath: null, + inputName: 'shipping_company', }, shipDifferentAddress: { selector: '#ship-to-different-address', valuePath: null, + inputName: 'ship_to_different_address', }, phone: { //'selector': '#billing_phone_field', // There is no shipping phone field. From 763f6a431e6e58ca6a4c3bee2d69669cf7683e77 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Oct 2024 15:03:38 +0200 Subject: [PATCH 12/30] Remove Axo from the Checkout block in the editor and add the ACDC card component in the editor view --- modules/ppcp-axo-block/src/AxoBlockModule.php | 8 +++++++- .../resources/js/advanced-card-checkout-block.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-axo-block/src/AxoBlockModule.php b/modules/ppcp-axo-block/src/AxoBlockModule.php index 2a52e0510..be16ef8f2 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -102,7 +102,13 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule add_action( 'woocommerce_blocks_payment_method_type_registration', function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { - $payment_method_registry->register( $c->get( 'axoblock.method' ) ); + /* Only register the method if we are not in the admin + * (to avoid two Debit & Credit Cards gateways in the + * checkout block in the editor: one from ACDC one from Axo). + */ + if ( ! is_admin() ) { + $payment_method_registry->register( $c->get( 'axoblock.method' ) ); + } } ); diff --git a/modules/ppcp-blocks/resources/js/advanced-card-checkout-block.js b/modules/ppcp-blocks/resources/js/advanced-card-checkout-block.js index 48df41935..cdce4fd87 100644 --- a/modules/ppcp-blocks/resources/js/advanced-card-checkout-block.js +++ b/modules/ppcp-blocks/resources/js/advanced-card-checkout-block.js @@ -7,7 +7,7 @@ registerPaymentMethod( { name: config.id, label:
, content: , - edit:
, + edit: , ariaLabel: config.title, canMakePayment: () => { return true; From 7dc3113be0f7988e84ec314a8a62852d5fa8b03c Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Oct 2024 15:48:27 +0200 Subject: [PATCH 13/30] Axo Block: Fix the Fastlane modal info message text overflow issue --- modules/ppcp-axo-block/resources/css/gateway.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/ppcp-axo-block/resources/css/gateway.scss b/modules/ppcp-axo-block/resources/css/gateway.scss index 36817cb1b..42a14bd6b 100644 --- a/modules/ppcp-axo-block/resources/css/gateway.scss +++ b/modules/ppcp-axo-block/resources/css/gateway.scss @@ -301,3 +301,13 @@ a.wc-block-axo-change-link { #shipping-fields .wc-block-components-checkout-step__heading { display: flex; } + +// 11. Fastlane modal info message fix +.wc-block-components-text-input { + .wc-block-components-form &, + & { + paypal-watermark { + white-space: wrap; + } + } +} From c15340160849b1da52247780c90702b82e3c8b4f Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Oct 2024 15:57:20 +0200 Subject: [PATCH 14/30] Fix PHPCS formatting issues --- modules/ppcp-axo-block/src/AxoBlockModule.php | 3 ++- .../src/AxoBlockPaymentMethod.php | 22 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/modules/ppcp-axo-block/src/AxoBlockModule.php b/modules/ppcp-axo-block/src/AxoBlockModule.php index be16ef8f2..669cc7cc5 100644 --- a/modules/ppcp-axo-block/src/AxoBlockModule.php +++ b/modules/ppcp-axo-block/src/AxoBlockModule.php @@ -102,7 +102,8 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule add_action( 'woocommerce_blocks_payment_method_type_registration', function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { - /* Only register the method if we are not in the admin + /* + * Only register the method if we are not in the admin * (to avoid two Debit & Credit Cards gateways in the * checkout block in the editor: one from ACDC one from Axo). */ diff --git a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php index bea2f09cd..136db8233 100644 --- a/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php +++ b/modules/ppcp-axo-block/src/AxoBlockPaymentMethod.php @@ -187,13 +187,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { } return array( - 'environment' => array( + 'environment' => array( 'is_sandbox' => $this->environment->current_environment() === 'sandbox', ), - 'widgets' => array( + 'widgets' => array( 'email' => 'render', ), - 'insights' => array( + 'insights' => array( 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ), 'session_id' => @@ -207,7 +207,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { : null, // Set to null if WC()->cart is null or get_total doesn't exist. ), ), - 'style_options' => array( + 'style_options' => array( 'root' => array( 'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '', 'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '', @@ -226,23 +226,23 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType { 'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '', ), ), - 'name_on_card' => $this->dcc_configuration->show_name_on_card(), - 'woocommerce' => array( + 'name_on_card' => $this->dcc_configuration->show_name_on_card(), + 'woocommerce' => array( 'states' => array( 'US' => WC()->countries->get_states( 'US' ), 'CA' => WC()->countries->get_states( 'CA' ), ), ), - 'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/', - 'module_url' => untrailingslashit( $this->module_url ), - 'ajax' => array( + 'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/', + 'module_url' => untrailingslashit( $this->module_url ), + 'ajax' => array( 'frontend_logger' => array( 'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ), ), ), - 'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '', - 'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, + 'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '', + 'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, ); } } From 9e03c503acfcea5c1ccabc2a99287c18432450fa Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 1 Oct 2024 16:16:53 +0200 Subject: [PATCH 15/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20common=20l?= =?UTF-8?q?ogic=20into=20a=20class-method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/FormFieldGroup.js | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index ec7f69736..b963f2637 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -13,7 +13,6 @@ class FormFieldGroup { this.#fields = config.fields || {}; this.#template = config.template; this.#stored = new Map(); - } setData( data ) { @@ -46,6 +45,25 @@ class FormFieldGroup { return value ? value : ''; } + /** + * Changes the value of the input field. + * + * @param {Element|null} field + * @param {string|boolean} value + */ + #setFieldValue( field, value ) { + if ( ! field ) { + return false; + } + + if ( 'checkbox' === field.type || 'radio' === field.type ) { + value = !! value; + field.checked = value; + } else { + field.value = value; + } + } + /** * Activate form group: Render a custom Fastlane UI to replace the WooCommerce form. * @@ -151,8 +169,10 @@ class FormFieldGroup { const storeValue = ( field, name ) => { if ( 'checkbox' === field.type || 'radio' === field.type ) { this.#stored.set( name, field.checked ); + this.#setFieldValue( field, this.dataValue( name ) ); } else { this.#stored.set( name, field.value ); + this.#setFieldValue( field, '' ); } }; @@ -172,22 +192,11 @@ class FormFieldGroup { * This function iterates through the stored form fields and resets their values or states. */ restoreFormData() { - const restoreValue = ( field, name ) => { - if ( 'checkbox' === field.type || 'radio' === field.type ) { - field.checked = this.#stored.get( name ); - } else { - field.value = this.#stored.get( name ); - } - }; - this.loopFields( ( { inputSelector }, fieldKey ) => { if ( inputSelector && this.#stored.has( fieldKey ) ) { const elInput = document.querySelector( inputSelector ); - if ( elInput ) { - restoreValue( elInput, fieldKey ); - } - + this.#setFieldValue( elInput, this.#stored.get( fieldKey ) ); this.#stored.delete( fieldKey ); } } ); From 1e645a64e20441a908004b73847548664b336776 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 1 Oct 2024 16:20:37 +0200 Subject: [PATCH 16/30] =?UTF-8?q?=E2=9C=A8=20Sync=20Fastlane=20data=20with?= =?UTF-8?q?=20Woo=20cart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main change of this branch. Tell WooCommerce about the shipping address used by Fastlane, so taxes and shipping options are correct --- .../resources/js/Components/FormFieldGroup.js | 33 +++++++++++++++++++ .../resources/js/Views/ShippingView.js | 4 +++ 2 files changed, 37 insertions(+) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index b963f2637..b94932dd2 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -50,18 +50,24 @@ class FormFieldGroup { * * @param {Element|null} field * @param {string|boolean} value + * @return {boolean} True indicates that the previous value was different from the new value. */ #setFieldValue( field, value ) { if ( ! field ) { return false; } + let oldVal; if ( 'checkbox' === field.type || 'radio' === field.type ) { value = !! value; + oldVal = field.checked; field.checked = value; } else { + oldVal = field.value; field.value = value; } + + return oldVal !== value; } /** @@ -202,6 +208,33 @@ class FormFieldGroup { } ); } + /** + * Syncs the internal field-data with the hidden checkout form fields. + */ + syncDataToForm() { + if ( ! this.#active ) { + return; + } + + let formHasChanged = false; + + // Push data to the (hidden) checkout form. + this.loopFields( ( { inputSelector }, fieldKey ) => { + const elInput = inputSelector + ? document.querySelector( inputSelector ) + : null; + + if ( this.#setFieldValue( elInput, this.dataValue( fieldKey ) ) ) { + formHasChanged = true; + } + } ); + + // Tell WooCommerce about the changes. + if ( formHasChanged ) { + document.body.dispatchEvent( new Event( 'update_checkout' ) ); + } + } + showField( selector ) { const field = document.querySelector( this.#baseSelector + ' ' + selector diff --git a/modules/ppcp-axo/resources/js/Views/ShippingView.js b/modules/ppcp-axo/resources/js/Views/ShippingView.js index c656a010e..4853659b7 100644 --- a/modules/ppcp-axo/resources/js/Views/ShippingView.js +++ b/modules/ppcp-axo/resources/js/Views/ShippingView.js @@ -136,6 +136,8 @@ class ShippingView { selector: '#ship-to-different-address', valuePath: null, inputName: 'ship_to_different_address', + // Used by Woo to ensure correct location for taxes & shipping cost. + valueCallback: () => true, }, phone: { //'selector': '#billing_phone_field', // There is no shipping phone field. @@ -173,6 +175,7 @@ class ShippingView { activate() { this.group.activate(); + this.group.syncDataToForm(); } deactivate() { @@ -185,6 +188,7 @@ class ShippingView { setData( data ) { this.group.setData( data ); + this.group.syncDataToForm(); } toSubmitData( data ) { From 09e7c3670a7211ab00f5989a2538f8bc44c4bcf4 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 1 Oct 2024 16:27:21 +0200 Subject: [PATCH 17/30] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Small=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/FormFieldGroup.js | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index b94932dd2..027a0c0d7 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -151,20 +151,17 @@ class FormFieldGroup { * @param {(field: object, key: string) => void} callback */ loopFields( callback ) { - Object.keys( this.#fields ).forEach( ( key ) => { - const field = this.#fields[ key ]; - const fieldSelector = `${ this.#baseSelector } ${ field.selector }`; + for ( const [ key, field ] of Object.entries( this.#fields ) ) { + const { selector, inputName } = field; + const inputSelector = `${ selector } [name="${ inputName }"]`; - callback( - { - inputSelector: field.inputName - ? `${ fieldSelector } [name="${ field.inputName }"]` - : '', - ...field, - }, - key - ); - } ); + const fieldInfo = { + inputSelector: inputName ? inputSelector : '', + ...field, + }; + + callback( fieldInfo, key ); + } } /** From a897557f4f12195eadd288d846f241d928ef05c0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 1 Oct 2024 16:28:04 +0200 Subject: [PATCH 18/30] =?UTF-8?q?=F0=9F=90=9B=20Fix=20duplicate=20AXO=20ch?= =?UTF-8?q?eck=20on=20initial=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ppcp-axo/resources/js/AxoManager.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index d315ce374..551566042 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -801,8 +801,6 @@ class AxoManager { } async onChangeEmail() { - this.clearData(); - if ( ! this.status.active ) { log( 'Email checking skipped, AXO not active.' ); return; @@ -813,11 +811,17 @@ class AxoManager { return; } + if ( this.data.email === this.emailInput.value ) { + log( 'Email has not changed since last validation.' ); + return; + } + log( `Email changed: ${ this.emailInput ? this.emailInput.value : '' }` ); + this.clearData(); this.emailInput.value = this.stripSpaces( this.emailInput.value ); From d8d87ce1b817d63bf2133e1e40fcfb22052adb0e Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 1 Oct 2024 17:29:28 +0200 Subject: [PATCH 19/30] =?UTF-8?q?=F0=9F=90=9B=20Prevent=20infinite=20loop?= =?UTF-8?q?=20for=20invalid=20state=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/js/Components/FormFieldGroup.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index 027a0c0d7..67fdd58ec 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -53,10 +53,25 @@ class FormFieldGroup { * @return {boolean} True indicates that the previous value was different from the new value. */ #setFieldValue( field, value ) { + let oldVal; + + const isValidOption = () => { + for ( let i = 0; i < field.options.length; i++ ) { + if ( field.options[ i ].value === value ) { + return true; + } + } + return false; + }; + if ( ! field ) { return false; } - let oldVal; + + // If an invalid option is provided, do nothing. + if ( 'SELECT' === field.tagName && ! isValidOption() ) { + return false; + } if ( 'checkbox' === field.type || 'radio' === field.type ) { value = !! value; From 41e69fe556bbf6921bb702fcdf870453fea9af37 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Tue, 1 Oct 2024 18:16:15 +0200 Subject: [PATCH 20/30] =?UTF-8?q?=F0=9F=90=9B=20Refresh=20cart=20when=20ex?= =?UTF-8?q?iting=20Ryan=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After restoring the original WooCommerce form, also trigger a cart refresh Ajax call --- .../resources/js/Components/FormFieldGroup.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js index 67fdd58ec..2ea88feee 100644 --- a/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js +++ b/modules/ppcp-axo/resources/js/Components/FormFieldGroup.js @@ -210,14 +210,28 @@ class FormFieldGroup { * This function iterates through the stored form fields and resets their values or states. */ restoreFormData() { - this.loopFields( ( { inputSelector }, fieldKey ) => { - if ( inputSelector && this.#stored.has( fieldKey ) ) { - const elInput = document.querySelector( inputSelector ); + let formHasChanged = false; - this.#setFieldValue( elInput, this.#stored.get( fieldKey ) ); - this.#stored.delete( fieldKey ); + // Reset form fields to their initial state. + this.loopFields( ( { inputSelector }, fieldKey ) => { + if ( ! this.#stored.has( fieldKey ) ) { + return; + } + + const elInput = inputSelector + ? document.querySelector( inputSelector ) + : null; + const oldValue = this.#stored.get( fieldKey ); + this.#stored.delete( fieldKey ); + + if ( this.#setFieldValue( elInput, oldValue ) ) { + formHasChanged = true; } } ); + + if ( formHasChanged ) { + document.body.dispatchEvent( new Event( 'update_checkout' ) ); + } } /** From 2436ceb487785fc241c569428229aadd0b4ac8c3 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 3 Oct 2024 14:09:12 +0200 Subject: [PATCH 21/30] Axo Block: Improve code comments and fix minor misc bugs --- .../resources/js/components/Card/Card.js | 2 +- .../js/components/Card/CardChangeButton.js | 9 ++++ .../Card/CardChangeButtonManager.js | 12 ++++++ .../resources/js/components/Card/utils.js | 13 ++++++ .../js/components/EmailButton/EmailButton.js | 11 +++++ .../js/components/EmailButton/utils.js | 24 ++++++++++- .../js/components/Payment/Payment.js | 16 ++++++++ .../Shipping/ShippingChangeButton.js | 9 ++++ .../Shipping/ShippingChangeButtonManager.js | 12 ++++++ .../resources/js/components/Shipping/utils.js | 12 ++++++ .../js/components/Watermark/Watermark.js | 17 +++++++- .../components/Watermark/WatermarkManager.js | 12 ++++++ .../js/components/Watermark/utils.js | 37 ++++++++++++++++- .../resources/js/events/emailLookupManager.js | 27 +++++++++++- .../resources/js/helpers/classnamesManager.js | 9 +++- .../resources/js/helpers/fieldHelpers.js | 33 ++++++++++++--- .../resources/js/hooks/useAddressEditing.js | 14 +++++++ .../resources/js/hooks/useAxoCleanup.js | 15 +++++++ .../resources/js/hooks/useAxoSetup.js | 19 +++++++++ .../resources/js/hooks/useCardChange.js | 24 +++++++---- .../resources/js/hooks/useCustomerData.js | 10 +++++ .../resources/js/hooks/useDeleteEmptyKeys.js | 18 ++++++++ .../resources/js/hooks/useFastlaneSdk.js | 15 +++++++ .../js/hooks/useHandlePaymentSetup.js | 13 +++++- .../resources/js/hooks/usePayPalScript.js | 8 ++++ .../js/hooks/usePaymentSetupEffect.js | 13 ++++++ .../js/hooks/useShippingAddressChange.js | 13 ++++++ .../js/hooks/useTokenizeCustomerData.js | 30 +++++++++----- .../resources/js/stores/axoStore.js | 41 ++++++++++++++++--- 29 files changed, 449 insertions(+), 39 deletions(-) diff --git a/modules/ppcp-axo-block/resources/js/components/Card/Card.js b/modules/ppcp-axo-block/resources/js/components/Card/Card.js index 51dfa15ae..3bf39b10b 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/Card.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/Card.js @@ -5,7 +5,7 @@ import { STORE_NAME } from '../../stores/axoStore'; const cardIcons = { VISA: 'visa-light.svg', - MASTER_CARD: 'mastercard-light.svg', + MASTERCARD: 'mastercard-light.svg', AMEX: 'amex-light.svg', DISCOVER: 'discover-light.svg', DINERS: 'dinersclub-light.svg', diff --git a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js index 1779d67b6..c2a4eaa65 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js @@ -1,6 +1,13 @@ import { createElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +/** + * Renders a button to change the selected card in the checkout process. + * + * @param {Object} props + * @param {Function} props.onChangeButtonClick - Callback function to handle the click event. + * @return {JSX.Element} The rendered button as an anchor tag. + */ const CardChangeButton = ( { onChangeButtonClick } ) => createElement( 'a', @@ -9,7 +16,9 @@ const CardChangeButton = ( { onChangeButtonClick } ) => 'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link', role: 'button', onClick: ( event ) => { + // Prevent default anchor behavior event.preventDefault(); + // Call the provided click handler onChangeButtonClick(); }, }, diff --git a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js index c38e6c0b6..2b6deba4f 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js @@ -1,6 +1,13 @@ import { createElement, createRoot, useEffect } from '@wordpress/element'; import CardChangeButton from './CardChangeButton'; +/** + * Manages the insertion and removal of the CardChangeButton in the DOM. + * + * @param {Object} props + * @param {Function} props.onChangeButtonClick - Callback function for when the card change button is clicked. + * @return {null} This component doesn't render any visible elements directly. + */ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { useEffect( () => { const radioLabelElement = document.getElementById( @@ -8,14 +15,17 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { ); if ( radioLabelElement ) { + // Check if the change button doesn't already exist if ( ! radioLabelElement.querySelector( '.wc-block-checkout-axo-block-card__edit' ) ) { + // Create a new container for the button const buttonContainer = document.createElement( 'div' ); radioLabelElement.appendChild( buttonContainer ); + // Create a React root and render the CardChangeButton const root = createRoot( buttonContainer ); root.render( createElement( CardChangeButton, { onChangeButtonClick } ) @@ -23,6 +33,7 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { } } + // Cleanup function to remove the button when the component unmounts return () => { const button = document.querySelector( '.wc-block-checkout-axo-block-card__edit' @@ -33,6 +44,7 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { }; }, [ onChangeButtonClick ] ); + // This component doesn't render anything directly return null; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Card/utils.js b/modules/ppcp-axo-block/resources/js/components/Card/utils.js index 93f903142..915511885 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/utils.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/utils.js @@ -1,18 +1,31 @@ import { createElement, createRoot } from '@wordpress/element'; import CardChangeButtonManager from './CardChangeButtonManager'; +/** + * Injects a card change button into the DOM. + * + * @param {Function} onChangeButtonClick - Callback function for when the card change button is clicked. + */ export const injectCardChangeButton = ( onChangeButtonClick ) => { + // Create a container for the button const container = document.createElement( 'div' ); document.body.appendChild( container ); + + // Render the CardChangeButtonManager in the new container createRoot( container ).render( createElement( CardChangeButtonManager, { onChangeButtonClick } ) ); }; +/** + * Removes the card change button from the DOM if it exists. + */ export const removeCardChangeButton = () => { const button = document.querySelector( '.wc-block-checkout-axo-block-card__edit' ); + + // Remove the button's parent node if it exists if ( button && button.parentNode ) { button.parentNode.remove(); } diff --git a/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js b/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js index ab41f067e..a7fcbb86d 100644 --- a/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js +++ b/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js @@ -2,7 +2,15 @@ import { STORE_NAME } from '../../stores/axoStore'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +/** + * Renders a submit button for email input in the AXO checkout process. + * + * @param {Object} props + * @param {Function} props.handleSubmit - Function to handle button click/submit. + * @return {JSX.Element|null} The rendered button or null if conditions are not met. + */ const EmailButton = ( { handleSubmit } ) => { + // Select relevant states from the AXO store const { isGuest, isAxoActive, isEmailSubmitted } = useSelect( ( select ) => ( { isGuest: select( STORE_NAME ).getIsGuest(), @@ -11,6 +19,7 @@ const EmailButton = ( { handleSubmit } ) => { } ) ); + // Only render the button for guests when AXO is active if ( ! isGuest || ! isAxoActive ) { return null; } @@ -24,6 +33,7 @@ const EmailButton = ( { handleSubmit } ) => { }` } disabled={ isEmailSubmitted } > + { /* Button text */ } { > { __( 'Continue', 'woocommerce-paypal-payments' ) } + { /* Loading spinner */ } { isEmailSubmitted && ( { if ( ! emailInput ) { emailInput = document.getElementById( 'email' ); @@ -18,6 +24,11 @@ const getEmailInput = () => { return emailInput; }; +/** + * Sets up email functionality for AXO checkout. + * + * @param {Function} onEmailSubmit - Callback function to handle email submission. + */ export const setupEmailFunctionality = ( onEmailSubmit ) => { const input = getEmailInput(); if ( ! input ) { @@ -28,6 +39,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { return; } + // Handler for email submission const handleEmailSubmit = async () => { const isEmailSubmitted = wp.data .select( STORE_NAME ) @@ -50,6 +62,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { } }; + // Set up keydown handler for Enter key keydownHandler = ( event ) => { const isAxoActive = wp.data.select( STORE_NAME ).getIsAxoActive(); if ( event.key === 'Enter' && isAxoActive ) { @@ -78,6 +91,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { ); } + // Function to render the EmailButton const renderButton = () => { if ( submitButtonReference.root ) { submitButtonReference.root.render( @@ -90,12 +104,15 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { renderButton(); - // Subscribe to state changes + // Subscribe to state changes and re-render button submitButtonReference.unsubscribe = wp.data.subscribe( () => { renderButton(); } ); }; +/** + * Removes email functionality and cleans up event listeners and DOM elements. + */ export const removeEmailFunctionality = () => { const input = getEmailInput(); if ( input && keydownHandler ) { @@ -120,6 +137,11 @@ export const removeEmailFunctionality = () => { keydownHandler = null; }; +/** + * Checks if email functionality is currently set up. + * + * @return {boolean} True if email functionality is set up, false otherwise. + */ export const isEmailFunctionalitySetup = () => { return !! submitButtonReference.root; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js b/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js index d6c96c51c..e1191c8ce 100644 --- a/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js +++ b/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js @@ -4,8 +4,18 @@ import { __ } from '@wordpress/i18n'; import { Card } from '../Card'; import { STORE_NAME } from '../../stores/axoStore'; +/** + * Renders the payment component based on the user's state (guest or authenticated). + * + * @param {Object} props + * @param {Object} props.fastlaneSdk - The Fastlane SDK instance. + * @param {Function} props.onPaymentLoad - Callback function when payment component is loaded. + * @return {JSX.Element} The rendered payment component. + */ export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => { const [ isCardElementReady, setIsCardElementReady ] = useState( false ); + + // Select relevant states from the AXO store const { isGuest, isEmailLookupCompleted } = useSelect( ( select ) => ( { isGuest: select( STORE_NAME ).getIsGuest(), @@ -31,16 +41,22 @@ export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => { onPaymentLoad, ] ); + // Set card element ready when guest email lookup is completed useEffect( () => { if ( isGuest && isEmailLookupCompleted ) { setIsCardElementReady( true ); } }, [ isGuest, isEmailLookupCompleted ] ); + // Load payment component when dependencies change useEffect( () => { loadPaymentComponent(); }, [ loadPaymentComponent ] ); + // Conditional rendering based on user state: + // 1. If authenticated: Render the Card component + // 2. If guest with completed email lookup: Render the card fields + // 3. If guest without completed email lookup: Render a message to enter email if ( isGuest ) { if ( isEmailLookupCompleted ) { return
; diff --git a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js index b7d55d508..a68de9789 100644 --- a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js +++ b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js @@ -1,11 +1,20 @@ import { __ } from '@wordpress/i18n'; +/** + * Renders a button to change the shipping address. + * + * @param {Object} props + * @param {Function} props.onChangeShippingAddressClick - Callback function to handle the click event. + * @return {JSX.Element} The rendered button as an anchor tag. + */ const ShippingChangeButton = ( { onChangeShippingAddressClick } ) => ( { + // Prevent default anchor behavior event.preventDefault(); + // Call the provided click handler onChangeShippingAddressClick(); } } > diff --git a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js index e072f0193..abbd7e4da 100644 --- a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js +++ b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js @@ -1,22 +1,32 @@ import { useEffect, createRoot } from '@wordpress/element'; import ShippingChangeButton from './ShippingChangeButton'; +/** + * Manages the insertion and removal of the ShippingChangeButton in the DOM. + * + * @param {Object} props + * @param {Function} props.onChangeShippingAddressClick - Callback function for when the shipping change button is clicked. + * @return {null} This component doesn't render any visible elements directly. + */ const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => { useEffect( () => { const shippingHeading = document.querySelector( '#shipping-fields .wc-block-components-checkout-step__heading' ); + // Check if the shipping heading exists and doesn't already have a change button if ( shippingHeading && ! shippingHeading.querySelector( '.wc-block-checkout-axo-block-card__edit' ) ) { + // Create a new span element to contain the ShippingChangeButton const spanElement = document.createElement( 'span' ); spanElement.className = 'wc-block-checkout-axo-block-card__edit'; shippingHeading.appendChild( spanElement ); + // Create a React root and render the ShippingChangeButton const root = createRoot( spanElement ); root.render( { /> ); + // Cleanup function to remove the button when the component unmounts return () => { root.unmount(); spanElement.remove(); @@ -33,6 +44,7 @@ const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => { } }, [ onChangeShippingAddressClick ] ); + // This component doesn't render anything directly return null; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js b/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js index 1bce1ea3f..a131da555 100644 --- a/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js +++ b/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js @@ -1,14 +1,23 @@ import { createRoot } from '@wordpress/element'; import ShippingChangeButtonManager from './ShippingChangeButtonManager'; +/** + * Injects a shipping change button into the DOM if it doesn't already exist. + * + * @param {Function} onChangeShippingAddressClick - Callback function for when the shipping change button is clicked. + */ export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => { + // Check if the button already exists const existingButton = document.querySelector( '#shipping-fields .wc-block-checkout-axo-block-card__edit' ); if ( ! existingButton ) { + // Create a new container for the button const container = document.createElement( 'div' ); document.body.appendChild( container ); + + // Render the ShippingChangeButtonManager in the new container createRoot( container ).render( { } }; +/** + * Removes the shipping change button from the DOM if it exists. + */ export const removeShippingChangeButton = () => { const span = document.querySelector( '#shipping-fields .wc-block-checkout-axo-block-card__edit' diff --git a/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js b/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js index fcc098768..2ec89de90 100644 --- a/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js +++ b/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js @@ -1,6 +1,15 @@ import { useEffect, useRef } from '@wordpress/element'; import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug'; +/** + * Watermark component for displaying AXO watermark. + * + * @param {Object} props + * @param {Object} props.fastlaneSdk - The Fastlane SDK instance. + * @param {string} [props.name='fastlane-watermark-container'] - ID for the watermark container. + * @param {boolean} [props.includeAdditionalInfo=true] - Whether to include additional info in the watermark. + * @return {JSX.Element} The watermark container element. + */ const Watermark = ( { fastlaneSdk, name = 'fastlane-watermark-container', @@ -10,15 +19,19 @@ const Watermark = ( { const watermarkRef = useRef( null ); useEffect( () => { + /** + * Renders the Fastlane watermark. + */ const renderWatermark = async () => { if ( ! containerRef.current ) { return; } - // Clear the container + // Clear the container before rendering containerRef.current.innerHTML = ''; try { + // Create and render the Fastlane watermark const watermark = await fastlaneSdk.FastlaneWatermarkComponent( { includeAdditionalInfo, @@ -34,6 +47,7 @@ const Watermark = ( { renderWatermark(); + // Cleanup function to clear the container on unmount return () => { if ( containerRef.current ) { containerRef.current.innerHTML = ''; @@ -41,6 +55,7 @@ const Watermark = ( { }; }, [ fastlaneSdk, name, includeAdditionalInfo ] ); + // Render the container for the watermark return
; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js b/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js index 41c078585..ef175a98f 100644 --- a/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js +++ b/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js @@ -7,7 +7,15 @@ import { updateWatermarkContent, } from './utils'; +/** + * Manages the lifecycle and content of the AXO watermark. + * + * @param {Object} props + * @param {Object} props.fastlaneSdk - The Fastlane SDK instance. + * @return {null} This component doesn't render any visible elements. + */ const WatermarkManager = ( { fastlaneSdk } ) => { + // Select relevant states from the AXO store const isGuest = useSelect( ( select ) => select( STORE_NAME ).getIsGuest() ); @@ -20,6 +28,7 @@ const WatermarkManager = ( { fastlaneSdk } ) => { useEffect( () => { if ( isAxoActive || ( ! isAxoActive && ! isAxoScriptLoaded ) ) { + // Create watermark container and update content when AXO is active or loading createWatermarkContainer(); updateWatermarkContent( { isAxoActive, @@ -28,12 +37,15 @@ const WatermarkManager = ( { fastlaneSdk } ) => { isGuest, } ); } else { + // Remove watermark when AXO is inactive and not loading removeWatermark(); } + // Cleanup function to remove watermark on unmount return removeWatermark; }, [ fastlaneSdk, isGuest, isAxoActive, isAxoScriptLoaded ] ); + // This component doesn't render anything directly return null; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js b/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js index 38f4a7582..638bffe61 100644 --- a/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js +++ b/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js @@ -1,11 +1,15 @@ import { createElement, createRoot } from '@wordpress/element'; import { Watermark, WatermarkManager } from '../Watermark'; +// Object to store references to the watermark container and root const watermarkReference = { container: null, root: null, }; +/** + * Creates a container for the watermark in the checkout contact information block. + */ export const createWatermarkContainer = () => { const textInputContainer = document.querySelector( '.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input' @@ -16,6 +20,7 @@ export const createWatermarkContainer = () => { textInputContainer.querySelector( 'input[id="email"]' ); if ( emailInput ) { + // Create watermark container watermarkReference.container = document.createElement( 'div' ); watermarkReference.container.setAttribute( 'class', @@ -26,7 +31,7 @@ export const createWatermarkContainer = () => { '.wc-block-axo-email-submit-button-container' ); - // If possible, insert the watermark after the "Continue" button. + // Insert the watermark after the "Continue" button or email input const insertAfterElement = emailButton || emailInput; insertAfterElement.parentNode.insertBefore( @@ -34,6 +39,7 @@ export const createWatermarkContainer = () => { insertAfterElement.nextSibling ); + // Create a root for the watermark watermarkReference.root = createRoot( watermarkReference.container ); @@ -41,12 +47,19 @@ export const createWatermarkContainer = () => { } }; +/** + * Sets up the watermark manager component. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @return {Function} Cleanup function to remove the watermark. + */ export const setupWatermark = ( fastlaneSdk ) => { const container = document.createElement( 'div' ); document.body.appendChild( container ); const root = createRoot( container ); root.render( createElement( WatermarkManager, { fastlaneSdk } ) ); + // Return cleanup function return () => { root.unmount(); if ( container && container.parentNode ) { @@ -55,6 +68,9 @@ export const setupWatermark = ( fastlaneSdk ) => { }; }; +/** + * Removes the watermark from the DOM and resets the reference. + */ export const removeWatermark = () => { if ( watermarkReference.root ) { watermarkReference.root.unmount(); @@ -65,6 +81,7 @@ export const removeWatermark = () => { watermarkReference.container ); } else { + // Fallback removal if parent node is not available const detachedContainer = document.querySelector( '.wc-block-checkout-axo-block-watermark-container' ); @@ -73,15 +90,30 @@ export const removeWatermark = () => { } } } + // Reset watermark reference Object.assign( watermarkReference, { container: null, root: null } ); }; +/** + * Renders content in the watermark container. + * + * @param {ReactElement} content - The content to render. + */ export const renderWatermarkContent = ( content ) => { if ( watermarkReference.root ) { watermarkReference.root.render( content ); } }; +/** + * Updates the watermark content based on the current state. + * + * @param {Object} params - State parameters. + * @param {boolean} params.isAxoActive - Whether AXO is active. + * @param {boolean} params.isAxoScriptLoaded - Whether AXO script is loaded. + * @param {Object} params.fastlaneSdk - The Fastlane SDK instance. + * @param {boolean} params.isGuest - Whether the user is a guest. + */ export const updateWatermarkContent = ( { isAxoActive, isAxoScriptLoaded, @@ -89,6 +121,7 @@ export const updateWatermarkContent = ( { isGuest, } ) => { if ( ! isAxoActive && ! isAxoScriptLoaded ) { + // Show loading spinner renderWatermarkContent( createElement( 'span', { className: 'wc-block-components-spinner', @@ -96,6 +129,7 @@ export const updateWatermarkContent = ( { } ) ); } else if ( isAxoActive ) { + // Show Fastlane watermark renderWatermarkContent( createElement( Watermark, { fastlaneSdk, @@ -104,6 +138,7 @@ export const updateWatermarkContent = ( { } ) ); } else { + // Clear watermark content renderWatermarkContent( null ); } }; diff --git a/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js b/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js index 85efa0234..563f9510d 100644 --- a/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js +++ b/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js @@ -4,6 +4,21 @@ import { injectShippingChangeButton } from '../components/Shipping'; import { injectCardChangeButton } from '../components/Card'; import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore'; +/** + * Creates an email lookup handler function for AXO checkout. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @param {Function} setShippingAddress - Function to set shipping address in the store. + * @param {Function} setCardDetails - Function to set card details in the store. + * @param {Function} snapshotFields - Function to save current field values. + * @param {Object} wooShippingAddress - Current WooCommerce shipping address. + * @param {Object} wooBillingAddress - Current WooCommerce billing address. + * @param {Function} setWooShippingAddress - Function to update WooCommerce shipping address. + * @param {Function} setWooBillingAddress - Function to update WooCommerce billing address. + * @param {Function} onChangeShippingAddressClick - Handler for shipping address change. + * @param {Function} onChangeCardButtonClick - Handler for card change. + * @return {Function} The email lookup handler function. + */ export const createEmailLookupHandler = ( fastlaneSdk, setShippingAddress, @@ -20,6 +35,7 @@ export const createEmailLookupHandler = ( try { log( `Email value being looked up: ${ email }` ); + // Validate Fastlane SDK initialization if ( ! fastlaneSdk ) { throw new Error( 'FastlaneSDK is not initialized' ); } @@ -30,12 +46,13 @@ export const createEmailLookupHandler = ( ); } + // Perform email lookup const lookup = await fastlaneSdk.identity.lookupCustomerByEmail( email ); log( `Lookup response: ${ JSON.stringify( lookup ) }` ); - // Gary flow + // Handle Gary flow (new user) if ( lookup && lookup.customerContextId === '' ) { setIsEmailLookupCompleted( true ); } @@ -45,6 +62,7 @@ export const createEmailLookupHandler = ( return; } + // Trigger authentication flow const authResponse = await fastlaneSdk.identity.triggerAuthenticationFlow( lookup.customerContextId @@ -56,15 +74,18 @@ export const createEmailLookupHandler = ( const { authenticationState, profileData } = authResponse; - // OTP success/fail/cancel flow + // Mark email lookup as completed for OTP flow if ( authResponse ) { setIsEmailLookupCompleted( true ); } + // Handle successful authentication if ( authenticationState === 'succeeded' ) { + // Save current field values snapshotFields( wooShippingAddress, wooBillingAddress ); setIsGuest( false ); + // Update store with profile data if ( profileData && profileData.shippingAddress ) { setShippingAddress( profileData.shippingAddress ); } @@ -74,12 +95,14 @@ export const createEmailLookupHandler = ( log( `Profile Data: ${ JSON.stringify( profileData ) }` ); + // Populate WooCommerce fields with profile data populateWooFields( profileData, setWooShippingAddress, setWooBillingAddress ); + // Inject change buttons for shipping and card injectShippingChangeButton( onChangeShippingAddressClick ); injectCardChangeButton( onChangeCardButtonClick ); } else { diff --git a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js index 2af96aea9..411f815cb 100644 --- a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js +++ b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js @@ -41,6 +41,10 @@ export const setupAuthenticationClassToggle = () => { return unsubscribe; }; +/** + * Sets up a class toggle based on the isEmailLookupCompleted state for the checkout fields block. + * @return {Function} Unsubscribe function for cleanup. + */ export const setupEmailLookupCompletedClassToggle = () => { const targetSelector = '.wp-block-woocommerce-checkout-fields-block'; const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed'; @@ -77,7 +81,7 @@ export const setupEmailLookupCompletedClassToggle = () => { }; /** - * Sets up class toggles for the contact information block based on isAxoActive and isGuest states. + * Sets up class toggles for the contact information block based on isAxoActive, isGuest, and isEmailLookupCompleted states. * @return {Function} Unsubscribe function for cleanup. */ export const setupCheckoutBlockClassToggles = () => { @@ -133,7 +137,7 @@ export const setupCheckoutBlockClassToggles = () => { /** * Initializes all class toggles. - * @return {Function} Cleanup function. + * @return {Function} Cleanup function to unsubscribe all listeners. */ export const initializeClassToggles = () => { const unsubscribeAuth = setupAuthenticationClassToggle(); @@ -141,6 +145,7 @@ export const initializeClassToggles = () => { setupEmailLookupCompletedClassToggle(); const unsubscribeContactInfo = setupCheckoutBlockClassToggles(); + // Return a cleanup function that unsubscribes all listeners return () => { if ( unsubscribeAuth ) { unsubscribeAuth(); diff --git a/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js b/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js index 9c66d762d..c04b62a24 100644 --- a/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js +++ b/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js @@ -1,6 +1,12 @@ import { dispatch } from '@wordpress/data'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; +/** + * Saves the current shipping and billing address to localStorage. + * + * @param {Object} shippingAddress - The current shipping address. + * @param {Object} billingAddress - The current billing address. + */ export const snapshotFields = ( shippingAddress, billingAddress ) => { if ( ! shippingAddress || ! billingAddress ) { log( @@ -15,6 +21,7 @@ export const snapshotFields = ( shippingAddress, billingAddress ) => { const originalData = { shippingAddress, billingAddress }; log( `Snapshot data: ${ JSON.stringify( originalData ) }` ); try { + // Save the original data to localStorage localStorage.setItem( 'axoOriginalCheckoutFields', JSON.stringify( originalData ) @@ -24,6 +31,12 @@ export const snapshotFields = ( shippingAddress, billingAddress ) => { } }; +/** + * Restores the original shipping and billing addresses from localStorage. + * + * @param {Function} updateShippingAddress - Function to update the shipping address. + * @param {Function} updateBillingAddress - Function to update the billing address. + */ export const restoreOriginalFields = ( updateShippingAddress, updateBillingAddress @@ -31,6 +44,7 @@ export const restoreOriginalFields = ( log( 'Attempting to restore original fields' ); let savedData; try { + // Retrieve saved data from localStorage savedData = localStorage.getItem( 'axoOriginalCheckoutFields' ); log( `Data retrieved from localStorage: ${ JSON.stringify( savedData ) }` @@ -42,11 +56,13 @@ export const restoreOriginalFields = ( if ( savedData ) { try { const parsedData = JSON.parse( savedData ); + // Restore shipping address if available if ( parsedData.shippingAddress ) { updateShippingAddress( parsedData.shippingAddress ); } else { log( `No shipping address found in saved data`, 'warn' ); } + // Restore billing address if available if ( parsedData.billingAddress ) { log( `Restoring billing address: @@ -67,6 +83,13 @@ export const restoreOriginalFields = ( } }; +/** + * Populates WooCommerce fields with profile data from AXO. + * + * @param {Object} profileData - The profile data from AXO. + * @param {Function} setWooShippingAddress - Function to set WooCommerce shipping address. + * @param {Function} setWooBillingAddress - Function to set WooCommerce billing address. + */ export const populateWooFields = ( profileData, setWooShippingAddress, @@ -82,14 +105,14 @@ export const populateWooFields = ( const checkoutDispatch = dispatch( CHECKOUT_STORE_KEY ); - // Uncheck the 'Use same address for billing' checkbox if the method exists. + // Uncheck the 'Use same address for billing' checkbox if the method exists if ( typeof checkoutDispatch.__internalSetUseShippingAsBilling === 'function' ) { checkoutDispatch.__internalSetUseShippingAsBilling( false ); } - // Save shipping address. + // Prepare and set shipping address const { address, name, phoneNumber } = profileData.shippingAddress; const shippingAddress = { @@ -111,7 +134,7 @@ export const populateWooFields = ( ); setWooShippingAddress( shippingAddress ); - // Save billing address. + // Prepare and set billing address const billingData = profileData.card.paymentSource.card.billingAddress; const billingAddress = { @@ -132,12 +155,12 @@ export const populateWooFields = ( ); setWooBillingAddress( billingAddress ); - // Collapse shipping address input fields into the card view. + // Collapse shipping address input fields into the card view if ( typeof checkoutDispatch.setEditingShippingAddress === 'function' ) { checkoutDispatch.setEditingShippingAddress( false ); } - // Collapse billing address input fields into the card view. + // Collapse billing address input fields into the card view if ( typeof checkoutDispatch.setEditingBillingAddress === 'function' ) { checkoutDispatch.setEditingBillingAddress( false ); } diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js b/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js index 59fc32769..f1e57c943 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js @@ -3,11 +3,21 @@ import { useDispatch, useSelect } from '@wordpress/data'; const CHECKOUT_STORE_KEY = 'wc/store/checkout'; +/** + * Custom hook to manage address editing states in the checkout process. + * + * When set to true (default), the shipping and billing address forms are displayed. + * When set to false, the address forms are hidden and the user can only view the address details (card view). + * + * @return {Object} An object containing address editing states and setter functions. + */ export const useAddressEditing = () => { + // Select address editing states from the checkout store const { isEditingShippingAddress, isEditingBillingAddress } = useSelect( ( select ) => { const store = select( CHECKOUT_STORE_KEY ); return { + // Default to true if the getter function doesn't exist isEditingShippingAddress: store.getEditingShippingAddress ? store.getEditingShippingAddress() : true, @@ -19,9 +29,11 @@ export const useAddressEditing = () => { [] ); + // Get dispatch functions to update address editing states const { setEditingShippingAddress, setEditingBillingAddress } = useDispatch( CHECKOUT_STORE_KEY ); + // Memoized function to update shipping address editing state const setShippingAddressEditing = useCallback( ( isEditing ) => { if ( typeof setEditingShippingAddress === 'function' ) { @@ -31,6 +43,7 @@ export const useAddressEditing = () => { [ setEditingShippingAddress ] ); + // Memoized function to update billing address editing state const setBillingAddressEditing = useCallback( ( isEditing ) => { if ( typeof setEditingBillingAddress === 'function' ) { @@ -40,6 +53,7 @@ export const useAddressEditing = () => { [ setEditingBillingAddress ] ); + // Return an object with address editing states and setter functions return { isEditingShippingAddress, isEditingBillingAddress, diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js b/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js index 23175e31f..871197722 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js @@ -12,13 +12,21 @@ import { import { restoreOriginalFields } from '../helpers/fieldHelpers'; import useCustomerData from './useCustomerData'; +/** + * Custom hook to handle cleanup of AXO functionality. + * This hook ensures that all AXO-related changes are reverted when the component unmounts (a different payment method gets selected). + */ const useAxoCleanup = () => { + // Get dispatch functions from the AXO store const { setIsAxoActive, setIsGuest } = useDispatch( STORE_NAME ); + + // Get functions to update WooCommerce shipping and billing addresses const { setShippingAddress: updateWooShippingAddress, setBillingAddress: updateWooBillingAddress, } = useCustomerData(); + // Effect to restore original WooCommerce fields on unmount useEffect( () => { return () => { log( 'Cleaning up: Restoring WooCommerce fields' ); @@ -29,14 +37,21 @@ const useAxoCleanup = () => { }; }, [ updateWooShippingAddress, updateWooBillingAddress ] ); + // Effect to clean up AXO-specific functionality on unmount useEffect( () => { return () => { log( 'Cleaning up Axo component' ); + + // Reset AXO state setIsAxoActive( false ); setIsGuest( true ); + + // Remove AXO UI elements removeShippingChangeButton(); removeCardChangeButton(); removeWatermark(); + + // Remove email functionality if it was set up if ( isEmailFunctionalitySetup() ) { log( 'Removing email functionality' ); removeEmailFunctionality(); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js index bdc615461..37ac4898b 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js @@ -12,20 +12,34 @@ import useCustomerData from './useCustomerData'; import useShippingAddressChange from './useShippingAddressChange'; import useCardChange from './useCardChange'; +/** + * Custom hook to set up AXO functionality. + * + * @param {Object} ppcpConfig - PayPal Checkout configuration. + * @param {Object} fastlaneSdk - Fastlane SDK instance. + * @param {Object} paymentComponent - Payment component instance. + * @return {boolean} Whether PayPal script has loaded. + */ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { + // Get dispatch functions from the AXO store const { setIsAxoActive, setIsAxoScriptLoaded, setShippingAddress, setCardDetails, } = useDispatch( STORE_NAME ); + + // Check if PayPal script has loaded const paypalLoaded = usePayPalScript( ppcpConfig ); + + // Set up card and shipping address change handlers const onChangeCardButtonClick = useCardChange( fastlaneSdk ); const onChangeShippingAddressClick = useShippingAddressChange( fastlaneSdk, setShippingAddress ); + // Get customer data and setter functions const { shippingAddress: wooShippingAddress, billingAddress: wooBillingAddress, @@ -33,17 +47,22 @@ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { setBillingAddress: setWooBillingAddress, } = useCustomerData(); + // Set up phone sync handler usePhoneSyncHandler( paymentComponent ); + // Initialize class toggles on mount useEffect( () => { initializeClassToggles(); }, [] ); + // Set up AXO functionality when PayPal and Fastlane are loaded useEffect( () => { setupWatermark( fastlaneSdk ); if ( paypalLoaded && fastlaneSdk ) { setIsAxoScriptLoaded( true ); setIsAxoActive( true ); + + // Create and set up email lookup handler const emailLookupHandler = createEmailLookupHandler( fastlaneSdk, setShippingAddress, diff --git a/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js b/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js index 6d2482416..29ab2f42c 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js @@ -5,22 +5,29 @@ import { useAddressEditing } from './useAddressEditing'; import useCustomerData from './useCustomerData'; import { STORE_NAME } from '../stores/axoStore'; +/** + * Custom hook to handle the 'Choose a different card' selection. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @return {Function} Callback function to trigger card selection and update related data. + */ export const useCardChange = ( fastlaneSdk ) => { const { setBillingAddressEditing } = useAddressEditing(); const { setBillingAddress: setWooBillingAddress } = useCustomerData(); - const { setCardDetails, setShippingAddress } = useDispatch( STORE_NAME ); + const { setCardDetails } = useDispatch( STORE_NAME ); return useCallback( async () => { if ( fastlaneSdk ) { + // Show card selector and get the user's selection const { selectionChanged, selectedCard } = await fastlaneSdk.profile.showCardSelector(); if ( selectionChanged && selectedCard?.paymentSource?.card ) { - // Use the fallback logic for cardholder's name. + // Extract cardholder and billing information from the selected card const { name, billingAddress } = selectedCard.paymentSource.card; - // If name is missing, use billing details as a fallback for the name. + // Parse cardholder's name, using billing details as a fallback if missing let firstName = ''; let lastName = ''; @@ -30,6 +37,7 @@ export const useCardChange = ( fastlaneSdk ) => { lastName = nameParts.slice( 1 ).join( ' ' ); } + // Transform the billing address into WooCommerce format const newBillingAddress = { first_name: firstName, last_name: lastName, @@ -41,20 +49,19 @@ export const useCardChange = ( fastlaneSdk ) => { country: billingAddress?.countryCode || '', }; - // Batch state updates. + // Batch update states await Promise.all( [ + // Update the selected card details in the custom store new Promise( ( resolve ) => { setCardDetails( selectedCard ); resolve(); } ), + // Update the WooCommerce billing address in the WooCommerce store new Promise( ( resolve ) => { setWooBillingAddress( newBillingAddress ); resolve(); } ), - new Promise( ( resolve ) => { - setShippingAddress( newBillingAddress ); - resolve(); - } ), + // Trigger the Address Card view by setting the billing address editing state to false new Promise( ( resolve ) => { setBillingAddressEditing( false ); resolve(); @@ -68,7 +75,6 @@ export const useCardChange = ( fastlaneSdk ) => { fastlaneSdk, setCardDetails, setWooBillingAddress, - setShippingAddress, setBillingAddressEditing, ] ); }; diff --git a/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js b/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js index 6f7ffe537..b839e3199 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js @@ -1,16 +1,24 @@ import { useCallback, useMemo } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; +/** + * Custom hook to manage customer data in the WooCommerce store. + * + * @return {Object} An object containing customer addresses and setter functions. + */ export const useCustomerData = () => { + // Fetch customer data from the WooCommerce store const customerData = useSelect( ( select ) => select( 'wc/store/cart' ).getCustomerData() ); + // Get dispatch functions to update shipping and billing addresses const { setShippingAddress: setShippingAddressDispatch, setBillingAddress: setBillingAddressDispatch, } = useDispatch( 'wc/store/cart' ); + // Memoized function to update shipping address const setShippingAddress = useCallback( ( address ) => { setShippingAddressDispatch( address ); @@ -18,6 +26,7 @@ export const useCustomerData = () => { [ setShippingAddressDispatch ] ); + // Memoized function to update billing address const setBillingAddress = useCallback( ( address ) => { setBillingAddressDispatch( address ); @@ -25,6 +34,7 @@ export const useCustomerData = () => { [ setBillingAddressDispatch ] ); + // Return memoized object with customer data and setter functions return useMemo( () => ( { shippingAddress: customerData.shippingAddress, diff --git a/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js b/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js index 04043e0a9..63eaffe14 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js @@ -3,17 +3,30 @@ import { useCallback } from '@wordpress/element'; const isObject = ( value ) => typeof value === 'object' && value !== null; const isNonEmptyString = ( value ) => value !== ''; +/** + * Recursively removes empty values from an object. + * Empty values are considered to be: + * - Empty strings + * - Empty objects + * - Null or undefined values + * + * @param {Object} obj - The object to clean. + * @return {Object} A new object with empty values removed. + */ const removeEmptyValues = ( obj ) => { + // If not an object, return the value as is if ( ! isObject( obj ) ) { return obj; } return Object.fromEntries( Object.entries( obj ) + // Recursively apply removeEmptyValues to nested objects .map( ( [ key, value ] ) => [ key, isObject( value ) ? removeEmptyValues( value ) : value, ] ) + // Filter out empty values .filter( ( [ _, value ] ) => isObject( value ) ? Object.keys( value ).length > 0 @@ -22,6 +35,11 @@ const removeEmptyValues = ( obj ) => { ); }; +/** + * Custom hook that returns a memoized function to remove empty values from an object. + * + * @return {Function} A memoized function that removes empty values from an object. + */ export const useDeleteEmptyKeys = () => { return useCallback( removeEmptyValues, [] ); }; diff --git a/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js b/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js index 9bd9db8d5..bf1d4c791 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js @@ -3,16 +3,27 @@ import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { useDeleteEmptyKeys } from './useDeleteEmptyKeys'; +/** + * Custom hook to initialize and manage the Fastlane SDK. + * + * @param {Object} axoConfig - Configuration for AXO. + * @param {Object} ppcpConfig - Configuration for PPCP. + * @return {Object|null} The initialized Fastlane SDK instance or null. + */ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { const [ fastlaneSdk, setFastlaneSdk ] = useState( null ); + // Ref to prevent multiple simultaneous initializations const initializingRef = useRef( false ); + // Ref to hold the latest config values const configRef = useRef( { axoConfig, ppcpConfig } ); + // Custom hook to remove empty keys from an object const deleteEmptyKeys = useDeleteEmptyKeys(); const styleOptions = useMemo( () => { return deleteEmptyKeys( configRef.current.axoConfig.style_options ); }, [ deleteEmptyKeys ] ); + // Effect to initialize Fastlane SDK useEffect( () => { const initFastlane = async () => { if ( initializingRef.current || fastlaneSdk ) { @@ -25,15 +36,18 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { try { const fastlane = new Fastlane(); + // Set sandbox environment if configured if ( configRef.current.axoConfig.environment.is_sandbox ) { window.localStorage.setItem( 'axoEnv', 'sandbox' ); } + // Connect to Fastlane with locale and style options await fastlane.connect( { locale: configRef.current.ppcpConfig.locale, styles: styleOptions, } ); + // Set locale (hardcoded to 'en_us' for now) fastlane.setLocale( 'en_us' ); setFastlaneSdk( fastlane ); @@ -47,6 +61,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { initFastlane(); }, [ fastlaneSdk, styleOptions ] ); + // Effect to update the config ref when configs change useEffect( () => { configRef.current = { axoConfig, ppcpConfig }; }, [ axoConfig, ppcpConfig ] ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js b/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js index ca74c887b..86975f78c 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js @@ -2,29 +2,40 @@ import { useCallback } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { STORE_NAME } from '../stores/axoStore'; +/** + * Custom hook to handle payment setup in the checkout process. + * + * @param {Object} emitResponse - Object containing response types. + * @param {Object} paymentComponent - The payment component instance. + * @param {Object} tokenizedCustomerData - Tokenized customer data for payment. + * @return {Function} Callback function to handle payment setup. + */ const useHandlePaymentSetup = ( emitResponse, paymentComponent, tokenizedCustomerData ) => { + // Select card details from the store const { cardDetails } = useSelect( ( select ) => ( { - shippingAddress: select( STORE_NAME ).getShippingAddress(), cardDetails: select( STORE_NAME ).getCardDetails(), } ), [] ); return useCallback( async () => { + // Determine if it's a Ryan flow (saved card) based on the presence of card ID const isRyanFlow = !! cardDetails?.id; let cardToken = cardDetails?.id; + // If no card token and payment component exists, get a new token if ( ! cardToken && paymentComponent ) { cardToken = await paymentComponent .getPaymentToken( tokenizedCustomerData ) .then( ( response ) => response.id ); } + // Handle error cases when card token is not available if ( ! cardToken ) { let reason = 'tokenization error'; diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index 8fc85829e..223525285 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -2,12 +2,20 @@ import { useState, useEffect } from '@wordpress/element'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { loadPaypalScript } from '../../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; +/** + * Custom hook to load the PayPal script. + * + * @param {Object} ppcpConfig - Configuration object for PayPal script. + * @return {boolean} True if the PayPal script has loaded, false otherwise. + */ const usePayPalScript = ( ppcpConfig ) => { const [ isLoaded, setIsLoaded ] = useState( false ); useEffect( () => { if ( ! isLoaded ) { log( 'Loading PayPal script' ); + + // Load the PayPal script using the provided configuration loadPaypalScript( ppcpConfig, () => { log( 'PayPal script loaded' ); setIsLoaded( true ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js b/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js index 087fc807e..8e82b5011 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js @@ -1,5 +1,13 @@ import { useEffect, useCallback } from '@wordpress/element'; +/** + * Custom hook to handle payment setup effects in the checkout flow. + * + * @param {Function} onPaymentSetup - Function to subscribe to payment setup events. + * @param {Function} handlePaymentSetup - Callback to process payment setup. + * @param {Function} setPaymentComponent - Function to update the payment component state. + * @return {Object} Object containing the handlePaymentLoad function. + */ const usePaymentSetupEffect = ( onPaymentSetup, handlePaymentSetup, @@ -17,6 +25,11 @@ const usePaymentSetupEffect = ( }; }, [ onPaymentSetup, handlePaymentSetup ] ); + /** + * Callback function to handle payment component loading. + * + * @param {Object} component - The loaded payment component. + */ const handlePaymentLoad = useCallback( ( component ) => { setPaymentComponent( component ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js b/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js index 6d8e7b4a2..2c45b8930 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js @@ -2,19 +2,30 @@ import { useCallback } from '@wordpress/element'; import { useAddressEditing } from './useAddressEditing'; import useCustomerData from './useCustomerData'; +/** + * Custom hook to handle the 'Choose a different shipping address' selection. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @param {Function} setShippingAddress - Function to update the shipping address state. + * @return {Function} Callback function to trigger shipping address selection and update. + */ export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => { const { setShippingAddressEditing } = useAddressEditing(); const { setShippingAddress: setWooShippingAddress } = useCustomerData(); return useCallback( async () => { if ( fastlaneSdk ) { + // Show shipping address selector and get the user's selection const { selectionChanged, selectedAddress } = await fastlaneSdk.profile.showShippingAddressSelector(); + if ( selectionChanged ) { + // Update the shipping address in the custom store with the selected address setShippingAddress( selectedAddress ); const { address, name, phoneNumber } = selectedAddress; + // Transform the selected address into WooCommerce format const newShippingAddress = { first_name: name.firstName, last_name: name.lastName, @@ -27,11 +38,13 @@ export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => { phone: phoneNumber.nationalNumber, }; + // Update the WooCommerce shipping address in the WooCommerce store await new Promise( ( resolve ) => { setWooShippingAddress( newShippingAddress ); resolve(); } ); + // Trigger the Address Card view by setting the shipping address editing state to false await new Promise( ( resolve ) => { setShippingAddressEditing( false ); resolve(); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js b/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js index 868ddcb85..c0a7dcf97 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js @@ -1,18 +1,27 @@ import { useMemo } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; +import useCustomerData from './useCustomerData'; +/** + * Custom hook to prepare customer data for tokenization. + * + * @return {Object} Formatted customer data for tokenization. + */ export const useTokenizeCustomerData = () => { - const customerData = useSelect( ( select ) => - select( 'wc/store/cart' ).getCustomerData() - ); + const { billingAddress, shippingAddress } = useCustomerData(); + /** + * Validates if an address contains the minimum required data. + * + * @param {Object} address - The address object to validate. + * @return {boolean} True if the address is valid, false otherwise. + */ const isValidAddress = ( address ) => { - // At least one name must be present. + // At least one name must be present if ( ! address.first_name && ! address.last_name ) { return false; } - // Street, city, postcode, country are mandatory; state is optional. + // Street, city, postcode, country are mandatory; state is optional return ( address.address_1 && address.city && @@ -21,15 +30,14 @@ export const useTokenizeCustomerData = () => { ); }; - // Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops). + // Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops) return useMemo( () => { - const { billingAddress, shippingAddress } = customerData; - - // Prefer billing address, but fallback to shipping address if billing address is not valid. + // Determine the main address, preferring billing address if valid const mainAddress = isValidAddress( billingAddress ) ? billingAddress : shippingAddress; + // Format the customer data for tokenization return { cardholderName: { fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`, @@ -43,7 +51,7 @@ export const useTokenizeCustomerData = () => { countryCode: mainAddress.country, }, }; - }, [ customerData ] ); + }, [ billingAddress, shippingAddress ] ); }; export default useTokenizeCustomerData; diff --git a/modules/ppcp-axo-block/resources/js/stores/axoStore.js b/modules/ppcp-axo-block/resources/js/stores/axoStore.js index 04323c6e1..da92d0e6f 100644 --- a/modules/ppcp-axo-block/resources/js/stores/axoStore.js +++ b/modules/ppcp-axo-block/resources/js/stores/axoStore.js @@ -2,7 +2,6 @@ import { createReduxStore, register, dispatch } from '@wordpress/data'; export const STORE_NAME = 'woocommerce-paypal-payments/axo-block'; -// Initial state const DEFAULT_STATE = { isGuest: true, isAxoActive: false, @@ -14,7 +13,7 @@ const DEFAULT_STATE = { phoneNumber: '', }; -// Actions +// Action creators for updating the store state const actions = { setIsGuest: ( isGuest ) => ( { type: 'SET_IS_GUEST', @@ -50,7 +49,13 @@ const actions = { } ), }; -// Reducer +/** + * Reducer function to handle state updates based on dispatched actions. + * + * @param {Object} state - Current state of the store. + * @param {Object} action - Dispatched action object. + * @return {Object} New state after applying the action. + */ const reducer = ( state = DEFAULT_STATE, action ) => { switch ( action.type ) { case 'SET_IS_GUEST': @@ -74,7 +79,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => { } }; -// Selectors +// Selector functions to retrieve specific pieces of state const selectors = { getIsGuest: ( state ) => state.isGuest, getIsAxoActive: ( state ) => state.isAxoActive, @@ -86,7 +91,7 @@ const selectors = { getPhoneNumber: ( state ) => state.phoneNumber, }; -// Create and register the store +// Create and register the Redux store for the AXO block const store = createReduxStore( STORE_NAME, { reducer, actions, @@ -96,22 +101,48 @@ const store = createReduxStore( STORE_NAME, { register( store ); // Action dispatchers + +/** + * Action dispatcher to update the guest status in the store. + * + * @param {boolean} isGuest - Whether the user is a guest or not. + */ export const setIsGuest = ( isGuest ) => { dispatch( STORE_NAME ).setIsGuest( isGuest ); }; +/** + * Action dispatcher to update the email lookup completion status in the store. + * + * @param {boolean} isEmailLookupCompleted - Whether the email lookup is completed. + */ export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => { dispatch( STORE_NAME ).setIsEmailLookupCompleted( isEmailLookupCompleted ); }; +/** + * Action dispatcher to update the shipping address in the store. + * + * @param {Object} shippingAddress - The user's shipping address. + */ export const setShippingAddress = ( shippingAddress ) => { dispatch( STORE_NAME ).setShippingAddress( shippingAddress ); }; +/** + * Action dispatcher to update the card details in the store. + * + * @param {Object} cardDetails - The user's card details. + */ export const setCardDetails = ( cardDetails ) => { dispatch( STORE_NAME ).setCardDetails( cardDetails ); }; +/** + * Action dispatcher to update the phone number in the store. + * + * @param {string} phoneNumber - The user's phone number. + */ export const setPhoneNumber = ( phoneNumber ) => { dispatch( STORE_NAME ).setPhoneNumber( phoneNumber ); }; From 05ea2f1e1096936826ead9ede32eb5fe5a62ccf6 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Thu, 3 Oct 2024 14:33:54 +0200 Subject: [PATCH 22/30] Axo Block: Improve the comment regarding class names --- .../ppcp-axo-block/resources/js/helpers/classnamesManager.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js index 411f815cb..7b25cec31 100644 --- a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js +++ b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js @@ -4,6 +4,8 @@ import { STORE_NAME } from '../stores/axoStore'; /** * Sets up a class toggle based on the isGuest state for the express payment block. + * This hides the express payment methods if the user is authenticated (Ryan flow). + * * @return {Function} Unsubscribe function for cleanup. */ export const setupAuthenticationClassToggle = () => { @@ -43,6 +45,9 @@ export const setupAuthenticationClassToggle = () => { /** * Sets up a class toggle based on the isEmailLookupCompleted state for the checkout fields block. + * This hides the Shipping Address fields, Billing Address fields, Shipping Options section, + * Order Notes section, Checkout Terms section, and Place Order button until email lookup is completed. + * * @return {Function} Unsubscribe function for cleanup. */ export const setupEmailLookupCompletedClassToggle = () => { From 5e1c2a22d26a1320e239aaf77b387d570230084c Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Sat, 5 Oct 2024 02:26:09 +0200 Subject: [PATCH 23/30] Introduce a unified script loader to improve script loading, debugging and prevent conflicts --- .../resources/js/hooks/useAxoSetup.js | 22 ++++-- .../resources/js/hooks/useFastlaneSdk.js | 21 ++++-- .../resources/js/hooks/usePayPalScript.js | 56 ++++++++++----- modules/ppcp-axo-block/resources/js/index.js | 68 ++++++++++++++----- .../resources/js/stores/axoStore.js | 17 +++++ modules/ppcp-axo/resources/js/AxoManager.js | 5 +- .../resources/js/Connection/Fastlane.js | 16 ++++- modules/ppcp-axo/resources/js/boot.js | 18 +++-- modules/ppcp-axo/src/AxoModule.php | 3 +- .../resources/js/checkout-block.js | 25 +++++-- 10 files changed, 186 insertions(+), 65 deletions(-) diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js index 37ac4898b..af1aaca38 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js @@ -15,12 +15,20 @@ import useCardChange from './useCardChange'; /** * Custom hook to set up AXO functionality. * - * @param {Object} ppcpConfig - PayPal Checkout configuration. - * @param {Object} fastlaneSdk - Fastlane SDK instance. - * @param {Object} paymentComponent - Payment component instance. + * @param {string} namespace - Namespace for the PayPal script. + * @param {Object} ppcpConfig - PayPal Checkout configuration. + * @param {boolean} isConfigLoaded - Whether the PayPal config has loaded. + * @param {Object} fastlaneSdk - Fastlane SDK instance. + * @param {Object} paymentComponent - Payment component instance. * @return {boolean} Whether PayPal script has loaded. */ -const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { +const useAxoSetup = ( + namespace, + ppcpConfig, + isConfigLoaded, + fastlaneSdk, + paymentComponent +) => { // Get dispatch functions from the AXO store const { setIsAxoActive, @@ -30,7 +38,11 @@ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { } = useDispatch( STORE_NAME ); // Check if PayPal script has loaded - const paypalLoaded = usePayPalScript( ppcpConfig ); + const paypalLoaded = usePayPalScript( + namespace, + ppcpConfig, + isConfigLoaded + ); // Set up card and shipping address change handlers const onChangeCardButtonClick = useCardChange( fastlaneSdk ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js b/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js index bf1d4c791..7e68ccab1 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js @@ -1,24 +1,31 @@ import { useEffect, useRef, useState, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { useDeleteEmptyKeys } from './useDeleteEmptyKeys'; +import { STORE_NAME } from '../stores/axoStore'; /** * Custom hook to initialize and manage the Fastlane SDK. * + * @param {string} namespace - Namespace for the PayPal script. * @param {Object} axoConfig - Configuration for AXO. * @param {Object} ppcpConfig - Configuration for PPCP. * @return {Object|null} The initialized Fastlane SDK instance or null. */ -const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { +const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => { const [ fastlaneSdk, setFastlaneSdk ] = useState( null ); - // Ref to prevent multiple simultaneous initializations const initializingRef = useRef( false ); - // Ref to hold the latest config values const configRef = useRef( { axoConfig, ppcpConfig } ); - // Custom hook to remove empty keys from an object const deleteEmptyKeys = useDeleteEmptyKeys(); + const { isPayPalLoaded } = useSelect( + ( select ) => ( { + isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(), + } ), + [] + ); + const styleOptions = useMemo( () => { return deleteEmptyKeys( configRef.current.axoConfig.style_options ); }, [ deleteEmptyKeys ] ); @@ -26,7 +33,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { // Effect to initialize Fastlane SDK useEffect( () => { const initFastlane = async () => { - if ( initializingRef.current || fastlaneSdk ) { + if ( initializingRef.current || fastlaneSdk || ! isPayPalLoaded ) { return; } @@ -34,7 +41,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { log( 'Init Fastlane' ); try { - const fastlane = new Fastlane(); + const fastlane = new Fastlane( namespace ); // Set sandbox environment if configured if ( configRef.current.axoConfig.environment.is_sandbox ) { @@ -59,7 +66,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { }; initFastlane(); - }, [ fastlaneSdk, styleOptions ] ); + }, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace ] ); // Effect to update the config ref when configs change useEffect( () => { diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index 223525285..d4c380d95 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -1,29 +1,53 @@ -import { useState, useEffect } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; -import { loadPaypalScript } from '../../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; +import UnifiedScriptLoader from '../../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { STORE_NAME } from '../stores/axoStore'; /** - * Custom hook to load the PayPal script. + * Custom hook to load the PayPal script using UnifiedScriptLoader. * - * @param {Object} ppcpConfig - Configuration object for PayPal script. + * @param {string} namespace - Namespace for the PayPal script. + * @param {Object} ppcpConfig - Configuration object for PayPal script. + * @param {boolean} isConfigLoaded - Whether the PayPal Commerce Gateway config is loaded. * @return {boolean} True if the PayPal script has loaded, false otherwise. */ -const usePayPalScript = ( ppcpConfig ) => { - const [ isLoaded, setIsLoaded ] = useState( false ); +const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => { + // Get dispatch functions from the AXO store + const { setIsPayPalLoaded } = useDispatch( STORE_NAME ); + + // Select relevant states from the AXO store + const { isPayPalLoaded } = useSelect( + ( select ) => ( { + isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(), + } ), + [] + ); useEffect( () => { - if ( ! isLoaded ) { - log( 'Loading PayPal script' ); + const loadScript = async () => { + if ( ! isPayPalLoaded && isConfigLoaded ) { + log( `Loading PayPal script for namespace: ${ namespace }` ); + try { + await UnifiedScriptLoader.loadPayPalScript( + namespace, + ppcpConfig + ); + log( `PayPal script loaded for namespace: ${ namespace }` ); + setIsPayPalLoaded( true ); + } catch ( error ) { + log( + `Error loading PayPal script for namespace: ${ namespace }`, + error + ); + } + } + }; - // Load the PayPal script using the provided configuration - loadPaypalScript( ppcpConfig, () => { - log( 'PayPal script loaded' ); - setIsLoaded( true ); - } ); - } - }, [ ppcpConfig, isLoaded ] ); + loadScript(); + }, [ ppcpConfig, isConfigLoaded, isPayPalLoaded ] ); - return isLoaded; + return isPayPalLoaded; }; export default usePayPalScript; diff --git a/modules/ppcp-axo-block/resources/js/index.js b/modules/ppcp-axo-block/resources/js/index.js index 9ee3b1112..a45473e50 100644 --- a/modules/ppcp-axo-block/resources/js/index.js +++ b/modules/ppcp-axo-block/resources/js/index.js @@ -9,25 +9,25 @@ import useAxoSetup from './hooks/useAxoSetup'; import useAxoCleanup from './hooks/useAxoCleanup'; import useHandlePaymentSetup from './hooks/useHandlePaymentSetup'; import usePaymentSetupEffect from './hooks/usePaymentSetupEffect'; +import usePayPalCommerceGateway from './hooks/usePayPalCommerceGateway'; // Components import { Payment } from './components/Payment/Payment'; const gatewayHandle = 'ppcp-axo-gateway'; -const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` ); - -if ( typeof window.PayPalCommerceGateway === 'undefined' ) { - window.PayPalCommerceGateway = ppcpConfig; -} - -const axoConfig = window.wc_ppcp_axo; - +const namespace = 'ppcpBlocksPaypalAxo'; +const initialConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` ); const Axo = ( props ) => { const { eventRegistration, emitResponse } = props; const { onPaymentSetup } = eventRegistration; const [ paymentComponent, setPaymentComponent ] = useState( null ); - const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig ); + const { isConfigLoaded, ppcpConfig } = + usePayPalCommerceGateway( initialConfig ); + + const axoConfig = window.wc_ppcp_axo; + + const fastlaneSdk = useFastlaneSdk( namespace, axoConfig, ppcpConfig ); const tokenizedCustomerData = useTokenizeCustomerData(); const handlePaymentSetup = useHandlePaymentSetup( emitResponse, @@ -35,7 +35,13 @@ const Axo = ( props ) => { tokenizedCustomerData ); - useAxoSetup( ppcpConfig, fastlaneSdk, paymentComponent ); + const isScriptLoaded = useAxoSetup( + namespace, + ppcpConfig, + isConfigLoaded, + fastlaneSdk, + paymentComponent + ); const { handlePaymentLoad } = usePaymentSetupEffect( onPaymentSetup, @@ -45,31 +51,57 @@ const Axo = ( props ) => { useAxoCleanup(); - return fastlaneSdk ? ( + if ( ! isConfigLoaded ) { + return ( + <> + { __( + 'Loading configuration…', + 'woocommerce-paypal-payments' + ) } + + ); + } + + if ( ! isScriptLoaded ) { + return ( + <> + { __( + 'Loading PayPal script…', + 'woocommerce-paypal-payments' + ) } + + ); + } + + if ( ! fastlaneSdk ) { + return ( + <>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) } + ); + } + + return ( - ) : ( - <>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) } ); }; registerPaymentMethod( { - name: ppcpConfig.id, + name: initialConfig.id, label: (
), content: , - edit: createElement( ppcpConfig.title ), - ariaLabel: ppcpConfig.title, + edit: createElement( initialConfig.title ), + ariaLabel: initialConfig.title, canMakePayment: () => true, supports: { showSavedCards: true, - features: ppcpConfig.supports, + features: initialConfig.supports, }, } ); diff --git a/modules/ppcp-axo-block/resources/js/stores/axoStore.js b/modules/ppcp-axo-block/resources/js/stores/axoStore.js index da92d0e6f..f779f983c 100644 --- a/modules/ppcp-axo-block/resources/js/stores/axoStore.js +++ b/modules/ppcp-axo-block/resources/js/stores/axoStore.js @@ -3,6 +3,7 @@ import { createReduxStore, register, dispatch } from '@wordpress/data'; export const STORE_NAME = 'woocommerce-paypal-payments/axo-block'; const DEFAULT_STATE = { + isPayPalLoaded: false, isGuest: true, isAxoActive: false, isAxoScriptLoaded: false, @@ -15,6 +16,10 @@ const DEFAULT_STATE = { // Action creators for updating the store state const actions = { + setIsPayPalLoaded: ( isPayPalLoaded ) => ( { + type: 'SET_IS_PAYPAL_LOADED', + payload: isPayPalLoaded, + } ), setIsGuest: ( isGuest ) => ( { type: 'SET_IS_GUEST', payload: isGuest, @@ -58,6 +63,8 @@ const actions = { */ const reducer = ( state = DEFAULT_STATE, action ) => { switch ( action.type ) { + case 'SET_IS_PAYPAL_LOADED': + return { ...state, isPayPalLoaded: action.payload }; case 'SET_IS_GUEST': return { ...state, isGuest: action.payload }; case 'SET_IS_AXO_ACTIVE': @@ -81,6 +88,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => { // Selector functions to retrieve specific pieces of state const selectors = { + getIsPayPalLoaded: ( state ) => state.isPayPalLoaded, getIsGuest: ( state ) => state.isGuest, getIsAxoActive: ( state ) => state.isAxoActive, getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded, @@ -102,6 +110,15 @@ register( store ); // Action dispatchers +/** + * Action dispatcher to update the PayPal script load status in the store. + * + * @param {boolean} isPayPalLoaded - Whether the PayPal script has loaded. + */ +export const setIsPayPalLoaded = ( isPayPalLoaded ) => { + dispatch( STORE_NAME ).setIsPayPalLoaded( isPayPalLoaded ); +}; + /** * Action dispatcher to update the guest status in the store. * diff --git a/modules/ppcp-axo/resources/js/AxoManager.js b/modules/ppcp-axo/resources/js/AxoManager.js index d315ce374..ddc61be32 100644 --- a/modules/ppcp-axo/resources/js/AxoManager.js +++ b/modules/ppcp-axo/resources/js/AxoManager.js @@ -52,11 +52,12 @@ class AxoManager { billingView = null; cardView = null; - constructor( axoConfig, ppcpConfig ) { + constructor( namespace, axoConfig, ppcpConfig ) { + this.namespace = namespace; this.axoConfig = axoConfig; this.ppcpConfig = ppcpConfig; - this.fastlane = new Fastlane(); + this.fastlane = new Fastlane( namespace ); this.$ = jQuery; this.status = { diff --git a/modules/ppcp-axo/resources/js/Connection/Fastlane.js b/modules/ppcp-axo/resources/js/Connection/Fastlane.js index d01ae8524..80490b1a4 100644 --- a/modules/ppcp-axo/resources/js/Connection/Fastlane.js +++ b/modules/ppcp-axo/resources/js/Connection/Fastlane.js @@ -1,5 +1,6 @@ class Fastlane { - construct() { + constructor( namespace ) { + this.namespace = namespace; this.connection = null; this.identity = null; this.profile = null; @@ -10,7 +11,16 @@ class Fastlane { connect( config ) { return new Promise( ( resolve, reject ) => { - window.paypal + if ( ! window[ this.namespace ] ) { + reject( + new Error( + `Namespace ${ this.namespace } not found on window object` + ) + ); + return; + } + + window[ this.namespace ] .Fastlane( config ) .then( ( result ) => { this.init( result ); @@ -18,7 +28,7 @@ class Fastlane { } ) .catch( ( error ) => { console.error( error ); - reject(); + reject( error ); } ); } ); } diff --git a/modules/ppcp-axo/resources/js/boot.js b/modules/ppcp-axo/resources/js/boot.js index 75bc9e636..7102e04c3 100644 --- a/modules/ppcp-axo/resources/js/boot.js +++ b/modules/ppcp-axo/resources/js/boot.js @@ -1,21 +1,27 @@ import AxoManager from './AxoManager'; -import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; +import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; ( function ( { axoConfig, ppcpConfig, jQuery } ) { + const namespace = 'ppcpPaypalClassicAxo'; const bootstrap = () => { - new AxoManager( axoConfig, ppcpConfig ); + new AxoManager( namespace, axoConfig, ppcpConfig ); }; document.addEventListener( 'DOMContentLoaded', () => { - if ( ! typeof PayPalCommerceGateway ) { + if ( typeof PayPalCommerceGateway === 'undefined' ) { console.error( 'AXO could not be configured.' ); return; } // Load PayPal - loadPaypalScript( ppcpConfig, () => { - bootstrap(); - } ); + UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) + .then( () => { + console.log( 'PayPal script loaded successfully' ); + bootstrap(); + } ) + .catch( ( error ) => { + console.error( 'Failed to load PayPal script:', error ); + } ); } ); } )( { axoConfig: window.wc_ppcp_axo, diff --git a/modules/ppcp-axo/src/AxoModule.php b/modules/ppcp-axo/src/AxoModule.php index b07f8ef53..a0470aeac 100644 --- a/modules/ppcp-axo/src/AxoModule.php +++ b/modules/ppcp-axo/src/AxoModule.php @@ -378,7 +378,8 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule { return ! is_user_logged_in() && CartCheckoutDetector::has_classic_checkout() && $dcc_configuration->use_fastlane() - && ! $this->is_excluded_endpoint(); + && ! $this->is_excluded_endpoint() + && is_checkout(); } /** diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 3d26908ea..b37dabda1 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -15,11 +15,12 @@ import { cartHasSubscriptionProducts, isPayPalSubscription, } from './Helper/Subscription'; -import { loadPaypalScriptPromise } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; +import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap'; +const namespace = 'ppcpBlocksPaypalExpressButtons'; const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); window.ppcpFundingSource = config.fundingSource; @@ -55,7 +56,10 @@ const PayPalComponent = ( { if ( ! paypalScriptLoaded ) { if ( ! paypalScriptPromise ) { // for editor, since canMakePayment was not called - paypalScriptPromise = loadPaypalScriptPromise( config.scriptData ); + paypalScriptPromise = UnifiedScriptLoader.loadPayPalScript( + namespace, + config.scriptData + ); } paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) ); } @@ -614,7 +618,10 @@ const PayPalComponent = ( { return null; } - const PayPalButton = paypal.Buttons.driver( 'react', { React, ReactDOM } ); + const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver( + 'react', + { React, ReactDOM } + ); const getOnShippingOptionsChange = ( fundingSource ) => { if ( fundingSource === 'venmo' ) { @@ -818,9 +825,11 @@ if ( block_enabled && config.enabled ) { ariaLabel: config.title, canMakePayment: async () => { if ( ! paypalScriptPromise ) { - paypalScriptPromise = loadPaypalScriptPromise( - config.scriptData - ); + paypalScriptPromise = + UnifiedScriptLoader.loadPayPalScript( + namespace, + config.scriptData + ); paypalScriptPromise.then( () => { const messagesBootstrap = new BlockCheckoutMessagesBootstrap( @@ -831,7 +840,9 @@ if ( block_enabled && config.enabled ) { } await paypalScriptPromise; - return paypal.Buttons( { fundingSource } ).isEligible(); + return ppcpBlocksPaypalExpressButtons + .Buttons( { fundingSource } ) + .isEligible(); }, supports: { features, From 07bedc44605a7d960d07a1519c7ca39e6d98487c Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 7 Oct 2024 10:42:56 +0200 Subject: [PATCH 24/30] Add the UnifiedScriptLoader and usePayPalCommerceGateway hook --- .../js/hooks/usePayPalCommerceGateway.js | 32 ++++++++ .../resources/js/hooks/usePayPalScript.js | 5 +- modules/ppcp-axo/resources/js/boot.js | 4 +- .../js/modules/Helper/UnifiedScriptLoader.js | 75 +++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js create mode 100644 modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js new file mode 100644 index 000000000..23c755d26 --- /dev/null +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js @@ -0,0 +1,32 @@ +import { useState, useEffect } from '@wordpress/element'; +import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; + +const usePayPalCommerceGateway = ( initialConfig ) => { + const [ isConfigLoaded, setIsConfigLoaded ] = useState( false ); + const [ ppcpConfig, setPpcpConfig ] = useState( initialConfig ); + + useEffect( () => { + const loadConfig = () => { + if ( typeof window.PayPalCommerceGateway !== 'undefined' ) { + setPpcpConfig( window.PayPalCommerceGateway ); + setIsConfigLoaded( true ); + } else { + log( 'PayPal Commerce Gateway config not loaded.', 'error' ); + } + }; + + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', loadConfig ); + } else { + loadConfig(); + } + + return () => { + document.removeEventListener( 'DOMContentLoaded', loadConfig ); + }; + }, [] ); + + return { isConfigLoaded, ppcpConfig }; +}; + +export default usePayPalCommerceGateway; diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index d4c380d95..26e237891 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -27,7 +27,6 @@ const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => { useEffect( () => { const loadScript = async () => { if ( ! isPayPalLoaded && isConfigLoaded ) { - log( `Loading PayPal script for namespace: ${ namespace }` ); try { await UnifiedScriptLoader.loadPayPalScript( namespace, @@ -37,8 +36,8 @@ const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => { setIsPayPalLoaded( true ); } catch ( error ) { log( - `Error loading PayPal script for namespace: ${ namespace }`, - error + `Error loading PayPal script for namespace: ${ namespace }. Error: ${ error }`, + 'error' ); } } diff --git a/modules/ppcp-axo/resources/js/boot.js b/modules/ppcp-axo/resources/js/boot.js index 7102e04c3..9a706360e 100644 --- a/modules/ppcp-axo/resources/js/boot.js +++ b/modules/ppcp-axo/resources/js/boot.js @@ -1,5 +1,6 @@ import AxoManager from './AxoManager'; import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { log } from './Helper/Debug'; ( function ( { axoConfig, ppcpConfig, jQuery } ) { const namespace = 'ppcpPaypalClassicAxo'; @@ -16,11 +17,10 @@ import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helpe // Load PayPal UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) .then( () => { - console.log( 'PayPal script loaded successfully' ); bootstrap(); } ) .catch( ( error ) => { - console.error( 'Failed to load PayPal script:', error ); + log( `Failed to load PayPal script: ${ error }`, 'error' ); } ); } ); } )( { diff --git a/modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js b/modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js new file mode 100644 index 000000000..a28d40d2c --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js @@ -0,0 +1,75 @@ +import { loadScript } from '@paypal/paypal-js'; + +class UnifiedScriptLoader { + constructor() { + this.loadedScripts = new Map(); + this.scriptPromises = new Map(); + } + + async loadPayPalScript( namespace, config ) { + if ( ! namespace ) { + throw new Error( 'Namespace is required' ); + } + + if ( this.loadedScripts.has( namespace ) ) { + console.log( + `PayPal script already loaded for namespace: ${ namespace }` + ); + return this.loadedScripts.get( namespace ); + } + + if ( this.scriptPromises.has( namespace ) ) { + console.log( + `PayPal script loading in progress for namespace: ${ namespace }` + ); + return this.scriptPromises.get( namespace ); + } + + const scriptPromise = new Promise( ( resolve, reject ) => { + const scriptOptions = { + ...config.url_params, + ...config.script_attributes, + 'data-namespace': namespace, + }; + + if ( config.axo?.sdk_client_token ) { + scriptOptions[ 'data-sdk-client-token' ] = + config.axo.sdk_client_token; + scriptOptions[ 'data-client-metadata-id' ] = + config.axo.client_metadata_id; + } + + if ( + config.save_payment_methods?.id_token && + ! config.axo?.sdk_client_token + ) { + scriptOptions[ 'data-user-id-token' ] = + config.save_payment_methods.id_token; + } + + loadScript( scriptOptions ) + .then( ( paypal ) => { + this.loadedScripts.set( namespace, paypal ); + console.log( + `PayPal script loaded for namespace: ${ namespace }` + ); + resolve( paypal ); + } ) + .catch( ( error ) => { + console.error( + `Failed to load PayPal script for namespace: ${ namespace }`, + error + ); + reject( error ); + } ) + .finally( () => { + this.scriptPromises.delete( namespace ); + } ); + } ); + + this.scriptPromises.set( namespace, scriptPromise ); + return scriptPromise; + } +} + +export default new UnifiedScriptLoader(); From 869a47f3cd508e57242f68a86255e3fb10f141b3 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 7 Oct 2024 15:30:27 +0200 Subject: [PATCH 25/30] Migrate Google Pay to the new script loader --- composer.lock | 2 +- .../resources/js/GooglepayManager.js | 7 ++- .../js/GooglepayManagerBlockEditor.js | 7 ++- .../ppcp-googlepay/resources/js/boot-block.js | 49 +++++++++++-------- modules/ppcp-googlepay/resources/js/boot.js | 30 ++++++++---- 5 files changed, 59 insertions(+), 36 deletions(-) diff --git a/composer.lock b/composer.lock index a18b56927..c88e1fb48 100644 --- a/composer.lock +++ b/composer.lock @@ -5554,5 +5554,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js index aaf85a6b0..f9520d23a 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js @@ -3,7 +3,8 @@ import GooglepayButton from './GooglepayButton'; import ContextHandlerFactory from './Context/ContextHandlerFactory'; class GooglepayManager { - constructor( buttonConfig, ppcpConfig ) { + constructor( namespace, buttonConfig, ppcpConfig ) { + this.namespace = namespace; this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; this.googlePayConfig = null; @@ -52,7 +53,9 @@ class GooglepayManager { try { if ( ! this.googlePayConfig ) { // Gets GooglePay configuration of the PayPal merchant. - this.googlePayConfig = await paypal.Googlepay().config(); + this.googlePayConfig = await window[ this.namespace ] + .Googlepay() + .config(); } if ( ! this.transactionInfo ) { diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js b/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js index 0fbbfbd72..2bf5a55c3 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js @@ -2,7 +2,8 @@ import GooglepayButton from './GooglepayButton'; import ContextHandlerFactory from './Context/ContextHandlerFactory'; class GooglepayManagerBlockEditor { - constructor( buttonConfig, ppcpConfig ) { + constructor( namespace, buttonConfig, ppcpConfig ) { + this.namespace = namespace; this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; this.googlePayConfig = null; @@ -19,7 +20,9 @@ class GooglepayManagerBlockEditor { async config() { try { // Gets GooglePay configuration of the PayPal merchant. - this.googlePayConfig = await ppcpBlocksEditorPaypalGooglepay.Googlepay().config(); + this.googlePayConfig = await window[ this.namespace ] + .Googlepay() + .config(); // Fetch transaction information. this.transactionInfo = await this.fetchTransactionInfo(); diff --git a/modules/ppcp-googlepay/resources/js/boot-block.js b/modules/ppcp-googlepay/resources/js/boot-block.js index 3d465ac93..acb5a310d 100644 --- a/modules/ppcp-googlepay/resources/js/boot-block.js +++ b/modules/ppcp-googlepay/resources/js/boot-block.js @@ -4,7 +4,7 @@ import { registerPaymentMethod, } from '@woocommerce/blocks-registry'; import { __ } from '@wordpress/i18n'; -import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; +import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; import GooglepayManager from './GooglepayManager'; import { loadCustomScript } from '@paypal/paypal-js'; import GooglepayManagerBlockEditor from './GooglepayManagerBlockEditor'; @@ -14,7 +14,7 @@ const ppcpConfig = ppcpData.scriptData; const buttonData = wc.wcSettings.getSetting( 'ppcp-googlepay_data' ); const buttonConfig = buttonData.scriptData; -const dataNamespace = 'ppcpBlocksEditorPaypalGooglepay'; +const namespace = 'ppcpBlocksPaypalGooglepay'; if ( typeof window.PayPalCommerceGateway === 'undefined' ) { window.PayPalCommerceGateway = ppcpConfig; @@ -24,14 +24,7 @@ const GooglePayComponent = ( props ) => { const [ bootstrapped, setBootstrapped ] = useState( false ); const [ paypalLoaded, setPaypalLoaded ] = useState( false ); const [ googlePayLoaded, setGooglePayLoaded ] = useState( false ); - - const bootstrap = function () { - const ManagerClass = props.isEditing - ? GooglepayManagerBlockEditor - : GooglepayManager; - const manager = new ManagerClass( buttonConfig, ppcpConfig ); - manager.init(); - }; + const [ manager, setManager ] = useState( null ); useEffect( () => { // Load GooglePay SDK @@ -41,22 +34,36 @@ const GooglePayComponent = ( props ) => { ppcpConfig.url_params.components += ',googlepay'; - if ( props.isEditing ) { - ppcpConfig.data_namespace = dataNamespace; - } - // Load PayPal - loadPaypalScript( ppcpConfig, () => { - setPaypalLoaded( true ); - } ); + UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) + .then( () => { + setPaypalLoaded( true ); + } ) + .catch( ( error ) => { + console.error( 'Failed to load PayPal script: ', error ); + } ); }, [] ); useEffect( () => { - if ( ! bootstrapped && paypalLoaded && googlePayLoaded ) { - setBootstrapped( true ); - bootstrap(); + if ( paypalLoaded && googlePayLoaded && ! manager ) { + const ManagerClass = props.isEditing + ? GooglepayManagerBlockEditor + : GooglepayManager; + const newManager = new ManagerClass( + namespace, + buttonConfig, + ppcpConfig + ); + setManager( newManager ); } - }, [ paypalLoaded, googlePayLoaded ] ); + }, [ paypalLoaded, googlePayLoaded, props.isEditing ] ); + + useEffect( () => { + if ( manager && ! bootstrapped ) { + setBootstrapped( true ); + manager.init(); + } + }, [ manager, bootstrapped ] ); return (
{ - paypalLoaded = true; - tryToBoot(); - } ); + UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) + .then( () => { + paypalLoaded = true; + tryToBoot(); + } ) + .catch( ( error ) => { + console.error( 'Failed to load PayPal script: ', error ); + } ); } ); } )( { buttonConfig: window.wc_ppcp_googlepay, From a874eca456624493f611d3dbe649c04653957336 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 7 Oct 2024 15:35:50 +0200 Subject: [PATCH 26/30] Remove log message --- modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index 26e237891..1999ecaba 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -32,7 +32,6 @@ const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => { namespace, ppcpConfig ); - log( `PayPal script loaded for namespace: ${ namespace }` ); setIsPayPalLoaded( true ); } catch ( error ) { log( From c3bc87b1a3019361479846a517c8473e67d9c610 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 7 Oct 2024 15:38:31 +0200 Subject: [PATCH 27/30] Revert plugin-api-version change --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index c88e1fb48..a18b56927 100644 --- a/composer.lock +++ b/composer.lock @@ -5554,5 +5554,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From eba3d12e4e9bed952553fd4f64137944b623b01d Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Mon, 7 Oct 2024 15:42:01 +0200 Subject: [PATCH 28/30] Update code comments --- .../resources/js/hooks/usePayPalCommerceGateway.js | 14 ++++++++++++++ .../resources/js/hooks/usePayPalScript.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js index 23c755d26..5205636e3 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalCommerceGateway.js @@ -1,11 +1,20 @@ import { useState, useEffect } from '@wordpress/element'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; +/** + * Custom hook to load and manage the PayPal Commerce Gateway configuration. + * + * @param {Object} initialConfig - Initial configuration object. + * @return {Object} An object containing the loaded config and a boolean indicating if it's loaded. + */ const usePayPalCommerceGateway = ( initialConfig ) => { const [ isConfigLoaded, setIsConfigLoaded ] = useState( false ); const [ ppcpConfig, setPpcpConfig ] = useState( initialConfig ); useEffect( () => { + /** + * Function to load the PayPal Commerce Gateway configuration. + */ const loadConfig = () => { if ( typeof window.PayPalCommerceGateway !== 'undefined' ) { setPpcpConfig( window.PayPalCommerceGateway ); @@ -15,17 +24,22 @@ const usePayPalCommerceGateway = ( initialConfig ) => { } }; + // Check if the DOM is still loading if ( document.readyState === 'loading' ) { + // If it's loading, add an event listener for when the DOM is fully loaded document.addEventListener( 'DOMContentLoaded', loadConfig ); } else { + // If it's already loaded, call the loadConfig function immediately loadConfig(); } + // Cleanup function to remove the event listener return () => { document.removeEventListener( 'DOMContentLoaded', loadConfig ); }; }, [] ); + // Return the loaded configuration and the loading status return { isConfigLoaded, ppcpConfig }; }; diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index 1999ecaba..a90321f8e 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -5,7 +5,7 @@ import UnifiedScriptLoader from '../../../../ppcp-button/resources/js/modules/He import { STORE_NAME } from '../stores/axoStore'; /** - * Custom hook to load the PayPal script using UnifiedScriptLoader. + * Custom hook to load the PayPal script. * * @param {string} namespace - Namespace for the PayPal script. * @param {Object} ppcpConfig - Configuration object for PayPal script. From e76c8b257be28ea32f415ea65f0c367e4b736b80 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 8 Oct 2024 00:41:30 +0200 Subject: [PATCH 29/30] Rename the new script loading script --- .../resources/js/hooks/usePayPalScript.js | 7 +- modules/ppcp-axo/resources/js/boot.js | 4 +- .../resources/js/checkout-block.js | 13 ++- .../ContextBootstrap/SingleProductBootstap.js | 6 +- .../js/modules/Helper/ConfigProcessor.js | 34 ++++++ .../js/modules/Helper/PayPalScriptLoading.js | 101 ++++++++++++++++++ .../js/modules/Helper/UnifiedScriptLoader.js | 75 ------------- .../ppcp-googlepay/resources/js/boot-block.js | 4 +- modules/ppcp-googlepay/resources/js/boot.js | 4 +- 9 files changed, 153 insertions(+), 95 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js create mode 100644 modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js delete mode 100644 modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index a90321f8e..1d536344f 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -1,7 +1,7 @@ import { useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; -import UnifiedScriptLoader from '../../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; import { STORE_NAME } from '../stores/axoStore'; /** @@ -28,10 +28,7 @@ const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => { const loadScript = async () => { if ( ! isPayPalLoaded && isConfigLoaded ) { try { - await UnifiedScriptLoader.loadPayPalScript( - namespace, - ppcpConfig - ); + await loadPayPalScript( namespace, ppcpConfig ); setIsPayPalLoaded( true ); } catch ( error ) { log( diff --git a/modules/ppcp-axo/resources/js/boot.js b/modules/ppcp-axo/resources/js/boot.js index 9a706360e..1effce798 100644 --- a/modules/ppcp-axo/resources/js/boot.js +++ b/modules/ppcp-axo/resources/js/boot.js @@ -1,5 +1,5 @@ import AxoManager from './AxoManager'; -import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; import { log } from './Helper/Debug'; ( function ( { axoConfig, ppcpConfig, jQuery } ) { @@ -15,7 +15,7 @@ import { log } from './Helper/Debug'; } // Load PayPal - UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) + loadPayPalScript( namespace, ppcpConfig ) .then( () => { bootstrap(); } ) diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index b37dabda1..97590838e 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -15,7 +15,7 @@ import { cartHasSubscriptionProducts, isPayPalSubscription, } from './Helper/Subscription'; -import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; @@ -56,7 +56,7 @@ const PayPalComponent = ( { if ( ! paypalScriptLoaded ) { if ( ! paypalScriptPromise ) { // for editor, since canMakePayment was not called - paypalScriptPromise = UnifiedScriptLoader.loadPayPalScript( + paypalScriptPromise = loadPayPalScript( namespace, config.scriptData ); @@ -825,11 +825,10 @@ if ( block_enabled && config.enabled ) { ariaLabel: config.title, canMakePayment: async () => { if ( ! paypalScriptPromise ) { - paypalScriptPromise = - UnifiedScriptLoader.loadPayPalScript( - namespace, - config.scriptData - ); + paypalScriptPromise = loadPayPalScript( + namespace, + config.scriptData + ); paypalScriptPromise.then( () => { const messagesBootstrap = new BlockCheckoutMessagesBootstrap( diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index 59e3a0d2c..f2179fb1b 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -2,7 +2,7 @@ import UpdateCart from '../Helper/UpdateCart'; import SingleProductActionHandler from '../ActionHandler/SingleProductActionHandler'; import { hide, show } from '../Helper/Hiding'; import BootstrapHelper from '../Helper/BootstrapHelper'; -import { loadPaypalJsScript } from '../Helper/ScriptLoading'; +import { loadAndRenderPayPalScript } from '../Helper/PayPalScriptLoading'; import { getPlanIdFromVariation } from '../Helper/Subscriptions'; import SimulateCart from '../Helper/SimulateCart'; import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils'; @@ -38,6 +38,7 @@ class SingleProductBootstap { ); this.subscriptionButtonsLoaded = false; + this.namespace = 'ppcpPaypalSingleProduct'; } form() { @@ -254,7 +255,8 @@ class SingleProductBootstap { if ( this.subscriptionButtonsLoaded ) { return; } - loadPaypalJsScript( + loadAndRenderPayPalScript( + this.namespace, { clientId: PayPalCommerceGateway.client_id, currency: PayPalCommerceGateway.currency, diff --git a/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js b/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js new file mode 100644 index 000000000..b70403a50 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/ConfigProcessor.js @@ -0,0 +1,34 @@ +import merge from 'deepmerge'; +import { v4 as uuidv4 } from 'uuid'; +import { keysToCamelCase } from './Utils'; + +const processAxoConfig = ( config ) => { + const scriptOptions = {}; + const sdkClientToken = config?.axo?.sdk_client_token; + const uuid = uuidv4().replace( /-/g, '' ); + if ( sdkClientToken ) { + scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken; + scriptOptions[ 'data-client-metadata-id' ] = uuid; + } + return scriptOptions; +}; + +const processUserIdToken = ( config, sdkClientToken ) => { + const userIdToken = config?.save_payment_methods?.id_token; + return userIdToken && ! sdkClientToken + ? { 'data-user-id-token': userIdToken } + : {}; +}; + +export const processConfig = ( config ) => { + let scriptOptions = keysToCamelCase( config.url_params ); + if ( config.script_attributes ) { + scriptOptions = merge( scriptOptions, config.script_attributes ); + } + const axoOptions = processAxoConfig( config ); + const userIdTokenOptions = processUserIdToken( + config, + axoOptions[ 'data-sdk-client-token' ] + ); + return merge.all( [ scriptOptions, axoOptions, userIdTokenOptions ] ); +}; diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js new file mode 100644 index 000000000..2fd5feaa3 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/PayPalScriptLoading.js @@ -0,0 +1,101 @@ +import { loadScript } from '@paypal/paypal-js'; +import dataClientIdAttributeHandler from '../DataClientIdAttributeHandler'; +import widgetBuilder from '../Renderer/WidgetBuilder'; +import { processConfig } from './ConfigProcessor'; + +const loadedScripts = new Map(); +const scriptPromises = new Map(); + +const handleDataClientIdAttribute = async ( scriptOptions, config ) => { + if ( + config.data_client_id?.set_attribute && + config.vault_v3_enabled !== '1' + ) { + return new Promise( ( resolve, reject ) => { + dataClientIdAttributeHandler( + scriptOptions, + config.data_client_id, + ( paypal ) => { + widgetBuilder.setPaypal( paypal ); + resolve( paypal ); + }, + reject + ); + } ); + } + return null; +}; + +export const loadPayPalScript = async ( namespace, config ) => { + if ( ! namespace ) { + throw new Error( 'Namespace is required' ); + } + + if ( loadedScripts.has( namespace ) ) { + console.log( `Script already loaded for namespace: ${ namespace }` ); + return loadedScripts.get( namespace ); + } + + if ( scriptPromises.has( namespace ) ) { + console.log( + `Script loading in progress for namespace: ${ namespace }` + ); + return scriptPromises.get( namespace ); + } + + const scriptOptions = { + ...processConfig( config ), + 'data-namespace': namespace, + }; + + const dataClientIdResult = await handleDataClientIdAttribute( + scriptOptions, + config + ); + if ( dataClientIdResult ) { + return dataClientIdResult; + } + + const scriptPromise = new Promise( ( resolve, reject ) => { + loadScript( scriptOptions ) + .then( ( script ) => { + widgetBuilder.setPaypal( script ); + loadedScripts.set( namespace, script ); + console.log( `Script loaded for namespace: ${ namespace }` ); + resolve( script ); + } ) + .catch( ( error ) => { + console.error( + `Failed to load script for namespace: ${ namespace }`, + error + ); + reject( error ); + } ) + .finally( () => { + scriptPromises.delete( namespace ); + } ); + } ); + + scriptPromises.set( namespace, scriptPromise ); + return scriptPromise; +}; + +export const loadAndRenderPayPalScript = async ( + namespace, + options, + renderFunction, + renderTarget +) => { + if ( ! namespace ) { + throw new Error( 'Namespace is required' ); + } + + const scriptOptions = { + ...options, + 'data-namespace': namespace, + }; + + const script = await loadScript( scriptOptions ); + widgetBuilder.setPaypal( script ); + await renderFunction( script, renderTarget ); +}; diff --git a/modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js b/modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js deleted file mode 100644 index a28d40d2c..000000000 --- a/modules/ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader.js +++ /dev/null @@ -1,75 +0,0 @@ -import { loadScript } from '@paypal/paypal-js'; - -class UnifiedScriptLoader { - constructor() { - this.loadedScripts = new Map(); - this.scriptPromises = new Map(); - } - - async loadPayPalScript( namespace, config ) { - if ( ! namespace ) { - throw new Error( 'Namespace is required' ); - } - - if ( this.loadedScripts.has( namespace ) ) { - console.log( - `PayPal script already loaded for namespace: ${ namespace }` - ); - return this.loadedScripts.get( namespace ); - } - - if ( this.scriptPromises.has( namespace ) ) { - console.log( - `PayPal script loading in progress for namespace: ${ namespace }` - ); - return this.scriptPromises.get( namespace ); - } - - const scriptPromise = new Promise( ( resolve, reject ) => { - const scriptOptions = { - ...config.url_params, - ...config.script_attributes, - 'data-namespace': namespace, - }; - - if ( config.axo?.sdk_client_token ) { - scriptOptions[ 'data-sdk-client-token' ] = - config.axo.sdk_client_token; - scriptOptions[ 'data-client-metadata-id' ] = - config.axo.client_metadata_id; - } - - if ( - config.save_payment_methods?.id_token && - ! config.axo?.sdk_client_token - ) { - scriptOptions[ 'data-user-id-token' ] = - config.save_payment_methods.id_token; - } - - loadScript( scriptOptions ) - .then( ( paypal ) => { - this.loadedScripts.set( namespace, paypal ); - console.log( - `PayPal script loaded for namespace: ${ namespace }` - ); - resolve( paypal ); - } ) - .catch( ( error ) => { - console.error( - `Failed to load PayPal script for namespace: ${ namespace }`, - error - ); - reject( error ); - } ) - .finally( () => { - this.scriptPromises.delete( namespace ); - } ); - } ); - - this.scriptPromises.set( namespace, scriptPromise ); - return scriptPromise; - } -} - -export default new UnifiedScriptLoader(); diff --git a/modules/ppcp-googlepay/resources/js/boot-block.js b/modules/ppcp-googlepay/resources/js/boot-block.js index acb5a310d..bc359be7d 100644 --- a/modules/ppcp-googlepay/resources/js/boot-block.js +++ b/modules/ppcp-googlepay/resources/js/boot-block.js @@ -4,7 +4,7 @@ import { registerPaymentMethod, } from '@woocommerce/blocks-registry'; import { __ } from '@wordpress/i18n'; -import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; import GooglepayManager from './GooglepayManager'; import { loadCustomScript } from '@paypal/paypal-js'; import GooglepayManagerBlockEditor from './GooglepayManagerBlockEditor'; @@ -35,7 +35,7 @@ const GooglePayComponent = ( props ) => { ppcpConfig.url_params.components += ',googlepay'; // Load PayPal - UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) + loadPayPalScript( namespace, ppcpConfig ) .then( () => { setPaypalLoaded( true ); } ) diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js index f4673fd07..7c839e858 100644 --- a/modules/ppcp-googlepay/resources/js/boot.js +++ b/modules/ppcp-googlepay/resources/js/boot.js @@ -8,7 +8,7 @@ */ import { loadCustomScript } from '@paypal/paypal-js'; -import UnifiedScriptLoader from '../../../ppcp-button/resources/js/modules/Helper/UnifiedScriptLoader'; +import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; import GooglepayManager from './GooglepayManager'; import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper'; import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap'; @@ -86,7 +86,7 @@ import moduleStorage from './Helper/GooglePayStorage'; } ); // Load PayPal - UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig ) + loadPayPalScript( namespace, ppcpConfig ) .then( () => { paypalLoaded = true; tryToBoot(); From 5970aeaceb43329687bbb8fdae6c534a903b8942 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 8 Oct 2024 10:57:09 +0200 Subject: [PATCH 30/30] Remove the new loader from the SingleProductBootstrap --- .../js/modules/ContextBootstrap/SingleProductBootstap.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index f2179fb1b..59e3a0d2c 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -2,7 +2,7 @@ import UpdateCart from '../Helper/UpdateCart'; import SingleProductActionHandler from '../ActionHandler/SingleProductActionHandler'; import { hide, show } from '../Helper/Hiding'; import BootstrapHelper from '../Helper/BootstrapHelper'; -import { loadAndRenderPayPalScript } from '../Helper/PayPalScriptLoading'; +import { loadPaypalJsScript } from '../Helper/ScriptLoading'; import { getPlanIdFromVariation } from '../Helper/Subscriptions'; import SimulateCart from '../Helper/SimulateCart'; import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils'; @@ -38,7 +38,6 @@ class SingleProductBootstap { ); this.subscriptionButtonsLoaded = false; - this.namespace = 'ppcpPaypalSingleProduct'; } form() { @@ -255,8 +254,7 @@ class SingleProductBootstap { if ( this.subscriptionButtonsLoaded ) { return; } - loadAndRenderPayPalScript( - this.namespace, + loadPaypalJsScript( { clientId: PayPalCommerceGateway.client_id, currency: PayPalCommerceGateway.currency,