Introduce a unified script loader to improve script loading, debugging and prevent conflicts

This commit is contained in:
Daniel Dudzic 2024-10-05 02:26:09 +02:00
parent 1325779582
commit 5e1c2a22d2
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
10 changed files with 186 additions and 65 deletions

View file

@ -15,12 +15,20 @@ import useCardChange from './useCardChange';
/** /**
* Custom hook to set up AXO functionality. * Custom hook to set up AXO functionality.
* *
* @param {Object} ppcpConfig - PayPal Checkout configuration. * @param {string} namespace - Namespace for the PayPal script.
* @param {Object} fastlaneSdk - Fastlane SDK instance. * @param {Object} ppcpConfig - PayPal Checkout configuration.
* @param {Object} paymentComponent - Payment component instance. * @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. * @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 // Get dispatch functions from the AXO store
const { const {
setIsAxoActive, setIsAxoActive,
@ -30,7 +38,11 @@ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => {
} = useDispatch( STORE_NAME ); } = useDispatch( STORE_NAME );
// Check if PayPal script has loaded // Check if PayPal script has loaded
const paypalLoaded = usePayPalScript( ppcpConfig ); const paypalLoaded = usePayPalScript(
namespace,
ppcpConfig,
isConfigLoaded
);
// Set up card and shipping address change handlers // Set up card and shipping address change handlers
const onChangeCardButtonClick = useCardChange( fastlaneSdk ); const onChangeCardButtonClick = useCardChange( fastlaneSdk );

View file

@ -1,24 +1,31 @@
import { useEffect, useRef, useState, useMemo } from '@wordpress/element'; import { useEffect, useRef, useState, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane'; import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys'; import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
import { STORE_NAME } from '../stores/axoStore';
/** /**
* Custom hook to initialize and manage the Fastlane SDK. * 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} axoConfig - Configuration for AXO.
* @param {Object} ppcpConfig - Configuration for PPCP. * @param {Object} ppcpConfig - Configuration for PPCP.
* @return {Object|null} The initialized Fastlane SDK instance or null. * @return {Object|null} The initialized Fastlane SDK instance or null.
*/ */
const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
const [ fastlaneSdk, setFastlaneSdk ] = useState( null ); const [ fastlaneSdk, setFastlaneSdk ] = useState( null );
// Ref to prevent multiple simultaneous initializations
const initializingRef = useRef( false ); const initializingRef = useRef( false );
// Ref to hold the latest config values
const configRef = useRef( { axoConfig, ppcpConfig } ); const configRef = useRef( { axoConfig, ppcpConfig } );
// Custom hook to remove empty keys from an object
const deleteEmptyKeys = useDeleteEmptyKeys(); const deleteEmptyKeys = useDeleteEmptyKeys();
const { isPayPalLoaded } = useSelect(
( select ) => ( {
isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(),
} ),
[]
);
const styleOptions = useMemo( () => { const styleOptions = useMemo( () => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options ); return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] ); }, [ deleteEmptyKeys ] );
@ -26,7 +33,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => {
// Effect to initialize Fastlane SDK // Effect to initialize Fastlane SDK
useEffect( () => { useEffect( () => {
const initFastlane = async () => { const initFastlane = async () => {
if ( initializingRef.current || fastlaneSdk ) { if ( initializingRef.current || fastlaneSdk || ! isPayPalLoaded ) {
return; return;
} }
@ -34,7 +41,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => {
log( 'Init Fastlane' ); log( 'Init Fastlane' );
try { try {
const fastlane = new Fastlane(); const fastlane = new Fastlane( namespace );
// Set sandbox environment if configured // Set sandbox environment if configured
if ( configRef.current.axoConfig.environment.is_sandbox ) { if ( configRef.current.axoConfig.environment.is_sandbox ) {
@ -59,7 +66,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => {
}; };
initFastlane(); initFastlane();
}, [ fastlaneSdk, styleOptions ] ); }, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace ] );
// Effect to update the config ref when configs change // Effect to update the config ref when configs change
useEffect( () => { useEffect( () => {

View file

@ -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 { 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. * @return {boolean} True if the PayPal script has loaded, false otherwise.
*/ */
const usePayPalScript = ( ppcpConfig ) => { const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => {
const [ isLoaded, setIsLoaded ] = useState( false ); // 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( () => { useEffect( () => {
if ( ! isLoaded ) { const loadScript = async () => {
log( 'Loading PayPal script' ); 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 loadScript();
loadPaypalScript( ppcpConfig, () => { }, [ ppcpConfig, isConfigLoaded, isPayPalLoaded ] );
log( 'PayPal script loaded' );
setIsLoaded( true );
} );
}
}, [ ppcpConfig, isLoaded ] );
return isLoaded; return isPayPalLoaded;
}; };
export default usePayPalScript; export default usePayPalScript;

View file

@ -9,25 +9,25 @@ import useAxoSetup from './hooks/useAxoSetup';
import useAxoCleanup from './hooks/useAxoCleanup'; import useAxoCleanup from './hooks/useAxoCleanup';
import useHandlePaymentSetup from './hooks/useHandlePaymentSetup'; import useHandlePaymentSetup from './hooks/useHandlePaymentSetup';
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect'; import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
import usePayPalCommerceGateway from './hooks/usePayPalCommerceGateway';
// Components // Components
import { Payment } from './components/Payment/Payment'; import { Payment } from './components/Payment/Payment';
const gatewayHandle = 'ppcp-axo-gateway'; const gatewayHandle = 'ppcp-axo-gateway';
const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` ); const namespace = 'ppcpBlocksPaypalAxo';
const initialConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
}
const axoConfig = window.wc_ppcp_axo;
const Axo = ( props ) => { const Axo = ( props ) => {
const { eventRegistration, emitResponse } = props; const { eventRegistration, emitResponse } = props;
const { onPaymentSetup } = eventRegistration; const { onPaymentSetup } = eventRegistration;
const [ paymentComponent, setPaymentComponent ] = useState( null ); 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 tokenizedCustomerData = useTokenizeCustomerData();
const handlePaymentSetup = useHandlePaymentSetup( const handlePaymentSetup = useHandlePaymentSetup(
emitResponse, emitResponse,
@ -35,7 +35,13 @@ const Axo = ( props ) => {
tokenizedCustomerData tokenizedCustomerData
); );
useAxoSetup( ppcpConfig, fastlaneSdk, paymentComponent ); const isScriptLoaded = useAxoSetup(
namespace,
ppcpConfig,
isConfigLoaded,
fastlaneSdk,
paymentComponent
);
const { handlePaymentLoad } = usePaymentSetupEffect( const { handlePaymentLoad } = usePaymentSetupEffect(
onPaymentSetup, onPaymentSetup,
@ -45,31 +51,57 @@ const Axo = ( props ) => {
useAxoCleanup(); 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 (
<Payment <Payment
fastlaneSdk={ fastlaneSdk } fastlaneSdk={ fastlaneSdk }
onPaymentLoad={ handlePaymentLoad } onPaymentLoad={ handlePaymentLoad }
/> />
) : (
<>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</>
); );
}; };
registerPaymentMethod( { registerPaymentMethod( {
name: ppcpConfig.id, name: initialConfig.id,
label: ( label: (
<div <div
id="ppcp-axo-block-radio-label" id="ppcp-axo-block-radio-label"
dangerouslySetInnerHTML={ { __html: ppcpConfig.title } } dangerouslySetInnerHTML={ { __html: initialConfig.title } }
/> />
), ),
content: <Axo />, content: <Axo />,
edit: createElement( ppcpConfig.title ), edit: createElement( initialConfig.title ),
ariaLabel: ppcpConfig.title, ariaLabel: initialConfig.title,
canMakePayment: () => true, canMakePayment: () => true,
supports: { supports: {
showSavedCards: true, showSavedCards: true,
features: ppcpConfig.supports, features: initialConfig.supports,
}, },
} ); } );

View file

@ -3,6 +3,7 @@ import { createReduxStore, register, dispatch } from '@wordpress/data';
export const STORE_NAME = 'woocommerce-paypal-payments/axo-block'; export const STORE_NAME = 'woocommerce-paypal-payments/axo-block';
const DEFAULT_STATE = { const DEFAULT_STATE = {
isPayPalLoaded: false,
isGuest: true, isGuest: true,
isAxoActive: false, isAxoActive: false,
isAxoScriptLoaded: false, isAxoScriptLoaded: false,
@ -15,6 +16,10 @@ const DEFAULT_STATE = {
// Action creators for updating the store state // Action creators for updating the store state
const actions = { const actions = {
setIsPayPalLoaded: ( isPayPalLoaded ) => ( {
type: 'SET_IS_PAYPAL_LOADED',
payload: isPayPalLoaded,
} ),
setIsGuest: ( isGuest ) => ( { setIsGuest: ( isGuest ) => ( {
type: 'SET_IS_GUEST', type: 'SET_IS_GUEST',
payload: isGuest, payload: isGuest,
@ -58,6 +63,8 @@ const actions = {
*/ */
const reducer = ( state = DEFAULT_STATE, action ) => { const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) { switch ( action.type ) {
case 'SET_IS_PAYPAL_LOADED':
return { ...state, isPayPalLoaded: action.payload };
case 'SET_IS_GUEST': case 'SET_IS_GUEST':
return { ...state, isGuest: action.payload }; return { ...state, isGuest: action.payload };
case 'SET_IS_AXO_ACTIVE': case 'SET_IS_AXO_ACTIVE':
@ -81,6 +88,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
// Selector functions to retrieve specific pieces of state // Selector functions to retrieve specific pieces of state
const selectors = { const selectors = {
getIsPayPalLoaded: ( state ) => state.isPayPalLoaded,
getIsGuest: ( state ) => state.isGuest, getIsGuest: ( state ) => state.isGuest,
getIsAxoActive: ( state ) => state.isAxoActive, getIsAxoActive: ( state ) => state.isAxoActive,
getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded, getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
@ -102,6 +110,15 @@ register( store );
// Action dispatchers // 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. * Action dispatcher to update the guest status in the store.
* *

View file

@ -52,11 +52,12 @@ class AxoManager {
billingView = null; billingView = null;
cardView = null; cardView = null;
constructor( axoConfig, ppcpConfig ) { constructor( namespace, axoConfig, ppcpConfig ) {
this.namespace = namespace;
this.axoConfig = axoConfig; this.axoConfig = axoConfig;
this.ppcpConfig = ppcpConfig; this.ppcpConfig = ppcpConfig;
this.fastlane = new Fastlane(); this.fastlane = new Fastlane( namespace );
this.$ = jQuery; this.$ = jQuery;
this.status = { this.status = {

View file

@ -1,5 +1,6 @@
class Fastlane { class Fastlane {
construct() { constructor( namespace ) {
this.namespace = namespace;
this.connection = null; this.connection = null;
this.identity = null; this.identity = null;
this.profile = null; this.profile = null;
@ -10,7 +11,16 @@ class Fastlane {
connect( config ) { connect( config ) {
return new Promise( ( resolve, reject ) => { 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 ) .Fastlane( config )
.then( ( result ) => { .then( ( result ) => {
this.init( result ); this.init( result );
@ -18,7 +28,7 @@ class Fastlane {
} ) } )
.catch( ( error ) => { .catch( ( error ) => {
console.error( error ); console.error( error );
reject(); reject( error );
} ); } );
} ); } );
} }

View file

@ -1,21 +1,27 @@
import AxoManager from './AxoManager'; 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 } ) { ( function ( { axoConfig, ppcpConfig, jQuery } ) {
const namespace = 'ppcpPaypalClassicAxo';
const bootstrap = () => { const bootstrap = () => {
new AxoManager( axoConfig, ppcpConfig ); new AxoManager( namespace, axoConfig, ppcpConfig );
}; };
document.addEventListener( 'DOMContentLoaded', () => { document.addEventListener( 'DOMContentLoaded', () => {
if ( ! typeof PayPalCommerceGateway ) { if ( typeof PayPalCommerceGateway === 'undefined' ) {
console.error( 'AXO could not be configured.' ); console.error( 'AXO could not be configured.' );
return; return;
} }
// Load PayPal // Load PayPal
loadPaypalScript( ppcpConfig, () => { UnifiedScriptLoader.loadPayPalScript( namespace, ppcpConfig )
bootstrap(); .then( () => {
} ); console.log( 'PayPal script loaded successfully' );
bootstrap();
} )
.catch( ( error ) => {
console.error( 'Failed to load PayPal script:', error );
} );
} ); } );
} )( { } )( {
axoConfig: window.wc_ppcp_axo, axoConfig: window.wc_ppcp_axo,

View file

@ -378,7 +378,8 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
return ! is_user_logged_in() return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout() && CartCheckoutDetector::has_classic_checkout()
&& $dcc_configuration->use_fastlane() && $dcc_configuration->use_fastlane()
&& ! $this->is_excluded_endpoint(); && ! $this->is_excluded_endpoint()
&& is_checkout();
} }
/** /**

View file

@ -15,11 +15,12 @@ import {
cartHasSubscriptionProducts, cartHasSubscriptionProducts,
isPayPalSubscription, isPayPalSubscription,
} from './Helper/Subscription'; } 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 { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style'; import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style';
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap'; import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap';
const namespace = 'ppcpBlocksPaypalExpressButtons';
const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' );
window.ppcpFundingSource = config.fundingSource; window.ppcpFundingSource = config.fundingSource;
@ -55,7 +56,10 @@ const PayPalComponent = ( {
if ( ! paypalScriptLoaded ) { if ( ! paypalScriptLoaded ) {
if ( ! paypalScriptPromise ) { if ( ! paypalScriptPromise ) {
// for editor, since canMakePayment was not called // for editor, since canMakePayment was not called
paypalScriptPromise = loadPaypalScriptPromise( config.scriptData ); paypalScriptPromise = UnifiedScriptLoader.loadPayPalScript(
namespace,
config.scriptData
);
} }
paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) ); paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) );
} }
@ -614,7 +618,10 @@ const PayPalComponent = ( {
return null; return null;
} }
const PayPalButton = paypal.Buttons.driver( 'react', { React, ReactDOM } ); const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver(
'react',
{ React, ReactDOM }
);
const getOnShippingOptionsChange = ( fundingSource ) => { const getOnShippingOptionsChange = ( fundingSource ) => {
if ( fundingSource === 'venmo' ) { if ( fundingSource === 'venmo' ) {
@ -818,9 +825,11 @@ if ( block_enabled && config.enabled ) {
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: async () => { canMakePayment: async () => {
if ( ! paypalScriptPromise ) { if ( ! paypalScriptPromise ) {
paypalScriptPromise = loadPaypalScriptPromise( paypalScriptPromise =
config.scriptData UnifiedScriptLoader.loadPayPalScript(
); namespace,
config.scriptData
);
paypalScriptPromise.then( () => { paypalScriptPromise.then( () => {
const messagesBootstrap = const messagesBootstrap =
new BlockCheckoutMessagesBootstrap( new BlockCheckoutMessagesBootstrap(
@ -831,7 +840,9 @@ if ( block_enabled && config.enabled ) {
} }
await paypalScriptPromise; await paypalScriptPromise;
return paypal.Buttons( { fundingSource } ).isEligible(); return ppcpBlocksPaypalExpressButtons
.Buttons( { fundingSource } )
.isEligible();
}, },
supports: { supports: {
features, features,