Add `CartEventContext` and dispatch events when pressing proceed to checkout button (https://github.com/woocommerce/woocommerce-blocks/pull/7809)
* Add CartEventsContext with onProceedToCheckout event * Wrap Cart in CartEventsProvider * Dispatch onProceedToCheckout event when button is pressed * Update type of children on CartEventsProvider * Add test for ProceedToCheckout block * Add tests for CartEventProvider * Remove superfluous div * Fix incorrect nesting after rebase * Wrap mini cart in CartEventsProvider * Dispatch onProceedToCheckout event when clicking button in mini cart * Add tests for mini cart onProceedToCheckout emitter * Make observer fail so navigation isn't attempted * Prevent console error on navigation * Try preventing navigation in unit tests * Try preventing navigation in unit tests * Try preventing navigation in unit tests * Try preventing navigation in unit tests * Try preventing navigation in unit tests * Try preventing navigation in unit tests * Try preventing navigation in unit tests
This commit is contained in:
parent
bc6dc106ab
commit
3ebcd7f601
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
emitterCallback,
|
||||
reducer,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
ActionType,
|
||||
} from '../../../event-emit';
|
||||
|
||||
// These events are emitted when the Cart status is BEFORE_PROCESSING and AFTER_PROCESSING
|
||||
// to enable third parties to hook into the cart process
|
||||
const EVENTS = {
|
||||
PROCEED_TO_CHECKOUT: 'cart_proceed_to_checkout',
|
||||
};
|
||||
|
||||
type EventEmittersType = Record< string, ReturnType< typeof emitterCallback > >;
|
||||
|
||||
/**
|
||||
* Receives a reducer dispatcher and returns an object with the
|
||||
* various event emitters for the payment processing events.
|
||||
*
|
||||
* Calling the event registration function with the callback will register it
|
||||
* for the event emitter and will return a dispatcher for removing the
|
||||
* registered callback (useful for implementation in `useEffect`).
|
||||
*
|
||||
* @param {Function} observerDispatch The emitter reducer dispatcher.
|
||||
* @return {Object} An object with the various payment event emitter registration functions
|
||||
*/
|
||||
const useEventEmitters = (
|
||||
observerDispatch: React.Dispatch< ActionType >
|
||||
): EventEmittersType => {
|
||||
const eventEmitters = useMemo(
|
||||
() => ( {
|
||||
onProceedToCheckout: emitterCallback(
|
||||
EVENTS.PROCEED_TO_CHECKOUT,
|
||||
observerDispatch
|
||||
),
|
||||
} ),
|
||||
[ observerDispatch ]
|
||||
);
|
||||
return eventEmitters;
|
||||
};
|
||||
|
||||
export { EVENTS, useEventEmitters, reducer, emitEvent, emitEventWithAbort };
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
useEventEmitters,
|
||||
reducer as emitReducer,
|
||||
emitEventWithAbort,
|
||||
EVENTS,
|
||||
} from './event-emit';
|
||||
import type { emitterCallback } from '../../../event-emit';
|
||||
|
||||
type CartEventsContextType = {
|
||||
// Used to register a callback that will fire when the cart has been processed and has an error.
|
||||
onProceedToCheckout: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback that will fire when the cart has been processed and has an error.
|
||||
dispatchOnProceedToCheckout: () => Promise< unknown[] >;
|
||||
};
|
||||
|
||||
const CartEventsContext = createContext< CartEventsContextType >( {
|
||||
onProceedToCheckout: () => () => void null,
|
||||
dispatchOnProceedToCheckout: () => new Promise( () => void null ),
|
||||
} );
|
||||
|
||||
export const useCartEventsContext = () => {
|
||||
return useContext( CartEventsContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkout Events provider
|
||||
* Emit Checkout events and provide access to Checkout event handlers
|
||||
*
|
||||
* @param {Object} props Incoming props for the provider.
|
||||
* @param {Object} props.children The children being wrapped.
|
||||
*/
|
||||
export const CartEventsProvider = ( {
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
} ): JSX.Element => {
|
||||
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
|
||||
const currentObservers = useRef( observers );
|
||||
const { onProceedToCheckout } = useEventEmitters( observerDispatch );
|
||||
|
||||
// set observers on ref so it's always current.
|
||||
useEffect( () => {
|
||||
currentObservers.current = observers;
|
||||
}, [ observers ] );
|
||||
|
||||
const dispatchOnProceedToCheckout = async () => {
|
||||
return await emitEventWithAbort(
|
||||
currentObservers.current,
|
||||
EVENTS.PROCEED_TO_CHECKOUT,
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const cartEvents = {
|
||||
onProceedToCheckout,
|
||||
dispatchOnProceedToCheckout,
|
||||
};
|
||||
return (
|
||||
<CartEventsContext.Provider value={ cartEvents }>
|
||||
{ children }
|
||||
</CartEventsContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useCartEventsContext } from '@woocommerce/base-context';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CartEventsProvider } from '../index';
|
||||
import Block from '../../../../../../blocks/cart/inner-blocks/proceed-to-checkout-block/block';
|
||||
|
||||
describe( 'CartEventsProvider', () => {
|
||||
it( 'allows observers to unsubscribe', async () => {
|
||||
const mockObserver = jest.fn().mockReturnValue( { type: 'error' } );
|
||||
const MockObserverComponent = () => {
|
||||
const { onProceedToCheckout } = useCartEventsContext();
|
||||
useEffect( () => {
|
||||
const unsubscribe = onProceedToCheckout( () => {
|
||||
mockObserver();
|
||||
unsubscribe();
|
||||
} );
|
||||
}, [ onProceedToCheckout ] );
|
||||
return <div>Mock observer</div>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CartEventsProvider>
|
||||
<div>
|
||||
<MockObserverComponent />
|
||||
<Block checkoutPageId={ 0 } className="test-block" />
|
||||
</div>
|
||||
</CartEventsProvider>
|
||||
);
|
||||
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
|
||||
const button = screen.getByText( 'Proceed to Checkout' );
|
||||
|
||||
// Forcibly set the button URL to # to prevent JSDOM error: `["Error: Not implemented: navigation (except hash changes)`
|
||||
button.parentElement?.removeAttribute( 'href' );
|
||||
|
||||
// Click twice. The observer should unsubscribe after the first click.
|
||||
button.click();
|
||||
button.click();
|
||||
await waitFor( () => {
|
||||
expect( mockObserver ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -1,6 +1,7 @@
|
|||
export * from './payment-events';
|
||||
export * from './shipping';
|
||||
export * from './checkout-events';
|
||||
export * from './cart-events';
|
||||
export * from './cart';
|
||||
export * from './checkout-processor';
|
||||
export * from './checkout-provider';
|
||||
|
|
|
@ -5,11 +5,15 @@ import { __ } from '@wordpress/i18n';
|
|||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { CartProvider, noticeContexts } from '@woocommerce/base-context';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import { translateJQueryEventToNative } from '@woocommerce/base-utils';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
import {
|
||||
CartEventsProvider,
|
||||
CartProvider,
|
||||
noticeContexts,
|
||||
} from '@woocommerce/base-context';
|
||||
import {
|
||||
SlotFillProvider,
|
||||
StoreNoticesContainer,
|
||||
|
@ -85,8 +89,10 @@ const Block = ( { attributes, children, scrollToTop } ) => (
|
|||
<StoreNoticesContainer context={ noticeContexts.CART } />
|
||||
<SlotFillProvider>
|
||||
<CartProvider>
|
||||
<Cart attributes={ attributes }>{ children }</Cart>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
<CartEventsProvider>
|
||||
<Cart attributes={ attributes }>{ children }</Cart>
|
||||
<ScrollOnError scrollToTop={ scrollToTop } />
|
||||
</CartEventsProvider>
|
||||
</CartProvider>
|
||||
</SlotFillProvider>
|
||||
</BlockErrorBoundary>
|
||||
|
|
|
@ -10,6 +10,8 @@ import { getSetting } from '@woocommerce/settings';
|
|||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';
|
||||
import { isErrorResponse } from '@woocommerce/base-context';
|
||||
import { useCartEventsContext } from '@woocommerce/base-context/providers';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -74,12 +76,22 @@ const Block = ( {
|
|||
arg: { cart },
|
||||
} );
|
||||
|
||||
const { dispatchOnProceedToCheckout } = useCartEventsContext();
|
||||
|
||||
const submitContainerContents = (
|
||||
<Button
|
||||
className="wc-block-cart__submit-button"
|
||||
href={ filteredLink }
|
||||
disabled={ isCalculating }
|
||||
onClick={ () => setShowSpinner( true ) }
|
||||
onClick={ ( e ) => {
|
||||
dispatchOnProceedToCheckout().then( ( observerResponses ) => {
|
||||
if ( observerResponses.some( isErrorResponse ) ) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
setShowSpinner( true );
|
||||
} );
|
||||
} }
|
||||
showSpinner={ showSpinner }
|
||||
>
|
||||
{ label }
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { registerCheckoutFilters } from '@woocommerce/blocks-checkout';
|
||||
import { useCartEventsContext } from '@woocommerce/base-context';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from '../block';
|
||||
import { CartEventsProvider } from '../../../../../base/context/providers';
|
||||
|
||||
describe( 'Proceed to checkout block', () => {
|
||||
it( 'allows the text to be filtered', () => {
|
||||
|
@ -49,4 +53,37 @@ describe( 'Proceed to checkout block', () => {
|
|||
//@todo When https://github.com/WordPress/gutenberg/issues/22850 is complete use that new matcher here for more specific error message assertion.
|
||||
expect( console ).toHaveErrored();
|
||||
} );
|
||||
it( 'dispatches the onProceedToCheckout event when the button is clicked', async () => {
|
||||
const mockObserver = jest.fn().mockReturnValue( { type: 'error' } );
|
||||
const MockObserverComponent = () => {
|
||||
const { onProceedToCheckout } = useCartEventsContext();
|
||||
useEffect( () => {
|
||||
return onProceedToCheckout( mockObserver );
|
||||
}, [ onProceedToCheckout ] );
|
||||
return <div>Mock observer</div>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CartEventsProvider>
|
||||
<div>
|
||||
<MockObserverComponent />
|
||||
<Block
|
||||
buttonLabel={ 'Proceed to Checkout' }
|
||||
checkoutPageId={ 0 }
|
||||
className="test-block"
|
||||
/>
|
||||
</div>
|
||||
</CartEventsProvider>
|
||||
);
|
||||
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
|
||||
const button = screen.getByText( 'Proceed to Checkout' );
|
||||
|
||||
// Forcibly set the button URL to # to prevent JSDOM error: `["Error: Not implemented: navigation (except hash changes)`
|
||||
button.parentElement?.removeAttribute( 'href' );
|
||||
|
||||
button.click();
|
||||
await waitFor( () => {
|
||||
expect( mockObserver ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { DrawerCloseButton } from '@woocommerce/base-components/drawer';
|
||||
import { CartEventsProvider } from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -20,8 +21,10 @@ export const MiniCartContentsBlock = (
|
|||
|
||||
return (
|
||||
<>
|
||||
<DrawerCloseButton />
|
||||
{ children }
|
||||
<CartEventsProvider>
|
||||
<DrawerCloseButton />
|
||||
{ children }
|
||||
</CartEventsProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,10 @@ import { CHECKOUT_URL } from '@woocommerce/block-settings';
|
|||
import Button from '@woocommerce/base-components/button';
|
||||
import classNames from 'classnames';
|
||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import {
|
||||
isErrorResponse,
|
||||
useCartEventsContext,
|
||||
} from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -24,6 +28,7 @@ const Block = ( {
|
|||
style,
|
||||
}: MiniCartCheckoutButtonBlockProps ): JSX.Element | null => {
|
||||
const styleProps = useStyleProps( { style } );
|
||||
const { dispatchOnProceedToCheckout } = useCartEventsContext();
|
||||
|
||||
if ( ! CHECKOUT_URL ) {
|
||||
return null;
|
||||
|
@ -39,6 +44,13 @@ const Block = ( {
|
|||
variant={ getVariant( className, 'contained' ) }
|
||||
style={ styleProps.style }
|
||||
href={ CHECKOUT_URL }
|
||||
onClick={ ( e ) => {
|
||||
dispatchOnProceedToCheckout().then( ( observerResponses ) => {
|
||||
if ( observerResponses.some( isErrorResponse ) ) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ checkoutButtonLabel || defaultCheckoutButtonLabel }
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
CartEventsProvider,
|
||||
useCartEventsContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from '../block';
|
||||
|
||||
describe( 'Mini Cart Checkout Button Block', () => {
|
||||
it( 'dispatches the onProceedToCheckout event when the button is clicked', async () => {
|
||||
const mockObserver = jest.fn().mockReturnValue( { type: 'error' } );
|
||||
const MockObserverComponent = () => {
|
||||
const { onProceedToCheckout } = useCartEventsContext();
|
||||
useEffect( () => {
|
||||
return onProceedToCheckout( mockObserver );
|
||||
}, [ onProceedToCheckout ] );
|
||||
return <div>Mock observer</div>;
|
||||
};
|
||||
|
||||
render(
|
||||
<CartEventsProvider>
|
||||
<div>
|
||||
<MockObserverComponent />
|
||||
<Block
|
||||
checkoutButtonLabel={ 'Proceed to Checkout' }
|
||||
className="test-block"
|
||||
/>
|
||||
</div>
|
||||
</CartEventsProvider>
|
||||
);
|
||||
expect( screen.getByText( 'Mock observer' ) ).toBeInTheDocument();
|
||||
const button = screen.getByText( 'Proceed to Checkout' );
|
||||
|
||||
// Forcibly set the button URL to # to prevent JSDOM error: `["Error: Not implemented: navigation (except hash changes)`
|
||||
button.parentElement?.removeAttribute( 'href' );
|
||||
button.click();
|
||||
await waitFor( () => {
|
||||
expect( mockObserver ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -86,7 +86,7 @@ global.wcSettings = {
|
|||
checkout: {
|
||||
id: 0,
|
||||
title: '',
|
||||
permalink: '',
|
||||
permalink: 'https://local/checkout/',
|
||||
},
|
||||
privacy: {
|
||||
id: 0,
|
||||
|
|
Loading…
Reference in New Issue