diff --git a/modules/ppcp-blocks/resources/js/Components/paypal.js b/modules/ppcp-blocks/resources/js/Components/paypal.js index 92287381f..f2185f0b7 100644 --- a/modules/ppcp-blocks/resources/js/Components/paypal.js +++ b/modules/ppcp-blocks/resources/js/Components/paypal.js @@ -21,6 +21,7 @@ import { onApproveSavePayment, } from '../paypal-config'; import { useRef } from 'react'; +import Spinner from '../../../../ppcp-button/resources/js/modules/Helper/Spinner'; const PAYPAL_GATEWAY_ID = 'ppcp-gateway'; @@ -52,8 +53,11 @@ export const PayPalComponent = ( { useState( false ); const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false ); + const [ isFullPageSpinnerActive, setIsFullPageSpinnerActive ] = + useState( false ); const paypalButtonRef = useRef( null ); + const spinnerRef = useRef( null ); if ( ! paypalScriptLoaded ) { if ( ! paypalScriptPromise ) { @@ -70,6 +74,18 @@ export const PayPalComponent = ( { ? `${ config.id }-${ fundingSource }` : config.id; + // Full-page spinner used to block UI interactions during flows like AppSwitch. + useEffect( () => { + if ( isFullPageSpinnerActive ) { + if ( ! spinnerRef.current ) { + spinnerRef.current = Spinner.fullPage(); + } + spinnerRef.current.block(); + } else if ( spinnerRef.current ) { + spinnerRef.current.unblock(); + } + }, [ isFullPageSpinnerActive ] ); + useEffect( () => { // fill the form if in continuation (for product or mini-cart buttons) if ( continuationFilled || ! config.scriptData.continuation?.order ) { @@ -286,6 +302,14 @@ export const PayPalComponent = ( { }; }, [ onPaymentSetup, paypalOrder, activePaymentMethod ] ); + useEffect( () => { + const unsubscribe = onCheckoutFail( () => { + setIsFullPageSpinnerActive( false ); + } ); + + return unsubscribe; + }, [ onCheckoutFail ] ); + useEffect( () => { if ( activePaymentMethod !== methodId ) { return; @@ -336,7 +360,8 @@ export const PayPalComponent = ( { setGotoContinuationOnError, onSubmit, onError, - onClose + onClose, + setIsFullPageSpinnerActive ); }, } @@ -491,7 +516,8 @@ export const PayPalComponent = ( { setGotoContinuationOnError, onSubmit, onError, - onClose + onClose, + setIsFullPageSpinnerActive ) } onShippingOptionsChange={ getOnShippingOptionsChange( diff --git a/modules/ppcp-blocks/resources/js/paypal-config.js b/modules/ppcp-blocks/resources/js/paypal-config.js index 4aacab318..93105786f 100644 --- a/modules/ppcp-blocks/resources/js/paypal-config.js +++ b/modules/ppcp-blocks/resources/js/paypal-config.js @@ -64,8 +64,11 @@ export const handleApprove = async ( setGotoContinuationOnError, onSubmit, onError, - onClose + onClose, + setIsFullPageSpinnerActive ) => { + setIsFullPageSpinnerActive( true ); + try { let order; @@ -168,6 +171,8 @@ export const handleApprove = async ( } catch ( err ) { console.error( err ); + setIsFullPageSpinnerActive( false ); + onError( err.message ); onClose(); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstrap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstrap.js index ef51b32d5..87dadb468 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstrap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstrap.js @@ -8,6 +8,7 @@ import SimulateCart from '../Helper/SimulateCart'; import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils'; import merge from 'deepmerge'; import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce'; +import ResumeFlowHelper from '../Helper/ResumeFlowHelper'; class SingleProductBootstrap { constructor( gateway, renderer, errorHandler ) { @@ -53,7 +54,10 @@ class SingleProductBootstrap { return; } - this.render(); + // Avoid re-rendering during the resume flow to prevent duplicate onApprove callbacks. + if ( ! ResumeFlowHelper.isResumeFlow() ) { + this.render(); + } this.renderer.enableSmartButtons( this.gateway.button.wrapper ); show( this.gateway.button.wrapper ); diff --git a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js index 30d0830f9..63f941279 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js +++ b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js @@ -14,12 +14,16 @@ class Spinner { background: '#fff', opacity: 0.6, }, + baseZ: 10000, } ); } unblock() { jQuery( this.target ).unblock(); } -} + static fullPage() { + return new Spinner( window ); + } +} export default Spinner; diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js index 13b914335..d5c5ab6dc 100644 --- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js +++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js @@ -1,3 +1,5 @@ +import Spinner from '../Helper/Spinner'; + const initiateRedirect = ( successUrl ) => { /** * Notice how this step initiates a redirect to a new page using a plain @@ -15,6 +17,10 @@ const initiateRedirect = ( successUrl ) => { const onApprove = ( context, errorHandler ) => { return ( data, actions ) => { + // Block the entire page during approval process + const spinner = Spinner.fullPage(); + spinner.block(); + const canCreateOrder = ! context.config.vaultingEnabled || data.paymentSource !== 'venmo'; @@ -50,6 +56,9 @@ const onApprove = ( context, errorHandler ) => { const orderReceivedUrl = approveData.data?.order_received_url; initiateRedirect( orderReceivedUrl || context.config.redirect ); + } ) + .finally( () => { + spinner.unblock(); } ); }; };