From 5e1c2a22d26a1320e239aaf77b387d570230084c Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Sat, 5 Oct 2024 02:26:09 +0200 Subject: [PATCH] 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,