From 216e261c1b1f68502ecaa1d44d9d84c5d3f5df7b Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Thu, 27 Feb 2020 04:11:48 -0500 Subject: [PATCH] prelminary work on the checkout context --- .../js/base/context/cart-checkout/checkout.js | 96 ------------- .../context/cart-checkout/checkout/actions.js | 43 ++++++ .../cart-checkout/checkout/constants.js | 17 +++ .../cart-checkout/checkout/event-emit.js | 41 ++++++ .../context/cart-checkout/checkout/index.js | 136 ++++++++++++++++++ .../context/cart-checkout/checkout/reducer.js | 63 ++++++++ .../js/base/context/cart-checkout/errors.js | 2 + .../shipping/shipping-data-context.js | 7 +- 8 files changed, 308 insertions(+), 97 deletions(-) delete mode 100644 assets/js/base/context/cart-checkout/checkout.js create mode 100644 assets/js/base/context/cart-checkout/checkout/actions.js create mode 100644 assets/js/base/context/cart-checkout/checkout/constants.js create mode 100644 assets/js/base/context/cart-checkout/checkout/event-emit.js create mode 100644 assets/js/base/context/cart-checkout/checkout/index.js create mode 100644 assets/js/base/context/cart-checkout/checkout/reducer.js diff --git a/assets/js/base/context/cart-checkout/checkout.js b/assets/js/base/context/cart-checkout/checkout.js deleted file mode 100644 index dc0901302a9..00000000000 --- a/assets/js/base/context/cart-checkout/checkout.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * External dependencies - */ -import { - createContext, - useContext, - useState, - useMemo, -} from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { PaymentMethodDataProvider } from './payment-methods'; -import { ShippingMethodDataProvider } from './shipping'; - -const CheckoutContext = createContext( {} ); - -export const useCheckoutContext = () => { - return useContext( CheckoutContext ); -}; - -// create context provider for shipping - -// create context provider for paymentevents - -// create context provider for coupons - -// create context provider for checkout validation errors. - -// checkout and cart provider context will be different context but implement -// the above (in terms of finalization.). For example cart context will redirect -// checkout for finalization. Checkout context handles pinging the server for -// processing checkout, validation of existing fields, and redirecting on -// success. - -// checkout context should have events (calculating, success, fail, pristine) -// Also, extensions should be able to register callbacks that will be fired as -// each event is triggered. So there should be a way to register and remove -// callbacks. -// for setCalculating dispatches... let's do this as an incremental thing, -// anything that triggers total changes will increment the calculator by one and when its -// done it triggers a decrement. That way `isCalculating` is actually a report -// on all things currently in processing. - -const CheckoutProvider = ( { - children, - activePaymentMethod: initialActivePaymentMethod, - placeOrderLabel = __( 'Place Order', 'woo-gutenberg-product-block' ), -} ) => { - const [ successRedirectUrl, setSuccessRedirectUrl ] = useState( '' ); - const [ failureRedirectUrl, setFailureRedirectUrl ] = useState( '' ); - const [ isCheckoutComplete, setIsCheckoutComplete ] = useState( false ); - const [ checkoutHasError, setCheckoutHasError ] = useState( false ); - const [ notices, updateNotices ] = useState( [] ); - const [ isCalculating, setIsCalculating ] = useState( false ); - const contextValue = useMemo( () => { - return { - successRedirectUrl, - setSuccessRedirectUrl, - failureRedirectUrl, - setFailureRedirectUrl, - isCheckoutComplete, - setIsCheckoutComplete, - checkoutHasError, - setCheckoutHasError, - isCalculating, - setIsCalculating, - notices, - updateNotices, - placeOrderLabel, - }; - }, [ - successRedirectUrl, - failureRedirectUrl, - isCheckoutComplete, - isCalculating, - checkoutHasError, - placeOrderLabel, - notices, - ] ); - return ( - - - - { children } - - - - ); -}; - -export default CheckoutProvider; diff --git a/assets/js/base/context/cart-checkout/checkout/actions.js b/assets/js/base/context/cart-checkout/checkout/actions.js new file mode 100644 index 00000000000..c10af3faeb5 --- /dev/null +++ b/assets/js/base/context/cart-checkout/checkout/actions.js @@ -0,0 +1,43 @@ +/** + * Internal dependencies + */ +import { TYPES } from './constants'; + +const { + SET_PRISTINE, + SET_REDIRECT_URL, + SET_COMPLETE, + SET_INCOMPLETE, + SET_HAS_ERROR, + SET_NO_ERROR, + INCREMENT_CALCULATING, + DECREMENT_CALCULATING, +} = TYPES; + +export const actions = { + setPristine: () => ( { + type: SET_PRISTINE, + } ), + setRedirectUrl: ( url ) => ( { + type: SET_REDIRECT_URL, + url, + } ), + setComplete: () => ( { + type: SET_COMPLETE, + } ), + setIncomplete: () => ( { + type: SET_INCOMPLETE, + } ), + setHasError: () => ( { + type: SET_HAS_ERROR, + } ), + clearError: () => ( { + type: SET_NO_ERROR, + } ), + incrementCalculating: () => ( { + type: INCREMENT_CALCULATING, + } ), + decrementCalculating: () => ( { + type: DECREMENT_CALCULATING, + } ), +}; diff --git a/assets/js/base/context/cart-checkout/checkout/constants.js b/assets/js/base/context/cart-checkout/checkout/constants.js new file mode 100644 index 00000000000..e801cfdcc0f --- /dev/null +++ b/assets/js/base/context/cart-checkout/checkout/constants.js @@ -0,0 +1,17 @@ +export const DEFAULT_STATE = { + redirectUrl: '', + isComplete: false, + hasError: false, + calculatingCount: 0, +}; + +export const TYPES = { + SET_PRISTINE: 'set_pristine', + SET_REDIRECT_URL: 'set_redirect_url', + SET_COMPLETE: 'set_checkout_complete', + SET_INCOMPLETE: 'set_checkout_incomplete', + SET_HAS_ERROR: 'set_checkout_has_error', + SET_NO_ERROR: 'set_checkout_no_error', + INCREMENT_CALCULATING: 'increment_calculating', + DECREMENT_CALCULATING: 'decrement_calculating', +}; diff --git a/assets/js/base/context/cart-checkout/checkout/event-emit.js b/assets/js/base/context/cart-checkout/checkout/event-emit.js new file mode 100644 index 00000000000..9bdef861798 --- /dev/null +++ b/assets/js/base/context/cart-checkout/checkout/event-emit.js @@ -0,0 +1,41 @@ +/** + * Internal dependencies + */ +import { actions, reducer, emitEvent } from '../event_emit'; + +const EMIT_TYPES = { + CHECKOUT_COMPLETE: 'checkout_complete', +}; + +/** + * Receives a reducer dispatcher and returns an object with the + * onCheckoutComplete callback registration function for the checkout emit + * 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} dispatcher The emitter reducer dispatcher. + * + * @return {Object} An object with the `onCheckoutComplete` emmitter registration + */ +const emitterSubscribers = ( dispatcher ) => ( { + onCheckoutComplete: ( callback ) => { + const action = actions.addEventCallback( + EMIT_TYPES.CHECKOUT_COMPLETE, + callback + ); + dispatcher( action ); + return () => { + dispatcher( + actions.removeEventCallback( + EMIT_TYPES.CHECKOUT_COMPLETE, + action.id + ) + ); + }; + }, +} ); + +export { EMIT_TYPES, emitterSubscribers, reducer, emitEvent }; diff --git a/assets/js/base/context/cart-checkout/checkout/index.js b/assets/js/base/context/cart-checkout/checkout/index.js new file mode 100644 index 00000000000..8a9cd1f1f0d --- /dev/null +++ b/assets/js/base/context/cart-checkout/checkout/index.js @@ -0,0 +1,136 @@ +/** + * Internal dependencies + */ +import { PaymentMethodDataProvider } from '../payment-methods'; +import { ShippingMethodDataProvider } from '../shipping'; +import { actions } from './actions'; +import { reducer } from './reducer'; +import { TYPES, DEFAULT_STATE } from './constants'; +import { + EMIT_TYPES, + emitterSubscribers, + emitEvent, + reducer as emitReducer, +} from './event-emit'; + +/** + * External dependencies + */ +import { + createContext, + useContext, + useReducer, + useRef, + useMemo, + useEffect, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const CheckoutContext = createContext( {} ); + +export const useCheckoutContext = () => { + return useContext( CheckoutContext ); +}; + +// create context provider for coupons + +// create context provider for checkout validation errors. + +// checkout and cart provider context will be different context but implement +// the above (in terms of finalization.). For example cart context will redirect +// checkout for finalization. Checkout context handles pinging the server for +// processing checkout, validation of existing fields, and redirecting on +// success. + +// checkout context should have events (calculating, success, fail, pristine) +// Also, extensions should be able to register callbacks that will be fired as +// each event is triggered. So there should be a way to register and remove +// callbacks. +// for setCalculating dispatches... let's do this as an incremental thing, +// anything that triggers total changes will increment the calculator by one and when its +// done it triggers a decrement. That way `isCalculating` is actually a report +// on all things currently in processing. + +// todo add onClick handler for checkout submit button and this will: +// - run validation (via validation context) +// - if active payment method is present, trigger the payment method processing. +// - when payment payment status is set to success, trigger the server request +// for finalizing the checkout (which should include all the payment data) +// - on receiving the server response, trigger checkout complete. +// - do redirect to redirectUrl if checkoutComplete and paymentMethodStatus is +// is complete (to allow for final payment method processing after checkout +// is complete) (NOTE: maybe we could just handle this by detecting if there are +// any observers left to process?) +// One way I might be able to do all the above is to pass through the onSubmit +// handler that is enhanced through each provider (maybe). Otherwise I'm going +// to have to figure out a way to connect the various providers. + +const CheckoutProvider = ( { + children, + activePaymentMethod: initialActivePaymentMethod, + redirectUrl, + submitLabel = __( 'Place Order', 'woo-gutenberg-product-block' ), +} ) => { + // note, this is done intentionally so that the default state now has + // the redirectUrl for when checkout is reset to PRISTINE state. + DEFAULT_STATE.redirectUrl = redirectUrl; + const [ checkoutState, dispatch ] = useReducer( reducer, DEFAULT_STATE ); + const [ observers, subscriber ] = useReducer( emitReducer ); + const currentObservers = useRef( observers ); + // set observers on ref so it's always current + useEffect( () => { + currentObservers.current = observers; + }, [ observers ] ); + const onCheckoutComplete = emitterSubscribers( subscriber ) + .onCheckoutComplete; + const dispatchActions = useMemo( + () => ( { + resetCheckout: () => void dispatch( actions.setPristine() ), + setRedirectUrl: ( url ) => + void dispatch( actions.setRedirectUrl( url ) ), + setHasError: () => void dispatch( actions.setHasError() ), + clearError: () => void dispatch( actions.clearError() ), + incrementCalculating: () => + void dispatch( actions.incrementCalculating() ), + decrementCalculating: () => + void dispatch( actions.decrementCalculating() ), + } ), + [] + ); + const onSubmit = () => { + // @todo this is where we do validation and checkout processing + dispatch( actions.setComplete() ); + }; + // emit events + useEffect( () => { + if ( checkoutState.isComplete ) { + emitEvent( + currentObservers.current, + EMIT_TYPES.CHECKOUT_COMPLETE, + {} + ); + } + }, [ checkoutState.isComplete ] ); + + // do redirect! + useEffect( () => { + // @todo check for if payment status and checkout status are complete + // and then redirect + window.location = checkoutState.redirectUrl; + }, [ checkoutState.isComplete ] ); + + //@todo setup the checkout data to pass on the checkout providers value. + return ( + + + + { children } + + + + ); +}; + +export default CheckoutProvider; diff --git a/assets/js/base/context/cart-checkout/checkout/reducer.js b/assets/js/base/context/cart-checkout/checkout/reducer.js new file mode 100644 index 00000000000..c8e22bafb55 --- /dev/null +++ b/assets/js/base/context/cart-checkout/checkout/reducer.js @@ -0,0 +1,63 @@ +/** + * Internal dependencies + */ +import { TYPES, DEFAULT_STATE } from './constants'; + +const { + SET_PRISTINE, + SET_REDIRECT_URL, + SET_COMPLETE, + SET_INCOMPLETE, + SET_HAS_ERROR, + SET_NO_ERROR, + INCREMENT_CALCULATING, + DECREMENT_CALCULATING, +} = TYPES; + +/** + * Reducer for the checkout state + * + * @param {Object} state Current state. + * @param {Object} action Incoming action object. + */ +export const reducer = ( state = DEFAULT_STATE, { url, type } ) => { + switch ( type ) { + case SET_PRISTINE: + return DEFAULT_STATE; + case SET_REDIRECT_URL: + return { + ...state, + redirectUrl: url, + }; + case SET_COMPLETE: + return { + ...state, + isComplete: true, + }; + case SET_INCOMPLETE: + return { + ...state, + isComplete: false, + }; + case SET_HAS_ERROR: + return { + ...state, + hasError: true, + }; + case SET_NO_ERROR: + return { + ...state, + hasError: false, + }; + case INCREMENT_CALCULATING: + return { + ...state, + calculatingCount: state.calculatingCount + 1, + }; + case DECREMENT_CALCULATING: + return { + ...state, + calculatingCount: Math.max( 0, state.calculatingCount - 1 ), + }; + } +}; diff --git a/assets/js/base/context/cart-checkout/errors.js b/assets/js/base/context/cart-checkout/errors.js index e69de29bb2d..453733d0ecb 100644 --- a/assets/js/base/context/cart-checkout/errors.js +++ b/assets/js/base/context/cart-checkout/errors.js @@ -0,0 +1,2 @@ +// @todo +// Create a context provider for validation error state. diff --git a/assets/js/base/context/cart-checkout/shipping/shipping-data-context.js b/assets/js/base/context/cart-checkout/shipping/shipping-data-context.js index 4234043eb4d..2f200b9a25f 100644 --- a/assets/js/base/context/cart-checkout/shipping/shipping-data-context.js +++ b/assets/js/base/context/cart-checkout/shipping/shipping-data-context.js @@ -145,7 +145,12 @@ export const ShippingMethodDataProvider = ( { children } ) => { shippingOptions ); } - }, [ shippingOptions, shippingOptionsLoading, shippingErrorStatus ] ); + }, [ + shippingOptions, + shippingOptionsLoading, + currentErrorStatus, + shippingErrorStatus, + ] ); const shippingMethodData = { shippingErrorStatus,