Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the WooPay Direct Checkout flow to the blocks mini cart widget #8795

Merged
merged 19 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() ||
lovo-h marked this conversation as resolved.
Show resolved Hide resolved
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
lovo-h marked this conversation as resolved.
Show resolved Hide resolved
);
return;
}
}
}
}

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

observer.observe( document.body, { childList: true } );
ricardo marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* 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
Loading