Skip to content

Commit

Permalink
Get WooPay 1st party auth flow to work on page load (#7602)
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarcosta99 authored Nov 6, 2023
1 parent 28f6363 commit d788b1f
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 34 deletions.
4 changes: 4 additions & 0 deletions changelog/update-2210-1st-party-auth-on-page-load
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: update

Get WooPay 1st party auth flow to work on page load.
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
} ) );
Expand Down Expand Up @@ -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(
<WoopayExpressCheckoutButton
isPreview={ false }
buttonSettings={ buttonSettings }
api={ api }
isProductPage={ false }
emailSelector="#email"
/>
);

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(
<WoopayExpressCheckoutButton
isPreview={ false }
buttonSettings={ buttonSettings }
api={ api }
isProductPage={ false }
emailSelector="#email"
/>
);

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';
Expand All @@ -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(
<WoopayExpressCheckoutButton
isPreview={ true }
Expand All @@ -122,10 +191,29 @@ describe( 'WoopayExpressCheckoutButton', () => {
} );
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(
<WoopayExpressCheckoutButton
isPreview={ false }
buttonSettings={ buttonSettings }
api={ api }
isProductPage={ true }
emailSelector="#email"
/>
);

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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
}
Expand Down Expand Up @@ -252,7 +267,7 @@ export const WoopayExpressCheckoutButton = ( {
getConfig( 'woopayHost' )
);
} )
.catch( () => {
?.catch( () => {
const errorMessage = __(
'Something went wrong. Please try again.',
'woocommerce-payments'
Expand All @@ -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' ) ) {
Expand Down Expand Up @@ -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 );
}
Expand All @@ -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 ) => {
Expand Down

0 comments on commit d788b1f

Please sign in to comment.