From d788b1f1806c79a223fe3e20ca56454652fbdc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Costa?= <10233985+cesarcosta99@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:41:20 -0500 Subject: [PATCH] Get WooPay 1st party auth flow to work on page load (#7602) --- .../update-2210-1st-party-auth-on-page-load | 4 + .../woopay-express-checkout-button.test.js | 92 ++++++++++++++++++- .../woopay-express-checkout-button.js | 89 +++++++++++------- 3 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 changelog/update-2210-1st-party-auth-on-page-load diff --git a/changelog/update-2210-1st-party-auth-on-page-load b/changelog/update-2210-1st-party-auth-on-page-load new file mode 100644 index 00000000000..27e47f358dc --- /dev/null +++ b/changelog/update-2210-1st-party-auth-on-page-load @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Get WooPay 1st party auth flow to work on page load. diff --git a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js index 457b1dabb4b..f37f92efac2 100644 --- a/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js +++ b/client/checkout/woopay/express-button/test/woopay-express-checkout-button.test.js @@ -10,10 +10,16 @@ import userEvent from '@testing-library/user-event'; import { WoopayExpressCheckoutButton } from '../woopay-express-checkout-button'; import { expressCheckoutIframe } from '../express-checkout-iframe'; import WCPayAPI from 'wcpay/checkout/api'; +import request from 'wcpay/checkout/utils/request'; import { getConfig } from 'utils/checkout'; import wcpayTracks from 'tracks'; import useExpressCheckoutProductHandler from '../use-express-checkout-product-handler'; +jest.mock( 'wcpay/checkout/utils/request', () => ( { + __esModule: true, + default: jest.fn( () => Promise.resolve( {} ) ), +} ) ); + jest.mock( 'utils/checkout', () => ( { getConfig: jest.fn(), } ) ); @@ -80,6 +86,69 @@ describe( 'WoopayExpressCheckoutButton', () => { ).toBeInTheDocument(); } ); + test( 'prefetch session data by default', async () => { + getConfig.mockImplementation( ( v ) => { + switch ( v ) { + case 'wcAjaxUrl': + return 'woopay.url'; + case 'woopaySessionNonce': + return 'sessionnonce'; + default: + return 'foo'; + } + } ); + render( + + ); + + await waitFor( () => { + expect( request ).toHaveBeenCalledWith( 'woopay.url', { + _ajax_nonce: 'sessionnonce', + } ); + expect( expressCheckoutIframe ).not.toHaveBeenCalled(); + } ); + } ); + + test( 'request session data on button click', async () => { + getConfig.mockImplementation( ( v ) => { + switch ( v ) { + case 'wcAjaxUrl': + return 'woopay.url'; + case 'woopaySessionNonce': + return 'sessionnonce'; + default: + return 'foo'; + } + } ); + render( + + ); + + const expressButton = screen.queryByRole( 'button', { + name: 'WooPay', + } ); + userEvent.click( expressButton ); + + await waitFor( () => { + expect( request ).toHaveBeenCalledWith( 'woopay.url', { + _ajax_nonce: 'sessionnonce', + } ); + expect( expressCheckoutIframe ).not.toHaveBeenCalled(); + } ); + } ); + test( 'call `expressCheckoutIframe` on button click when `isPreview` is false', () => { getConfig.mockImplementation( ( v ) => { return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo'; @@ -106,7 +175,7 @@ describe( 'WoopayExpressCheckoutButton', () => { ); } ); - test( 'should not call `expressCheckoutIframe` on button click when `isPreview` is true', () => { + test( 'should not call `expressCheckoutIframe` or request session data on button click when `isPreview` is true', async () => { render( { } ); userEvent.click( expressButton ); - expect( expressCheckoutIframe ).not.toHaveBeenCalled(); + await waitFor( () => { + expect( request ).not.toHaveBeenCalled(); + expect( expressCheckoutIframe ).not.toHaveBeenCalled(); + } ); } ); describe( 'Product Page', () => { + test( 'does not prefetch session data by default', async () => { + render( + + ); + + await waitFor( () => { + expect( request ).not.toHaveBeenCalled(); + } ); + } ); + test( 'should shown an alert when clicking the button when add to cart button is disabled', () => { getConfig.mockImplementation( ( v ) => { return v === 'isWoopayFirstPartyAuthEnabled' ? false : 'foo'; diff --git a/client/checkout/woopay/express-button/woopay-express-checkout-button.js b/client/checkout/woopay/express-button/woopay-express-checkout-button.js index ea53795faf4..d0900eee4c3 100644 --- a/client/checkout/woopay/express-button/woopay-express-checkout-button.js +++ b/client/checkout/woopay/express-button/woopay-express-checkout-button.js @@ -35,6 +35,7 @@ export const WoopayExpressCheckoutButton = ( { const sessionDataPromiseRef = useRef( null ); const initWoopayRef = useRef( null ); const buttonRef = useRef( null ); + const initialOnClickEventRef = useRef( null ); const isLoadingRef = useRef( false ); const { type: buttonType, height, size, theme, context } = buttonSettings; const [ isLoading, setIsLoading ] = useState( false ); @@ -83,9 +84,19 @@ export const WoopayExpressCheckoutButton = ( { } }, [ isPreview, context ] ); - const defaultOnClick = useCallback( + const defaultOnClick = useCallback( ( event ) => { + // This will only be called if user clicks the button too quickly. + // It saves the event for later use. + initialOnClickEventRef.current = event; + // Set isLoadingRef to true to prevent multiple clicks. + isLoadingRef.current = true; + setIsLoading( true ); + }, [] ); + + const onClickFallback = useCallback( + // OTP flow ( e ) => { - e.preventDefault(); + e?.preventDefault(); if ( isPreview ) { return; // eslint-disable-line no-useless-return @@ -116,23 +127,18 @@ export const WoopayExpressCheckoutButton = ( { return; } - addToCartRef - .current( productData ) - .then( ( res ) => { - if ( res.error ) { - if ( res.submit ) { - // Some extensions needs to submit the form - // to show error messages. - document.querySelector( 'form.cart' ).submit(); - } - return; + addToCartRef.current( productData ).then( ( res ) => { + if ( res.error ) { + if ( res.submit ) { + // Some extensions needs to submit the form + // to show error messages. + document.querySelector( 'form.cart' ).submit(); } + return; + } - expressCheckoutIframe( api, context, emailSelector ); - } ) - .catch( () => { - // handle error. - } ); + expressCheckoutIframe( api, context, emailSelector ); + } ); } else { expressCheckoutIframe( api, context, emailSelector ); } @@ -174,12 +180,19 @@ export const WoopayExpressCheckoutButton = ( { iframe.style.position = 'absolute'; iframe.style.top = '0'; + iframe.addEventListener( 'error', () => { + initWoopayRef.current = onClickFallback; + } ); + iframe.addEventListener( 'load', () => { // Change button's onClick handle to use express checkout flow. initWoopayRef.current = ( e ) => { e.preventDefault(); - if ( isPreview || isLoadingRef.current ) { + if ( + isPreview || + ( isLoadingRef.current && ! initialOnClickEventRef.current ) + ) { return; } @@ -201,14 +214,16 @@ export const WoopayExpressCheckoutButton = ( { return; } - if ( listenForCartChanges.stop ) { + if ( typeof listenForCartChanges.stop === 'function' ) { // Temporarily stop listening for cart changes to prevent // rendering a new button + iFrame when the cart is updated. listenForCartChanges.stop(); } addToCartRef.current( productData ).then( () => { - if ( listenForCartChanges.start ) { + if ( + typeof listenForCartChanges.start === 'function' + ) { // Start listening for cart changes, again. listenForCartChanges.start(); } @@ -252,7 +267,7 @@ export const WoopayExpressCheckoutButton = ( { getConfig( 'woopayHost' ) ); } ) - .catch( () => { + ?.catch( () => { const errorMessage = __( 'Something went wrong. Please try again.', 'woocommerce-payments' @@ -263,10 +278,21 @@ export const WoopayExpressCheckoutButton = ( { } ); } }; + + // Trigger first party auth flow if button was clicked before iframe was loaded. + if ( initialOnClickEventRef.current ) { + initWoopayRef.current( initialOnClickEventRef.current ); + } } ); return iframe; - }, [ isProductPage, context, isPreview, listenForCartChanges ] ); + }, [ + isProductPage, + context, + isPreview, + listenForCartChanges, + onClickFallback, + ] ); useEffect( () => { if ( isPreview || ! getConfig( 'isWoopayFirstPartyAuthEnabled' ) ) { @@ -303,14 +329,10 @@ export const WoopayExpressCheckoutButton = ( { if ( isSessionDataSuccess ) { window.location.href = event.data.value.redirect_url; } else if ( isSessionDataError ) { - const errorMessage = __( - 'WooPay is unavailable at this time. Please try again.', - 'woocommerce-payments' - ); - showErrorMessage( context, errorMessage ); + onClickFallback( null ); // Set button's default onClick handle to use modal checkout flow. - initWoopayRef.current = defaultOnClick; + initWoopayRef.current = onClickFallback; isLoadingRef.current = false; setIsLoading( false ); } @@ -322,12 +344,15 @@ export const WoopayExpressCheckoutButton = ( { window.removeEventListener( 'message', onMessage ); }; // Note: Any changes to this dependency array may cause a duplicate iframe to be appended. - }, [ context, defaultOnClick, isPreview, isProductPage, newIframe ] ); + }, [ context, onClickFallback, isPreview, isProductPage, newIframe ] ); useEffect( () => { - // Set button's default onClick handle to use modal checkout flow. - initWoopayRef.current = defaultOnClick; - }, [ defaultOnClick ] ); + if ( getConfig( 'isWoopayFirstPartyAuthEnabled' ) ) { + initWoopayRef.current = defaultOnClick; + } else { + initWoopayRef.current = onClickFallback; + } + }, [ defaultOnClick, onClickFallback ] ); useEffect( () => { const handlePageShow = ( event ) => {