diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/event-emit.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/event-emit.js index aed614c09a3..65e2d502840 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/event-emit.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/event-emit.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { actions, reducer, emitEvent } from '../event_emit'; +import { actions, reducer, emitEvent, emitEventWithAbort } from '../event-emit'; const EMIT_TYPES = { CHECKOUT_COMPLETE_WITH_SUCCESS: 'checkout_complete', @@ -70,4 +70,10 @@ const emitterSubscribers = ( dispatcher ) => ( { }, } ); -export { EMIT_TYPES, emitterSubscribers, reducer, emitEvent }; +export { + EMIT_TYPES, + emitterSubscribers, + reducer, + emitEvent, + emitEventWithAbort, +}; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/index.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/index.js index 5ef5763e772..3fe62256f8e 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/index.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/checkout/index.js @@ -10,6 +10,7 @@ import { EMIT_TYPES, emitterSubscribers, emitEvent, + emitEventWithAbort, reducer as emitReducer, } from './event-emit'; @@ -124,23 +125,18 @@ export const CheckoutProvider = ( { useEffect( () => { const status = checkoutState.status; if ( status === STATUS.PROCESSING ) { - const error = emitEvent( + emitEventWithAbort( currentObservers.current, EMIT_TYPES.CHECKOUT_PROCESSING, {} - ); - //@todo bail if error object detected (see flow). - //Fire off checkoutFail event, and then reset checkout - //status to idle (with hasError flag) - then return from this hook. - // Finally after the event subscribers have processed, do the - // checkout submit sending the order to the server for processing - // and followup on errors from it. - // I'll need to setup a special emitter that aborts on first fail - // instead of hitting all subscribed event observers. - if ( error ) { - dispatchActions.setHasError(); - } - dispatch( actions.setComplete() ); + ).then( ( response ) => { + if ( response !== true ) { + // @todo handle any validation error property values in the + // response + dispatchActions.setHasError(); + } + dispatch( actions.setComplete() ); + } ); } if ( checkoutState.isComplete ) { if ( checkoutState.hasError ) { @@ -154,14 +150,21 @@ export const CheckoutProvider = ( { currentObservers.current, EMIT_TYPES.CHECKOUT_COMPLETE_WITH_SUCCESS, {} - ); - } - // all observers have done their thing so let's redirect (if no error) - if ( ! checkoutState.hasError ) { - window.location = checkoutState.redirectUrl; + ).then( () => { + // all observers have done their thing so let's redirect + // (if no error) + if ( ! checkoutState.hasError ) { + window.location = checkoutState.redirectUrl; + } + } ); } } - }, [ checkoutState.status, checkoutState.hasError ] ); + }, [ + checkoutState.status, + checkoutState.hasError, + checkoutState.isComplete, + checkoutState.redirectUrl, + ] ); const onSubmit = () => { dispatch( actions.setProcessing() ); diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/emitters.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/emitters.js new file mode 100644 index 00000000000..9c74023cf47 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/emitters.js @@ -0,0 +1,63 @@ +/** + * Emits events on registered observers for the provided type and passes along + * the provided data. + * + * This event emitter will silently catch promise errors, but doesn't care + * otherwise if any errors are caused by observers. So events that do care + * should use `emitEventWithAbort` instead. + * + * @param {Object} observers The registered observers to omit to. + * @param {string} eventType The event type being emitted. + * @param {*} data Data passed along to the observer when it is + * invoked. + * + * @return {Promise} A promise that resolves to true after all observers have + * executed. + */ +export const emitEvent = async ( observers, eventType, data ) => { + const observersByType = observers[ eventType ] || []; + for ( let i = 0; i < observersByType.length; i++ ) { + try { + await Promise.resolve( observersByType[ i ]( data ) ); + } catch ( e ) { + // we don't care about errors blocking execution, but will + // console.error for troubleshooting. + // eslint-disable-next-line no-console + console.error( e ); + } + } + return true; +}; + +/** + * Emits events on registered observers for the provided type and passes along + * the provided data. This event emitter will abort and return any value from + * observers that do not return true. + * + * @param {Object} observers The registered observers to omit to. + * @param {string} eventType The event type being emitted. + * @param {*} data Data passed along to the observer when it is + * invoked. + * + * @return {Promise} Returns a promise that resolves to either boolean or the + * return value of the aborted observer. + */ +export const emitEventWithAbort = async ( observers, eventType, data ) => { + const observersByType = observers[ eventType ] || []; + for ( let i = 0; i < observersByType.length; i++ ) { + try { + const response = await Promise.resolve( + observersByType[ i ]( data ) + ); + if ( response !== true ) { + return response; + } + } catch ( e ) { + // we don't handle thrown errors but just console.log for + // troubleshooting + // eslint-disable-next-line no-console + console.error( e ); + } + } + return true; +}; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/index.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/index.js new file mode 100644 index 00000000000..8e7c304fd6c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/index.js @@ -0,0 +1,2 @@ +export * from './reducer'; +export * from './emitters'; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event_emit/reducer.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/reducer.js similarity index 60% rename from plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event_emit/reducer.js rename to plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/reducer.js index f318bb74f6f..df03e26801a 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event_emit/reducer.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/reducer.js @@ -26,26 +26,6 @@ export const actions = { }, }; -/** - * Emits events on registered observers for the provided type and passes along - * the provided data. - * - * @param {Object} observers The registered observers to omit to. - * @param {string} eventType The event type being emitted. - * @param {*} data Data passed along to the observer when it is - * invoked. - */ -export const emitEvent = ( observers, eventType, data ) => { - const observersByType = observers[ eventType ] || []; - let hasError = false; - observersByType.forEach( ( observer ) => { - if ( typeof observer === 'function' ) { - hasError = !! observer( data, hasError ); - } - } ); - return hasError; -}; - /** * Handles actions for emmitters * diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/test/emitters.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/test/emitters.js new file mode 100644 index 00000000000..66c1f19c131 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event-emit/test/emitters.js @@ -0,0 +1,49 @@ +/** + * Internal dependencies + */ +import { emitEvent, emitEventWithAbort } from '../emitters'; + +describe( 'Testing emitters', () => { + let observerMocks = {}; + beforeEach( () => { + observerMocks = { + observerA: jest.fn().mockReturnValue( true ), + observerB: jest.fn().mockReturnValue( true ), + observerReturnValue: jest.fn().mockReturnValue( 10 ), + observerPromiseWithReject: jest + .fn() + .mockRejectedValue( 'an error' ), + observerPromiseWithResolvedValue: jest.fn().mockResolvedValue( 10 ), + }; + } ); + describe( 'Testing emitEvent()', () => { + it( 'invokes all observers', async () => { + const observers = { test: Object.values( observerMocks ) }; + const response = await emitEvent( observers, 'test', 'foo' ); + expect( console ).toHaveErroredWith( 'an error' ); + expect( observerMocks.observerA ).toHaveBeenCalledTimes( 1 ); + expect( observerMocks.observerB ).toHaveBeenCalledWith( 'foo' ); + expect( response ).toBe( true ); + } ); + } ); + describe( 'Testing emitEventWithAbort()', () => { + it( + 'aborts on non truthy value and does not invoke remaining ' + + 'observers', + async () => { + const observers = { test: Object.values( observerMocks ) }; + const response = await emitEventWithAbort( + observers, + 'test', + 'foo' + ); + expect( console ).not.toHaveErrored(); + expect( observerMocks.observerB ).toHaveBeenCalledTimes( 1 ); + expect( + observerMocks.observerPromiseWithResolvedValue + ).not.toHaveBeenCalled(); + expect( response ).toBe( 10 ); + } + ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event_emit/index.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event_emit/index.js deleted file mode 100644 index 38f5723ee1a..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/event_emit/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './reducer'; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/shipping/event-emit.js b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/shipping/event-emit.js index e65c1c1143d..6f21d965b4c 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/shipping/event-emit.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/cart-checkout/shipping/event-emit.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { actions, reducer, emitEvent } from '../event_emit'; +import { actions, reducer, emitEvent } from '../event-emit'; const EMIT_TYPES = { SHIPPING_RATES_SUCCESS: 'shipping_rates_success',