Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
Add new contexts for checkout
Browse files Browse the repository at this point in the history
  • Loading branch information
nerrad committed Mar 5, 2020
1 parent 6cb9033 commit ea99485
Show file tree
Hide file tree
Showing 19 changed files with 1,203 additions and 67 deletions.
43 changes: 43 additions & 0 deletions assets/js/base/context/cart-checkout/checkout/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Internal dependencies
*/
import { TYPES } from './constants';

const {
SET_PRISTINE,
SET_PROCESSING,
SET_REDIRECT_URL,
SET_COMPLETE,
SET_HAS_ERROR,
SET_NO_ERROR,
INCREMENT_CALCULATING,
DECREMENT_CALCULATING,
} = TYPES;

export const actions = {
setPristine: () => ( {
type: SET_PRISTINE,
} ),
setProcessing: () => ( {
type: SET_PROCESSING,
} ),
setRedirectUrl: ( url ) => ( {
type: SET_REDIRECT_URL,
url,
} ),
setComplete: () => ( {
type: SET_COMPLETE,
} ),
setHasError: () => ( {
type: SET_HAS_ERROR,
} ),
clearError: () => ( {
type: SET_NO_ERROR,
} ),
incrementCalculating: () => ( {
type: INCREMENT_CALCULATING,
} ),
decrementCalculating: () => ( {
type: DECREMENT_CALCULATING,
} ),
};
31 changes: 31 additions & 0 deletions assets/js/base/context/cart-checkout/checkout/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @type {import("@woocommerce/type-defs/checkout").CheckoutStatusConstants}
*/
export const STATUS = {
PRISTINE: 'pristine',
IDLE: 'idle',
CALCULATING: 'calculating',
PROCESSING: 'processing',
COMPLETE: 'complete',
};

export const DEFAULT_STATE = {
redirectUrl: '',
status: STATUS.PRISTINE,
// this is used by the reducer to set what status the state will
// take after calculating is complete.
nextStatus: STATUS.IDLE,
hasError: false,
calculatingCount: 0,
};

export const TYPES = {
SET_PRISTINE: 'set_pristine',
SET_REDIRECT_URL: 'set_redirect_url',
SET_COMPLETE: 'set_checkout_complete',
SET_PROCESSING: 'set_checkout_is_processing',
SET_HAS_ERROR: 'set_checkout_has_error',
SET_NO_ERROR: 'set_checkout_no_error',
INCREMENT_CALCULATING: 'increment_calculating',
DECREMENT_CALCULATING: 'decrement_calculating',
};
73 changes: 73 additions & 0 deletions assets/js/base/context/cart-checkout/checkout/event-emit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Internal dependencies
*/
import { actions, reducer, emitEvent } from '../event_emit';

const EMIT_TYPES = {
CHECKOUT_COMPLETE_WITH_SUCCESS: 'checkout_complete',
CHECKOUT_COMPLETE_WITH_ERROR: 'checkout_complete_error',
CHECKOUT_PROCESSING: 'checkout_processing',
};

/**
* 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 ) => ( {
onCheckoutCompleteSuccess: ( callback ) => {
const action = actions.addEventCallback(
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_SUCCESS,
callback
);
dispatcher( action );
return () => {
dispatcher(
actions.removeEventCallback(
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_SUCCESS,
action.id
)
);
};
},
onCheckoutCompleteError: ( callback ) => {
const action = actions.addEventCallback(
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_ERROR,
callback
);
dispatcher( action );
return () => {
dispatcher(
actions.removeEventCallback(
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_ERROR,
action.id
)
);
};
},
onCheckoutProcessing: ( callback ) => {
const action = actions.addEventCallback(
EMIT_TYPES.CHECKOUT_PROCESSING,
callback
);
dispatcher( action );
return () => {
dispatcher(
actions.removeEventCallback(
EMIT_TYPES.CHECKOUT_PROCESSING,
action.id
)
);
};
},
} );

export { EMIT_TYPES, emitterSubscribers, reducer, emitEvent };
189 changes: 189 additions & 0 deletions assets/js/base/context/cart-checkout/checkout/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Internal dependencies
*/
import { PaymentMethodDataProvider } from '../payment-methods';
import { ShippingMethodDataProvider } from '../shipping';
import { actions } from './actions';
import { reducer } from './reducer';
import { DEFAULT_STATE, STATUS } 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';

/**
* @typedef {import('@woocommerce/type-defs/checkout').CheckoutDispatchActions} CheckoutDispatchActions
* @typedef {import('@woocommerce/type-defs/contexts').CheckoutDataContext} CheckoutDataContext
*/

const CheckoutContext = createContext( {
submitLabel: '',
onSubmit: () => void null,
isComplete: false,
isIdle: false,
isCalculating: false,
isProcessing: false,
hasError: false,
redirectUrl: '',
onCheckoutCompleteSuccess: () => void null,
onCheckoutCompleteError: () => void null,
onCheckoutProcessing: () => void null,
dispatchActions: {
resetCheckout: () => void null,
setRedirectUrl: () => void null,
setHasError: () => void null,
clearError: () => void null,
incrementCalculating: () => void null,
decrementCalculating: () => void null,
},
} );

/**
* @return {CheckoutDataContext} Returns the checkout data context value
*/
export const useCheckoutContext = () => {
return useContext( CheckoutContext );
};

// create context provider for coupons

// todo have a local state (reducer) for validation error objects (see checkout processing event).
// this will need exposed for checkout/cart steps to read from when checkout.hasError to be able
// to show what fields need addressed.

// 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.

export 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 onCheckoutCompleteSuccess = emitterSubscribers( subscriber )
.onCheckoutCompleteSuccess;
const onCheckoutCompleteError = emitterSubscribers( subscriber )
.onCheckoutCompleteError;
const onCheckoutProcessing = emitterSubscribers( subscriber )
.onCheckoutProcessing;

/**
* @type {CheckoutDispatchActions}
*/
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() ),
} ),
[]
);

// emit events
useEffect( () => {
const status = checkoutState.status;
if ( status === STATUS.PROCESSING ) {
const error = emitEvent(
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.
if ( error ) {
dispatchActions.setHasError();
}
dispatch( actions.setComplete() );
}
if ( checkoutState.isComplete ) {
if ( checkoutState.hasError ) {
emitEvent(
currentObservers.current,
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_ERROR,
{}
);
} else {
emitEvent(
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;
}
}
}, [ checkoutState.status, checkoutState.hasError ] );

const onSubmit = () => {
dispatch( actions.setProcessing() );
};

/**
* @type {CheckoutDataContext}
*/
const checkoutData = {
submitLabel,
onSubmit,
isComplete: checkoutState.status === STATUS.COMPLETE,
isIdle: checkoutState.status === STATUS.IDLE,
isCalculating: checkoutState.status === STATUS.CALCULATING,
isProcessing: checkoutState.status === STATUS.PROCESSING,
hasError: checkoutState.hasError,
redirectUrl: checkoutState.redirectUrl,
onCheckoutCompleteSuccess,
onCheckoutCompleteError,
onCheckoutProcessing,
dispatchActions,
};
return (
<CheckoutContext.Provider value={ checkoutData }>
<PaymentMethodDataProvider
activePaymentMethod={ initialActivePaymentMethod }
>
<ShippingMethodDataProvider>
{ children }
</ShippingMethodDataProvider>
</PaymentMethodDataProvider>
</CheckoutContext.Provider>
);
};
Loading

0 comments on commit ea99485

Please sign in to comment.