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..335e5f16491
--- /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_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,
+ } ),
+};
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..7868e6235ec
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/checkout/constants.js
@@ -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',
+};
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..aed614c09a3
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/checkout/event-emit.js
@@ -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 };
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..8bbfe573f22
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/checkout/index.js
@@ -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 (
+
+
+
+ { children }
+
+
+
+ );
+};
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..b50c32a0a8f
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/checkout/reducer.js
@@ -0,0 +1,134 @@
+/**
+ * Internal dependencies
+ */
+import { TYPES, DEFAULT_STATE, STATUS } from './constants';
+
+const {
+ SET_PRISTINE,
+ SET_PROCESSING,
+ SET_REDIRECT_URL,
+ SET_COMPLETE,
+ SET_HAS_ERROR,
+ SET_NO_ERROR,
+ INCREMENT_CALCULATING,
+ DECREMENT_CALCULATING,
+} = TYPES;
+
+const { PRISTINE, IDLE, CALCULATING, PROCESSING, COMPLETE } = STATUS;
+
+/**
+ * Reducer for the checkout state
+ *
+ * @param {Object} state Current state.
+ * @param {Object} action Incoming action object.
+ */
+export const reducer = ( state = DEFAULT_STATE, { url, type } ) => {
+ let status, nextStatus, newState;
+ switch ( type ) {
+ case SET_PRISTINE:
+ newState = DEFAULT_STATE;
+ break;
+ case SET_REDIRECT_URL:
+ newState =
+ url !== state.url
+ ? {
+ ...state,
+ redirectUrl: url,
+ }
+ : state;
+ break;
+ case SET_COMPLETE:
+ status = COMPLETE;
+ nextStatus = state.status;
+ if ( state.status === CALCULATING ) {
+ status = CALCULATING;
+ nextStatus = COMPLETE;
+ }
+ newState =
+ status !== state.status
+ ? {
+ ...state,
+ status,
+ nextStatus,
+ }
+ : state;
+ break;
+ case SET_PROCESSING:
+ status = PROCESSING;
+ nextStatus = state.status;
+ if ( state.status === CALCULATING ) {
+ status = CALCULATING;
+ nextStatus = PROCESSING;
+ }
+ newState =
+ status !== state.status
+ ? {
+ ...state,
+ status,
+ nextStatus,
+ hasError: false,
+ }
+ : state;
+ // clear any error state.
+ newState =
+ newState.hasError === false
+ ? newState
+ : { ...newState, hasError: false };
+ break;
+ case SET_HAS_ERROR:
+ newState = state.hasError
+ ? state
+ : {
+ ...state,
+ hasError: true,
+ };
+ newState =
+ state.status === PROCESSING
+ ? {
+ ...newState,
+ status: IDLE,
+ }
+ : newState;
+ break;
+ case SET_NO_ERROR:
+ newState = state.hasError
+ ? {
+ ...state,
+ hasError: false,
+ }
+ : state;
+ break;
+ case INCREMENT_CALCULATING:
+ newState = {
+ ...state,
+ status: CALCULATING,
+ nextStatus: state.status,
+ calculatingCount: state.calculatingCount + 1,
+ };
+ break;
+ case DECREMENT_CALCULATING:
+ status = CALCULATING;
+ nextStatus = state.status;
+ if ( state.calculatingCount <= 1 ) {
+ status = state.nextStatus;
+ nextStatus = IDLE;
+ }
+ newState = {
+ ...state,
+ status,
+ nextStatus,
+ calculatingCount: Math.max( 0, state.calculatingCount - 1 ),
+ };
+ break;
+ }
+ // automatically update state to idle from pristine as soon as it
+ // initially changes.
+ if (
+ newState !== state &&
+ type !== SET_PRISTINE &&
+ newState.status === PRISTINE
+ ) {
+ newState.status = IDLE;
+ }
+ return newState;
+};
diff --git a/assets/js/base/context/cart-checkout/event_emit/index.js b/assets/js/base/context/cart-checkout/event_emit/index.js
new file mode 100644
index 00000000000..38f5723ee1a
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/event_emit/index.js
@@ -0,0 +1 @@
+export * from './reducer';
diff --git a/assets/js/base/context/cart-checkout/event_emit/reducer.js b/assets/js/base/context/cart-checkout/event_emit/reducer.js
new file mode 100644
index 00000000000..f318bb74f6f
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/event_emit/reducer.js
@@ -0,0 +1,72 @@
+/**
+ * External dependencies
+ */
+import { omit, uniqueId } from 'lodash';
+
+export const TYPES = {
+ ADD_EVENT_CALLBACK: 'add_event_callback',
+ REMOVE_EVENT_CALLBACK: 'remove_event_callback',
+};
+
+export const actions = {
+ addEventCallback: ( eventType, callback ) => {
+ return {
+ id: uniqueId(),
+ type: TYPES.ADD_EVENT_CALLBACK,
+ eventType,
+ callback,
+ };
+ },
+ removeEventCallback: ( eventType, id ) => {
+ return {
+ id,
+ type: TYPES.REMOVE_EVENT_CALLBACK,
+ eventType,
+ };
+ },
+};
+
+/**
+ * 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
+ *
+ * @param {Object} state Current state.
+ * @param {Object} action Incoming action object
+ */
+export const reducer = ( state = {}, { type, eventType, id, callback } ) => {
+ switch ( type ) {
+ case TYPES.ADD_EVENT_CALLBACK:
+ return {
+ ...state,
+ [ eventType ]: {
+ ...state[ eventType ],
+ [ id ]: callback,
+ },
+ };
+ case TYPES.REMOVE_EVENT_CALLBACK:
+ return {
+ ...state,
+ [ eventType ]: omit( state[ eventType ], [ id ] ),
+ };
+ }
+ return state;
+};
diff --git a/assets/js/base/context/cart-checkout/index.js b/assets/js/base/context/cart-checkout/index.js
new file mode 100644
index 00000000000..f3a2609c7bd
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/index.js
@@ -0,0 +1,3 @@
+export * from './payment-methods';
+export * from './shipping';
+export * from './checkout';
diff --git a/assets/js/base/context/cart-checkout/payment-methods/actions.js b/assets/js/base/context/cart-checkout/payment-methods/actions.js
new file mode 100644
index 00000000000..9ee80930427
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/payment-methods/actions.js
@@ -0,0 +1,32 @@
+/**
+ * Internal dependencies
+ */
+import { STATUS } from './constants';
+const { ERROR, FAILED, SUCCESS } = STATUS;
+const SET_BILLING_DATA = 'set_billing_data';
+
+export const statusOnly = ( type ) => ( { type } );
+export const error = ( errorMessage ) => ( {
+ type: ERROR,
+ errorMessage,
+} );
+export const failed = ( {
+ errorMessage,
+ billingData,
+ paymentMethodData,
+} ) => ( {
+ type: FAILED,
+ errorMessage,
+ billingData,
+ paymentMethodData,
+} );
+export const success = ( { billingData, paymentMethodData } ) => ( {
+ type: SUCCESS,
+ billingData,
+ paymentMethodData,
+} );
+
+export const setBillingData = ( billingData ) => ( {
+ type: SET_BILLING_DATA,
+ billingData,
+} );
diff --git a/assets/js/base/context/cart-checkout/payment-methods/constants.js b/assets/js/base/context/cart-checkout/payment-methods/constants.js
new file mode 100644
index 00000000000..b09e3f95b84
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/payment-methods/constants.js
@@ -0,0 +1,61 @@
+/**
+ * @typedef {import('@woocommerce/type-defs/cart').CartBillingAddress} CartBillingAddress
+ * @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
+ */
+
+export const STATUS = {
+ PRISTINE: 'pristine',
+ STARTED: 'started',
+ PROCESSING: 'processing',
+ ERROR: 'has_error',
+ FAILED: 'failed',
+ SUCCESS: 'success',
+ COMPLETE: 'complete',
+};
+
+/**
+ * @type {CartBillingAddress}
+ */
+const DEFAULT_BILLING_DATA = {
+ first_name: '',
+ last_name: '',
+ company: '',
+ email: '',
+ phone: '',
+ country: '',
+ address_1: '',
+ address_2: '',
+ city: '',
+ state: '',
+ postcode: '',
+};
+
+/**
+ * @todo do typedefs for the payment event state.
+ */
+
+export const DEFAULT_PAYMENT_DATA = {
+ currentStatus: STATUS.PRISTINE,
+ billingData: DEFAULT_BILLING_DATA,
+ paymentMethodData: {
+ payment_method: '',
+ // arbitrary data the payment method
+ // wants to pass along for payment
+ // processing server side.
+ },
+ errorMessage: '',
+};
+
+/**
+ * @type {PaymentMethodDataContext}
+ */
+export const DEFAULT_PAYMENT_METHOD_DATA = {
+ setPaymentStatus: () => null,
+ currentStatus: STATUS.PRISTINE,
+ paymentStatuses: STATUS,
+ billingData: DEFAULT_BILLING_DATA,
+ paymentMethodData: {},
+ errorMessage: '',
+ activePaymentMethod: '',
+ setActivePaymentMethod: () => null,
+};
diff --git a/assets/js/base/context/cart-checkout/payment-methods/index.js b/assets/js/base/context/cart-checkout/payment-methods/index.js
new file mode 100644
index 00000000000..a6db02d53f9
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/payment-methods/index.js
@@ -0,0 +1,4 @@
+export {
+ PaymentMethodDataProvider,
+ usePaymentMethodDataContext,
+} from './payment-method-data-context';
diff --git a/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js b/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js
new file mode 100644
index 00000000000..39d3a92bc56
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/payment-methods/payment-method-data-context.js
@@ -0,0 +1,141 @@
+/**
+ * Internal dependencies
+ */
+import {
+ STATUS,
+ DEFAULT_PAYMENT_DATA,
+ DEFAULT_PAYMENT_METHOD_DATA,
+} from './constants';
+import reducer from './reducer';
+import {
+ statusOnly,
+ error,
+ failed,
+ success,
+ setBillingData as setBilling,
+} from './actions';
+
+/**
+ * External dependencies
+ */
+import {
+ createContext,
+ useContext,
+ useState,
+ useReducer,
+} from '@wordpress/element';
+
+/**
+ * @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
+ * @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
+ * @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatchers} PaymentStatusDispatchers
+ * @typedef {import('@woocommerce/type-defs/cart').CartBillingAddress} CartBillingAddress
+ */
+
+const {
+ STARTED,
+ PROCESSING,
+ COMPLETE,
+ PRISTINE,
+ ERROR,
+ FAILED,
+ SUCCESS,
+} = STATUS;
+
+const PaymentMethodDataContext = createContext( DEFAULT_PAYMENT_METHOD_DATA );
+
+/**
+ * @return {PaymentMethodDataContext} The data and functions exposed by the
+ * payment method context provider.
+ */
+export const usePaymentMethodDataContext = () => {
+ return useContext( PaymentMethodDataContext );
+};
+
+// @todo implement billing (and payment method) data saved to the cart data
+// store. That way checkout can obtain it for processing the order.
+
+export const PaymentMethodDataProvider = ( {
+ children,
+ activePaymentMethod: initialActivePaymentMethod,
+} ) => {
+ // dispatcher from checkout for updating it's status
+ const [ activePaymentMethod, setActive ] = useState(
+ initialActivePaymentMethod
+ );
+ const [ paymentStatus, dispatch ] = useReducer(
+ reducer,
+ DEFAULT_PAYMENT_DATA
+ );
+ const setActivePaymentMethod = ( paymentMethodSlug ) => {
+ setActive( paymentMethodSlug );
+ dispatch( statusOnly( PRISTINE ) );
+ };
+
+ /**
+ * @type {PaymentStatusDispatch}
+ */
+ const setPaymentStatus = () => ( {
+ started: () => dispatch( statusOnly( STARTED ) ),
+ processing: () => dispatch( statusOnly( PROCESSING ) ),
+ completed: () => dispatch( statusOnly( COMPLETE ) ),
+ /**
+ * @param {string} errorMessage An error message
+ */
+ error: ( errorMessage ) => dispatch( error( errorMessage ) ),
+ /**
+ * @param {string} errorMessage An error message
+ * @param {CartBillingAddress} billingData The billing data accompanying the payment method
+ * @param {Object} paymentMethodData Arbitrary payment method data to accompany the checkout submission.
+ */
+ failed: ( errorMessage, billingData, paymentMethodData ) =>
+ dispatch(
+ failed( {
+ errorMessage,
+ billingData,
+ paymentMethodData,
+ } )
+ ),
+ /**
+ * @param {CartBillingAddress} billingData The billing data accompanying the payment method.
+ * @param {Object} paymentMethodData Arbitrary payment method data to accompany the checkout.
+ */
+ success: ( billingData, paymentMethodData ) =>
+ dispatch(
+ success( {
+ billingData,
+ paymentMethodData,
+ } )
+ ),
+ } );
+ const setBillingData = ( billingData ) =>
+ dispatch( setBilling( billingData ) );
+ const currentStatus = {
+ isPristine: paymentStatus === PRISTINE,
+ isStarted: paymentStatus === STARTED,
+ isProcessing: paymentStatus === PROCESSING,
+ isFinished: [ ERROR, FAILED, SUCCESS ].includes( paymentStatus ),
+ hasError: paymentStatus === ERROR,
+ hasFailed: paymentStatus === FAILED,
+ isSuccessful: paymentStatus === SUCCESS,
+ };
+ /**
+ * @type {PaymentMethodDataContext}
+ */
+ const paymentData = {
+ setPaymentStatus,
+ currentStatus,
+ paymentStatuses: STATUS,
+ billingData: paymentStatus.billingData,
+ paymentMethodData: paymentStatus.paymentMethodData,
+ errorMessage: paymentStatus.errorMessage,
+ activePaymentMethod,
+ setActivePaymentMethod,
+ setBillingData,
+ };
+ return (
+
+ { children }
+
+ );
+};
diff --git a/assets/js/base/context/cart-checkout/payment-methods/reducer.js b/assets/js/base/context/cart-checkout/payment-methods/reducer.js
new file mode 100644
index 00000000000..1c8915aae74
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/payment-methods/reducer.js
@@ -0,0 +1,79 @@
+/**
+ * Internal dependencies
+ */
+import { STATUS, DEFAULT_PAYMENT_DATA } from './constants';
+const {
+ STARTED,
+ ERROR,
+ FAILED,
+ SUCCESS,
+ PROCESSING,
+ PRISTINE,
+ COMPLETE,
+} = STATUS;
+
+const SET_BILLING_DATA = 'set_billing_data';
+
+/**
+ * Reducer for payment data state
+ *
+ * @param {Object} state Current state.
+ * @param {Object} action Current action.
+ */
+const reducer = (
+ state = DEFAULT_PAYMENT_DATA,
+ { type, billingData, paymentMethodData, errorMessage }
+) => {
+ switch ( type ) {
+ case STARTED:
+ return {
+ ...state,
+ currentStatus: STARTED,
+ };
+ case ERROR:
+ return {
+ ...state,
+ currentStatus: ERROR,
+ errorMessage: errorMessage || state.errorMessage,
+ };
+ case FAILED:
+ return {
+ ...state,
+ currentStatus: FAILED,
+ billingData: billingData || state.billingData,
+ paymentMethodData: paymentMethodData || state.paymentMethodData,
+ errorMessage: errorMessage || state.errorMessage,
+ };
+ case SUCCESS:
+ return {
+ ...state,
+ currentStatus: SUCCESS,
+ billingData: billingData || state.billingData,
+ paymentMethodData: paymentMethodData || state.paymentMethodData,
+ };
+ case PROCESSING:
+ return {
+ ...state,
+ currentStatus: PROCESSING,
+ };
+ case COMPLETE:
+ return {
+ ...state,
+ currentStatus: COMPLETE,
+ };
+
+ case PRISTINE:
+ return {
+ ...DEFAULT_PAYMENT_DATA,
+ currentStatus: PRISTINE,
+ };
+ case SET_BILLING_DATA:
+ return {
+ ...state,
+ billingData,
+ };
+ }
+ return state;
+};
+
+export default reducer;
diff --git a/assets/js/base/context/cart-checkout/shipping/constants.js b/assets/js/base/context/cart-checkout/shipping/constants.js
new file mode 100644
index 00000000000..c51089e5db8
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/shipping/constants.js
@@ -0,0 +1,42 @@
+/**
+ * @type {import('@woocommerce/type-defs/contexts').ShippingErrorTypes}
+ */
+export const ERROR_TYPES = {
+ NONE: 'none',
+ INVALID_ADDRESS: 'invalid_address',
+ UNKNOWN: 'unknown_error',
+};
+
+/**
+ * @type {import('@woocommerce/type-defs/cart').CartShippingAddress}
+ */
+export const DEFAULT_SHIPPING_ADDRESS = {
+ first_name: '',
+ last_name: '',
+ company: '',
+ address_1: '',
+ address_2: '',
+ city: '',
+ state: '',
+ postcode: '',
+ country: '',
+};
+
+/**
+ * @type {import('@woocommerce/type-defs/contexts').ShippingMethodDataContext}
+ */
+export const DEFAULT_SHIPPING_CONTEXT_DATA = {
+ shippingErrorStatus: ERROR_TYPES.NONE,
+ dispatchErrorStatus: () => null,
+ shippingErrorTypes: ERROR_TYPES,
+ shippingRates: [],
+ setShippingRates: () => null,
+ shippingRatesLoading: false,
+ selectedRates: [],
+ setSelectedRates: () => null,
+ shippingAddress: DEFAULT_SHIPPING_ADDRESS,
+ setShippingAddress: () => null,
+ onShippingRateSuccess: () => null,
+ onShippingRateFail: () => null,
+ needsShipping: false,
+};
diff --git a/assets/js/base/context/cart-checkout/shipping/event-emit.js b/assets/js/base/context/cart-checkout/shipping/event-emit.js
new file mode 100644
index 00000000000..e65c1c1143d
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/shipping/event-emit.js
@@ -0,0 +1,82 @@
+/**
+ * Internal dependencies
+ */
+import { actions, reducer, emitEvent } from '../event_emit';
+
+const EMIT_TYPES = {
+ SHIPPING_RATES_SUCCESS: 'shipping_rates_success',
+ SHIPPING_RATES_FAIL: 'shipping_rates_fail',
+ SHIPPING_RATE_SELECT_SUCCESS: 'shipping_rate_select_success',
+ SHIPPING_RATE_SELECT_FAIL: 'shipping_rate_select_fail',
+};
+
+/**
+ * Receives a reducer dispatcher and returns an object with the onSuccess and
+ * onFail callback registration points for the shipping option 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 A reducer dispatcher
+ * @return {Object} An object with `onSuccess` and `onFail` emitter registration.
+ */
+const emitterSubscribers = ( dispatcher ) => ( {
+ onSuccess: ( callback ) => {
+ const action = actions.addEventCallback(
+ EMIT_TYPES.SHIPPING_RATES_SUCCESS,
+ callback
+ );
+ dispatcher( action );
+ return () => {
+ dispatcher(
+ actions.removeEventCallback(
+ EMIT_TYPES.SHIPPING_RATES_SUCCESS,
+ action.id
+ )
+ );
+ };
+ },
+ onFail: ( callback ) => {
+ const action = actions.removeEventCallback(
+ EMIT_TYPES.SHIPPING_RATES_FAIL,
+ callback
+ );
+ dispatcher( action );
+ return () => {
+ dispatcher( EMIT_TYPES.SHIPPING_RATES_FAIL, action.id );
+ };
+ },
+ onSelectSuccess: ( callback ) => {
+ const action = actions.addEventCallback(
+ EMIT_TYPES.SHIPPING_RATE_SELECT_SUCCESS,
+ callback
+ );
+ dispatcher( action );
+ return () => {
+ dispatcher(
+ actions.removeEventCallback(
+ EMIT_TYPES.SHIPPING_RATE_SELECT_SUCCESS,
+ action.id
+ )
+ );
+ };
+ },
+ onSelectFail: ( callback ) => {
+ const action = actions.addEventCallback(
+ EMIT_TYPES.SHIPPING_RATE_SELECT_FAIL,
+ callback
+ );
+ dispatcher( action );
+ return () => {
+ dispatcher(
+ actions.removeEventCallback(
+ EMIT_TYPES.SHIPPING_RATE_SELECT_FAIL,
+ action.id
+ )
+ );
+ };
+ },
+} );
+
+export { EMIT_TYPES, emitterSubscribers, reducer, emitEvent };
diff --git a/assets/js/base/context/cart-checkout/shipping/index.js b/assets/js/base/context/cart-checkout/shipping/index.js
new file mode 100644
index 00000000000..1cfff9bbf95
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/shipping/index.js
@@ -0,0 +1,4 @@
+export {
+ useShippingMethodDataContext,
+ ShippingMethodDataProvider,
+} from './shipping-data-context';
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
new file mode 100644
index 00000000000..3973c636f45
--- /dev/null
+++ b/assets/js/base/context/cart-checkout/shipping/shipping-data-context.js
@@ -0,0 +1,208 @@
+/**
+ * Internal dependencies
+ */
+import {
+ ERROR_TYPES,
+ DEFAULT_SHIPPING_ADDRESS,
+ DEFAULT_SHIPPING_CONTEXT_DATA,
+} from './constants';
+import {
+ EMIT_TYPES,
+ emitterSubscribers,
+ reducer as emitReducer,
+ emitEvent,
+} from './event-emit';
+
+/**
+ * External dependencies
+ */
+import {
+ createContext,
+ useContext,
+ useState,
+ useReducer,
+ useEffect,
+ useCallback,
+ useRef,
+} from '@wordpress/element';
+import { useShippingRates, useStoreCart } from '@woocommerce/base-hooks';
+import { useCheckoutContext } from '@woocommerce/base-context';
+
+/**
+ * @typedef {import('@woocommerce/type-defs/contexts').ShippingMethodDataContext} ShippingMethodDataContext
+ */
+
+const { NONE, INVALID_ADDRESS, UNKNOWN } = ERROR_TYPES;
+
+const setStatusAction = ( status ) => ( {
+ type: status,
+} );
+
+/**
+ * Reducer for shipping status state
+ *
+ * @param {string} state The current status.
+ * @param {Object} action The incoming action.
+ */
+const errorStatusReducer = ( state, { type } ) => {
+ if ( Object.keys( ERROR_TYPES ).includes( type ) ) {
+ return state;
+ }
+ return type;
+};
+
+const ShippingMethodDataContext = createContext(
+ DEFAULT_SHIPPING_CONTEXT_DATA
+);
+
+/**
+ * @return {ShippingMethodDataContext} Returns data and functions related to
+ * shipping methods.
+ */
+export const useShippingMethodDataContext = () => {
+ return useContext( ShippingMethodDataContext );
+};
+
+/**
+ * Calculating component for shipping rates.
+ *
+ * @param {Object} props Incoming props for the component
+ */
+const ShippingRateCalculation = ( { address, onChange } ) => {
+ // @todo, it'd be handy if we could pass through callbacks that are fired on
+ // successful rate retrieval vs callbacks fired on unsuccessful rates
+ // retrieval. That way emitters could just be fed into the hook directly.
+ const { shippingRates, shippingRatesLoading } = useShippingRates( address );
+ useEffect( () => {
+ onChange( shippingRates, shippingRatesLoading );
+ }, [ shippingRates, shippingRatesLoading ] );
+ return null;
+};
+
+// @todo wire up checkout context needed here (like isCalculating etc)
+// @todo useShippingRates needs to be wired up with error handling so we know
+// when an invalid address is provided for it (because payment methods might
+// provide an invalid address)
+export const ShippingMethodDataProvider = ( { children } ) => {
+ const { dispatchActions } = useCheckoutContext();
+ const { cartNeedsShipping: needsShipping } = useStoreCart();
+ const [ shippingErrorStatus, dispatchErrorStatus ] = useReducer(
+ errorStatusReducer,
+ NONE
+ );
+ const [ observers, subscriber ] = useReducer( emitReducer, {} );
+ const [ currentShippingAddress, setAddressState ] = useState(
+ DEFAULT_SHIPPING_ADDRESS
+ );
+ const currentObservers = useRef( observers );
+ const [ shippingOptions, setShippingOptions ] = useState( [] );
+ const [ shippingOptionsLoading, setShippingOptionsLoading ] = useState(
+ false
+ );
+ // @todo, this will need wired up to persistence (useSelectedRates?) which
+ // will be setup similar to `useShippingRates` (or maybe in the same hook?)
+ const [ selectedRates, setSelectedRates ] = useState( [] );
+ const setShippingAddress = useCallback( ( address ) => {
+ setAddressState( ( prevAddress ) => ( { ...prevAddress, address } ) );
+ }, [] );
+ const onShippingRateSuccess = emitterSubscribers( subscriber ).onSuccess;
+ const onShippingRateFail = emitterSubscribers( subscriber ).onFail;
+ const onShippingRateSelectSuccess = emitterSubscribers( subscriber )
+ .onSelectSuccess;
+ const onShippingRateSelectFail = emitterSubscribers( subscriber )
+ .onShippingRateSelectFail;
+
+ // set observers on ref so it's always current
+ useEffect( () => {
+ currentObservers.current = observers;
+ }, [ observers ] );
+
+ // increment/decrement checkout calculating counts when shipping is loading
+ useEffect( () => {
+ if ( shippingOptionsLoading ) {
+ dispatchActions.incrementCalculating();
+ } else {
+ dispatchActions.decrementCalculating();
+ }
+ }, [ shippingOptionsLoading ] );
+
+ // @todo need to add error handling to useShippingRates so that errors are
+ // exposed. We need error types exposed by the error handling as well.
+ // also we need to add similar logic for selection/unselection of rates and
+ // emit the events (see emit events block)
+ const onRateChange = ( shippingRates, shippingRatesLoading, error ) => {
+ setShippingOptions( shippingRates );
+ setShippingOptionsLoading( shippingRatesLoading );
+ if ( ! shippingRatesLoading && error && error.type ) {
+ // @todo this type might need normalizing to something recognized by
+ // the ERROR_TYPE constants.
+ dispatchErrorStatus( setStatusAction( error.type ) );
+ } else if ( ! shippingRatesLoading && shippingRates ) {
+ dispatchErrorStatus( NONE );
+ }
+ };
+
+ const currentErrorStatus = {
+ isPristine: shippingErrorStatus === NONE,
+ isValid: shippingErrorStatus === NONE,
+ hasInvalidAddress: shippingErrorStatus === INVALID_ADDRESS,
+ hasError:
+ shippingErrorStatus === UNKNOWN ||
+ shippingErrorStatus === INVALID_ADDRESS,
+ };
+
+ // emit events
+ // @todo add emitters for shipping rate selection.
+ useEffect( () => {
+ if ( ! shippingOptionsLoading && currentErrorStatus.hasError ) {
+ emitEvent(
+ currentObservers.current,
+ EMIT_TYPES.SHIPPING_RATES_SUCCESS,
+ shippingErrorStatus
+ );
+ } else if ( ! shippingOptionsLoading && shippingOptions ) {
+ emitEvent(
+ currentObservers.current,
+ EMIT_TYPES.SHIPPING_RATES_SUCCESS,
+ shippingOptions
+ );
+ }
+ }, [
+ shippingOptions,
+ shippingOptionsLoading,
+ currentErrorStatus,
+ shippingErrorStatus,
+ ] );
+
+ /**
+ * @type {ShippingMethodDataContext}
+ */
+ const shippingMethodData = {
+ shippingErrorStatus,
+ dispatchErrorStatus,
+ shippingErrorTypes: ERROR_TYPES,
+ shippingRates: shippingOptions,
+ setShippingRates: setShippingOptions,
+ shippingRatesLoading: shippingOptionsLoading,
+ selectedRates,
+ setSelectedRates,
+ shippingAddress: currentShippingAddress,
+ setShippingAddress,
+ onShippingRateSuccess,
+ onShippingRateFail,
+ onShippingRateSelectSuccess,
+ onShippingRateSelectFail,
+ needsShipping,
+ };
+ return (
+ <>
+
+
+ { children }
+
+ >
+ );
+};
diff --git a/assets/js/base/context/checkout-context.js b/assets/js/base/context/checkout-context.js
deleted file mode 100644
index b0bb3aff1fb..00000000000
--- a/assets/js/base/context/checkout-context.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * External dependencies
- */
-import {
- createContext,
- useContext,
- useState,
- useMemo,
-} from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-
-const CheckoutContext = createContext( {} );
-
-export const useCheckoutContext = () => {
- return useContext( CheckoutContext );
-};
-
-const CheckoutProvider = ( {
- children,
- 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 [ activePaymentMethod, setActivePaymentMethod ] = useState(
- initialActivePaymentMethod
- );
- const contextValue = useMemo( () => {
- return {
- successRedirectUrl,
- setSuccessRedirectUrl,
- failureRedirectUrl,
- setFailureRedirectUrl,
- isCheckoutComplete,
- setIsCheckoutComplete,
- checkoutHasError,
- setCheckoutHasError,
- isCalculating,
- setIsCalculating,
- notices,
- updateNotices,
- activePaymentMethod,
- setActivePaymentMethod,
- placeOrderLabel,
- };
- }, [
- successRedirectUrl,
- failureRedirectUrl,
- isCheckoutComplete,
- isCalculating,
- checkoutHasError,
- activePaymentMethod,
- placeOrderLabel,
- notices,
- ] );
- return (
-
- { children }
-
- );
-};
-
-export default CheckoutProvider;
diff --git a/assets/js/base/context/index.js b/assets/js/base/context/index.js
new file mode 100644
index 00000000000..e5f1e8dbcf6
--- /dev/null
+++ b/assets/js/base/context/index.js
@@ -0,0 +1,4 @@
+export * from './cart-checkout';
+export * from './inner-block-configuration-context';
+export * from './product-layout-context';
+export * from './query-state-context';