Skip to content

Commit

Permalink
Add the WooPay Direct Checkout flow to the blocks mini cart widget (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardo authored May 16, 2024
1 parent c0033b4 commit f78e96c
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 141 deletions.
4 changes: 4 additions & 0 deletions changelog/add-woopay-direct-checkout-to-minicart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add the WooPay Direct Checkout flow to the blocks mini cart widget.
166 changes: 114 additions & 52 deletions client/checkout/woopay/direct-checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,94 @@ import { debounce } from 'lodash';
* Internal dependencies
*/
import { WC_STORE_CART } from 'wcpay/checkout/constants';
import { waitMilliseconds } from 'wcpay/checkout/woopay/direct-checkout/utils';
import {
waitMilliseconds,
waitForSelector,
} from 'wcpay/checkout/woopay/direct-checkout/utils';
import WooPayDirectCheckout from 'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout';
import { shouldSkipWooPay } from 'wcpay/checkout/woopay/utils';

let isThirdPartyCookieEnabled = false;

window.addEventListener( 'load', async () => {
if (
! WooPayDirectCheckout.isWooPayDirectCheckoutEnabled() ||
shouldSkipWooPay()
) {
/**
* Handle the WooPay direct checkout for the given checkout buttons.
*
* @param {HTMLElement[]} checkoutButtons An array of checkout button elements.
*/
const handleWooPayDirectCheckout = async ( checkoutButtons ) => {
if ( ! checkoutButtons ) {
return;
}

WooPayDirectCheckout.init();

isThirdPartyCookieEnabled = await WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled();
const checkoutElements = WooPayDirectCheckout.getCheckoutRedirectElements();
if ( isThirdPartyCookieEnabled ) {
if ( await WooPayDirectCheckout.isUserLoggedIn() ) {
WooPayDirectCheckout.maybePrefetchEncryptedSessionData();
WooPayDirectCheckout.redirectToWooPay( checkoutElements, true );
WooPayDirectCheckout.addRedirectToWooPayEventListener(
checkoutButtons,
true
);
}

return;
}

// Pass false to indicate we are not sure if the user is logged in or not.
WooPayDirectCheckout.redirectToWooPay( checkoutElements, false );
} );
WooPayDirectCheckout.addRedirectToWooPayEventListener(
checkoutButtons,
false
);
};

jQuery( ( $ ) => {
$( document.body ).on( 'updated_cart_totals', async () => {
if (
! WooPayDirectCheckout.isWooPayDirectCheckoutEnabled() ||
shouldSkipWooPay()
) {
return;
}
/**
* Add an event listener to the mini cart checkout button.
*/
const addMiniCartEventListener = () => {
const checkoutButton = WooPayDirectCheckout.getMiniCartProceedToCheckoutButton();
handleWooPayDirectCheckout( [ checkoutButton ] );
};

// When "updated_cart_totals" is triggered, the classic 'Proceed to Checkout' button is
// re-rendered. So, the click-event listener needs to be re-attached to the new button.
const checkoutButton = WooPayDirectCheckout.getClassicProceedToCheckoutButton();
if ( isThirdPartyCookieEnabled ) {
if ( await WooPayDirectCheckout.isUserLoggedIn() ) {
WooPayDirectCheckout.maybePrefetchEncryptedSessionData();
WooPayDirectCheckout.redirectToWooPay( [ checkoutButton ] );
}
/**
* If the mini cart widget is available on the page, observe when the drawer element gets added to the DOM.
*
* As of today, no window events are triggered when the mini cart is opened or closed,
* nor there are attribute changes to the "open" button, so we have to rely on a MutationObserver
* attached to the `document.body`, which is where the mini cart drawer element is added.
*/
const maybeObserveMiniCart = () => {
// Check if the widget is available on the page.
if (
! document.querySelector( '[data-block-name="woocommerce/mini-cart"]' )
) {
return;
}

return;
// Create a MutationObserver to check when the mini cart drawer is added to the DOM.
const observer = new MutationObserver( ( mutations ) => {
for ( const mutation of mutations ) {
if ( mutation?.addedNodes?.length > 0 ) {
for ( const node of mutation.addedNodes ) {
// Check if the mini cart drawer parent selector was added to the DOM.
if (
node.nodeType === 1 &&
node.matches(
'.wc-block-components-drawer__screen-overlay'
)
) {
// Wait until the button is rendered and add the event listener to it.
waitForSelector(
WooPayDirectCheckout.redirectElements
.BLOCKS_MINI_CART_PROCEED_BUTTON,
addMiniCartEventListener
);
return;
}
}
}
}

WooPayDirectCheckout.redirectToWooPay( [ checkoutButton ], true );
} );
} );

observer.observe( document.body, { childList: true } );
};

/**
* Determines whether the encrypted session data should be prefetched.
Expand Down Expand Up @@ -173,22 +206,51 @@ const removeItemCallback = async ( { product } ) => {
}
};

// Note, although the following hooks are prefixed with 'experimental__', they will be
// graduated to stable in the near future (it'll include the 'experimental__' prefix).
addAction(
'experimental__woocommerce_blocks-cart-add-item',
'wcpay_woopay_direct_checkout',
addItemCallback
);

addAction(
'experimental__woocommerce_blocks-cart-set-item-quantity',
'wcpay_woopay_direct_checkout',
debounceSetItemQtyCallback
);

addAction(
'experimental__woocommerce_blocks-cart-remove-item',
'wcpay_woopay_direct_checkout',
removeItemCallback
);
window.addEventListener( 'load', async () => {
if ( shouldSkipWooPay() ) {
return;
}

WooPayDirectCheckout.init();

isThirdPartyCookieEnabled = await WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled();

// Note, although the following hooks are prefixed with 'experimental__', they will be
// graduated to stable in the near future (it'll include the 'experimental__' prefix).
addAction(
'experimental__woocommerce_blocks-cart-add-item',
'wcpay_woopay_direct_checkout',
addItemCallback
);

addAction(
'experimental__woocommerce_blocks-cart-set-item-quantity',
'wcpay_woopay_direct_checkout',
debounceSetItemQtyCallback
);

addAction(
'experimental__woocommerce_blocks-cart-remove-item',
'wcpay_woopay_direct_checkout',
removeItemCallback
);

// If the mini cart is available, check when it's opened so we can add the event listener to the mini cart's checkout button.
maybeObserveMiniCart();

const checkoutButtons = WooPayDirectCheckout.getCheckoutButtonElements();
handleWooPayDirectCheckout( checkoutButtons );
} );

jQuery( ( $ ) => {
$( document.body ).on( 'updated_cart_totals', async () => {
if ( shouldSkipWooPay() ) {
return;
}

// When "updated_cart_totals" is triggered, the classic 'Proceed to Checkout' button is
// re-rendered. So, the click-event listener needs to be re-attached to the new button.
const checkoutButton = WooPayDirectCheckout.getClassicProceedToCheckoutButton();
handleWooPayDirectCheckout( [ checkoutButton ] );
} );
} );
93 changes: 21 additions & 72 deletions client/checkout/woopay/direct-checkout/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ jest.mock( '@wordpress/hooks', () => ( {
jest.mock(
'wcpay/checkout/woopay/direct-checkout/woopay-direct-checkout',
() => ( {
isWooPayDirectCheckoutEnabled: jest.fn(),
init: jest.fn(),
isWooPayThirdPartyCookiesEnabled: jest.fn(),
getCheckoutRedirectElements: jest.fn(),
getCheckoutButtonElements: jest.fn(),
isUserLoggedIn: jest.fn(),
maybePrefetchEncryptedSessionData: jest.fn(),
getClassicProceedToCheckoutButton: jest.fn(),
redirectToWooPay: jest.fn(),
addRedirectToWooPayEventListener: jest.fn(),
setEncryptedSessionDataAsNotPrefetched: jest.fn(),
} )
);
Expand All @@ -53,30 +52,12 @@ describe( 'WooPay direct checkout window "load" event listener', () => {
jest.clearAllMocks();
} );

it( 'does not initialize WooPay direct checkout if not enabled', async () => {
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue(
false
);

fireEvent.load( window );

await new Promise( ( resolve ) => setImmediate( resolve ) );

expect(
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled
).toHaveBeenCalled();
expect( WooPayDirectCheckout.init ).not.toHaveBeenCalled();
} );

it( 'calls `redirectToWooPay` method if third-party cookies are enabled and user is logged-in', async () => {
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue(
true
);
it( 'calls `addRedirectToWooPayEventListener` method if third-party cookies are enabled and user is logged-in', async () => {
WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue(
true
);
WooPayDirectCheckout.isUserLoggedIn.mockResolvedValue( true );
WooPayDirectCheckout.getCheckoutRedirectElements.mockReturnValue( [] );
WooPayDirectCheckout.getCheckoutButtonElements.mockReturnValue( [] );

fireEvent.load( window );

Expand All @@ -90,20 +71,16 @@ describe( 'WooPay direct checkout window "load" event listener', () => {
expect(
WooPayDirectCheckout.maybePrefetchEncryptedSessionData
).toHaveBeenCalled();
expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith(
expect.any( Array ),
true
);
expect(
WooPayDirectCheckout.addRedirectToWooPayEventListener
).toHaveBeenCalledWith( expect.any( Array ), true );
} );

it( 'calls `redirectToWooPay` method with "checkout_redirect" if third-party cookies are disabled', async () => {
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue(
true
);
it( 'calls `addRedirectToWooPayEventListener` method with "checkout_redirect" if third-party cookies are disabled', async () => {
WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue(
false
);
WooPayDirectCheckout.getCheckoutRedirectElements.mockReturnValue( [] );
WooPayDirectCheckout.getCheckoutButtonElements.mockReturnValue( [] );

fireEvent.load( window );

Expand All @@ -117,10 +94,9 @@ describe( 'WooPay direct checkout window "load" event listener', () => {
expect(
WooPayDirectCheckout.maybePrefetchEncryptedSessionData
).not.toHaveBeenCalled();
expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith(
expect.any( Array ),
false
);
expect(
WooPayDirectCheckout.addRedirectToWooPayEventListener
).toHaveBeenCalledWith( expect.any( Array ), false );
} );
} );

Expand All @@ -129,34 +105,12 @@ describe( 'WooPay direct checkout "updated_cart_totals" jQuery event listener',
jest.clearAllMocks();
} );

it( 'should not proceed if direct checkout is not enabled', async () => {
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue(
false
);

fireEvent.load( window );

await new Promise( ( resolve ) => setImmediate( resolve ) );

$( document.body ).trigger( 'updated_cart_totals' );

expect(
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled
).toHaveBeenCalled();
expect(
WooPayDirectCheckout.getClassicProceedToCheckoutButton
).not.toHaveBeenCalled();
} );

it( 'calls `redirectToWooPay` method if third-party cookies are enabled and user is logged-in', async () => {
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue(
true
);
it( 'calls `addRedirectToWooPayEventListener` method if third-party cookies are enabled and user is logged-in', async () => {
WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue(
true
);
WooPayDirectCheckout.isUserLoggedIn.mockResolvedValue( true );
WooPayDirectCheckout.getCheckoutRedirectElements.mockReturnValue( [] );
WooPayDirectCheckout.getCheckoutButtonElements.mockReturnValue( [] );

fireEvent.load( window );

Expand All @@ -172,16 +126,12 @@ describe( 'WooPay direct checkout "updated_cart_totals" jQuery event listener',
expect(
WooPayDirectCheckout.maybePrefetchEncryptedSessionData
).toHaveBeenCalled();
expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith(
expect.any( Array ),
true
);
expect(
WooPayDirectCheckout.addRedirectToWooPayEventListener
).toHaveBeenCalledWith( expect.any( Array ), true );
} );

it( 'calls `redirectToWooPay` method with "checkout_redirect" if third-party cookies are disabled', async () => {
WooPayDirectCheckout.isWooPayDirectCheckoutEnabled.mockReturnValue(
true
);
it( 'calls `addRedirectToWooPayEventListener` method with "checkout_redirect" if third-party cookies are disabled', async () => {
WooPayDirectCheckout.isWooPayThirdPartyCookiesEnabled.mockResolvedValue(
false
);
Expand All @@ -203,10 +153,9 @@ describe( 'WooPay direct checkout "updated_cart_totals" jQuery event listener',
expect(
WooPayDirectCheckout.maybePrefetchEncryptedSessionData
).not.toHaveBeenCalled();
expect( WooPayDirectCheckout.redirectToWooPay ).toHaveBeenCalledWith(
expect.any( Array ),
false
);
expect(
WooPayDirectCheckout.addRedirectToWooPayEventListener
).toHaveBeenCalledWith( expect.any( Array ), false );
} );
} );

Expand Down
Loading

0 comments on commit f78e96c

Please sign in to comment.