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 ) => {