initial commit
This commit is contained in:
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { PaymentResultDataType, CheckoutStateContextState } from './types';
|
||||
|
||||
export enum ACTION {
|
||||
SET_IDLE = 'set_idle',
|
||||
SET_PRISTINE = 'set_pristine',
|
||||
SET_REDIRECT_URL = 'set_redirect_url',
|
||||
SET_COMPLETE = 'set_checkout_complete',
|
||||
SET_BEFORE_PROCESSING = 'set_before_processing',
|
||||
SET_AFTER_PROCESSING = 'set_after_processing',
|
||||
SET_PROCESSING_RESPONSE = 'set_processing_response',
|
||||
SET_PROCESSING = 'set_checkout_is_processing',
|
||||
SET_HAS_ERROR = 'set_checkout_has_error',
|
||||
SET_NO_ERROR = 'set_checkout_no_error',
|
||||
SET_CUSTOMER_ID = 'set_checkout_customer_id',
|
||||
SET_ORDER_ID = 'set_checkout_order_id',
|
||||
SET_ORDER_NOTES = 'set_checkout_order_notes',
|
||||
INCREMENT_CALCULATING = 'increment_calculating',
|
||||
DECREMENT_CALCULATING = 'decrement_calculating',
|
||||
SET_SHOULD_CREATE_ACCOUNT = 'set_should_create_account',
|
||||
SET_EXTENSION_DATA = 'set_extension_data',
|
||||
}
|
||||
|
||||
export interface ActionType extends Partial< CheckoutStateContextState > {
|
||||
type: ACTION;
|
||||
data?:
|
||||
| Record< string, unknown >
|
||||
| Record< string, never >
|
||||
| PaymentResultDataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the actions that can be dispatched for the checkout.
|
||||
*/
|
||||
export const actions = {
|
||||
setPristine: () =>
|
||||
( {
|
||||
type: ACTION.SET_PRISTINE,
|
||||
} as const ),
|
||||
setIdle: () =>
|
||||
( {
|
||||
type: ACTION.SET_IDLE,
|
||||
} as const ),
|
||||
setProcessing: () =>
|
||||
( {
|
||||
type: ACTION.SET_PROCESSING,
|
||||
} as const ),
|
||||
setRedirectUrl: ( redirectUrl: string ) =>
|
||||
( {
|
||||
type: ACTION.SET_REDIRECT_URL,
|
||||
redirectUrl,
|
||||
} as const ),
|
||||
setProcessingResponse: ( data: PaymentResultDataType ) =>
|
||||
( {
|
||||
type: ACTION.SET_PROCESSING_RESPONSE,
|
||||
data,
|
||||
} as const ),
|
||||
setComplete: ( data: Record< string, unknown > = {} ) =>
|
||||
( {
|
||||
type: ACTION.SET_COMPLETE,
|
||||
data,
|
||||
} as const ),
|
||||
setBeforeProcessing: () =>
|
||||
( {
|
||||
type: ACTION.SET_BEFORE_PROCESSING,
|
||||
} as const ),
|
||||
setAfterProcessing: () =>
|
||||
( {
|
||||
type: ACTION.SET_AFTER_PROCESSING,
|
||||
} as const ),
|
||||
setHasError: ( hasError = true ) =>
|
||||
( {
|
||||
type: hasError ? ACTION.SET_HAS_ERROR : ACTION.SET_NO_ERROR,
|
||||
} as const ),
|
||||
incrementCalculating: () =>
|
||||
( {
|
||||
type: ACTION.INCREMENT_CALCULATING,
|
||||
} as const ),
|
||||
decrementCalculating: () =>
|
||||
( {
|
||||
type: ACTION.DECREMENT_CALCULATING,
|
||||
} as const ),
|
||||
setCustomerId: ( customerId: number ) =>
|
||||
( {
|
||||
type: ACTION.SET_CUSTOMER_ID,
|
||||
customerId,
|
||||
} as const ),
|
||||
setOrderId: ( orderId: number ) =>
|
||||
( {
|
||||
type: ACTION.SET_ORDER_ID,
|
||||
orderId,
|
||||
} as const ),
|
||||
setShouldCreateAccount: ( shouldCreateAccount: boolean ) =>
|
||||
( {
|
||||
type: ACTION.SET_SHOULD_CREATE_ACCOUNT,
|
||||
shouldCreateAccount,
|
||||
} as const ),
|
||||
setOrderNotes: ( orderNotes: string ) =>
|
||||
( {
|
||||
type: ACTION.SET_ORDER_NOTES,
|
||||
orderNotes,
|
||||
} as const ),
|
||||
setExtensionData: (
|
||||
extensionData: Record< string, Record< string, unknown > >
|
||||
) =>
|
||||
( {
|
||||
type: ACTION.SET_EXTENSION_DATA,
|
||||
extensionData,
|
||||
} as const ),
|
||||
};
|
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type {
|
||||
CheckoutStateContextType,
|
||||
CheckoutStateContextState,
|
||||
} from './types';
|
||||
|
||||
export enum STATUS {
|
||||
// Checkout is in it's initialized state.
|
||||
PRISTINE = 'pristine',
|
||||
// When checkout state has changed but there is no activity happening.
|
||||
IDLE = 'idle',
|
||||
// After BEFORE_PROCESSING status emitters have finished successfully. Payment processing is started on this checkout status.
|
||||
PROCESSING = 'processing',
|
||||
// After the AFTER_PROCESSING event emitters have completed. This status triggers the checkout redirect.
|
||||
COMPLETE = 'complete',
|
||||
// This is the state before checkout processing begins after the checkout button has been pressed/submitted.
|
||||
BEFORE_PROCESSING = 'before_processing',
|
||||
// After server side checkout processing is completed this status is set
|
||||
AFTER_PROCESSING = 'after_processing',
|
||||
}
|
||||
|
||||
const preloadedApiRequests = getSetting( 'preloadedApiRequests', {} ) as Record<
|
||||
string,
|
||||
{ body: Record< string, unknown > }
|
||||
>;
|
||||
|
||||
const checkoutData = {
|
||||
order_id: 0,
|
||||
customer_id: 0,
|
||||
...( preloadedApiRequests[ '/wc/store/checkout' ]?.body || {} ),
|
||||
};
|
||||
|
||||
export const DEFAULT_CHECKOUT_STATE_DATA: CheckoutStateContextType = {
|
||||
dispatchActions: {
|
||||
resetCheckout: () => void null,
|
||||
setRedirectUrl: ( url ) => void url,
|
||||
setHasError: ( hasError ) => void hasError,
|
||||
setAfterProcessing: ( response ) => void response,
|
||||
incrementCalculating: () => void null,
|
||||
decrementCalculating: () => void null,
|
||||
setCustomerId: ( id ) => void id,
|
||||
setOrderId: ( id ) => void id,
|
||||
setOrderNotes: ( orderNotes ) => void orderNotes,
|
||||
setExtensionData: ( extensionData ) => void extensionData,
|
||||
},
|
||||
onSubmit: () => void null,
|
||||
isComplete: false,
|
||||
isIdle: false,
|
||||
isCalculating: false,
|
||||
isProcessing: false,
|
||||
isBeforeProcessing: false,
|
||||
isAfterProcessing: false,
|
||||
hasError: false,
|
||||
redirectUrl: '',
|
||||
orderId: 0,
|
||||
orderNotes: '',
|
||||
customerId: 0,
|
||||
onCheckoutAfterProcessingWithSuccess: () => () => void null,
|
||||
onCheckoutAfterProcessingWithError: () => () => void null,
|
||||
onCheckoutBeforeProcessing: () => () => void null, // deprecated for onCheckoutValidationBeforeProcessing
|
||||
onCheckoutValidationBeforeProcessing: () => () => void null,
|
||||
hasOrder: false,
|
||||
isCart: false,
|
||||
shouldCreateAccount: false,
|
||||
setShouldCreateAccount: ( value ) => void value,
|
||||
extensionData: {},
|
||||
};
|
||||
|
||||
export const DEFAULT_STATE: CheckoutStateContextState = {
|
||||
redirectUrl: '',
|
||||
status: STATUS.PRISTINE,
|
||||
hasError: false,
|
||||
calculatingCount: 0,
|
||||
orderId: checkoutData.order_id,
|
||||
orderNotes: '',
|
||||
customerId: checkoutData.customer_id,
|
||||
shouldCreateAccount: false,
|
||||
processingResponse: null,
|
||||
extensionData: {},
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
emitterCallback,
|
||||
reducer,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
ActionType,
|
||||
} from '../../../event-emit';
|
||||
|
||||
const EMIT_TYPES = {
|
||||
CHECKOUT_VALIDATION_BEFORE_PROCESSING:
|
||||
'checkout_validation_before_processing',
|
||||
CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS:
|
||||
'checkout_after_processing_with_success',
|
||||
CHECKOUT_AFTER_PROCESSING_WITH_ERROR:
|
||||
'checkout_after_processing_with_error',
|
||||
};
|
||||
|
||||
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(
|
||||
() => ( {
|
||||
onCheckoutAfterProcessingWithSuccess: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
observerDispatch
|
||||
),
|
||||
onCheckoutAfterProcessingWithError: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
observerDispatch
|
||||
),
|
||||
onCheckoutValidationBeforeProcessing: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
|
||||
observerDispatch
|
||||
),
|
||||
} ),
|
||||
[ observerDispatch ]
|
||||
);
|
||||
return eventEmitters;
|
||||
};
|
||||
|
||||
export { EMIT_TYPES, useEventEmitters, reducer, emitEvent, emitEventWithAbort };
|
@ -0,0 +1,397 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useRef,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { usePrevious } from '@woocommerce/base-hooks';
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
import { isObject } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { actions } from './actions';
|
||||
import { reducer } from './reducer';
|
||||
import { getPaymentResultFromCheckoutResponse } from './utils';
|
||||
import {
|
||||
DEFAULT_STATE,
|
||||
STATUS,
|
||||
DEFAULT_CHECKOUT_STATE_DATA,
|
||||
} from './constants';
|
||||
import type {
|
||||
CheckoutStateDispatchActions,
|
||||
CheckoutStateContextType,
|
||||
} from './types';
|
||||
import {
|
||||
EMIT_TYPES,
|
||||
useEventEmitters,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
reducer as emitReducer,
|
||||
} from './event-emit';
|
||||
import { useValidationContext } from '../../validation';
|
||||
import { useStoreNotices } from '../../../hooks/use-store-notices';
|
||||
import { useStoreEvents } from '../../../hooks/use-store-events';
|
||||
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
|
||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').CheckoutDataContext} CheckoutDataContext
|
||||
*/
|
||||
|
||||
const CheckoutContext = createContext( DEFAULT_CHECKOUT_STATE_DATA );
|
||||
|
||||
export const useCheckoutContext = (): CheckoutStateContextType => {
|
||||
return useContext( CheckoutContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkout state provider
|
||||
* This provides an API interface exposing checkout state for use with cart or checkout blocks.
|
||||
*
|
||||
* @param {Object} props Incoming props for the provider.
|
||||
* @param {Object} props.children The children being wrapped.
|
||||
* @param {string} props.redirectUrl Initialize what the checkout will redirect to after successful submit.
|
||||
* @param {boolean} props.isCart If context provider is being used in cart context.
|
||||
*/
|
||||
export const CheckoutStateProvider = ( {
|
||||
children,
|
||||
redirectUrl,
|
||||
isCart = false,
|
||||
}: {
|
||||
children: React.ReactChildren;
|
||||
redirectUrl: string;
|
||||
isCart: boolean;
|
||||
} ): JSX.Element => {
|
||||
// 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 { setValidationErrors } = useValidationContext();
|
||||
const { addErrorNotice, removeNotices } = useStoreNotices();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const isCalculating = checkoutState.calculatingCount > 0;
|
||||
const {
|
||||
isSuccessResponse,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
shouldRetry,
|
||||
} = useEmitResponse();
|
||||
const {
|
||||
checkoutNotices,
|
||||
paymentNotices,
|
||||
expressPaymentNotices,
|
||||
} = useCheckoutNotices();
|
||||
|
||||
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
|
||||
const currentObservers = useRef( observers );
|
||||
const {
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
onCheckoutValidationBeforeProcessing,
|
||||
} = useEventEmitters( observerDispatch );
|
||||
|
||||
// set observers on ref so it's always current.
|
||||
useEffect( () => {
|
||||
currentObservers.current = observers;
|
||||
}, [ observers ] );
|
||||
|
||||
/**
|
||||
* @deprecated use onCheckoutValidationBeforeProcessing instead
|
||||
*
|
||||
* To prevent the deprecation message being shown at render time
|
||||
* we need an extra function between useMemo and event emitters
|
||||
* so that the deprecated message gets shown only at invocation time.
|
||||
* (useMemo calls the passed function at render time)
|
||||
* See: https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4039/commits/a502d1be8828848270993264c64220731b0ae181
|
||||
*/
|
||||
const onCheckoutBeforeProcessing = useMemo( () => {
|
||||
return function (
|
||||
...args: Parameters< typeof onCheckoutValidationBeforeProcessing >
|
||||
) {
|
||||
deprecated( 'onCheckoutBeforeProcessing', {
|
||||
alternative: 'onCheckoutValidationBeforeProcessing',
|
||||
plugin: 'WooCommerce Blocks',
|
||||
} );
|
||||
return onCheckoutValidationBeforeProcessing( ...args );
|
||||
};
|
||||
}, [ onCheckoutValidationBeforeProcessing ] );
|
||||
|
||||
const dispatchActions = useMemo(
|
||||
(): CheckoutStateDispatchActions => ( {
|
||||
resetCheckout: () => void dispatch( actions.setPristine() ),
|
||||
setRedirectUrl: ( url ) =>
|
||||
void dispatch( actions.setRedirectUrl( url ) ),
|
||||
setHasError: ( hasError ) =>
|
||||
void dispatch( actions.setHasError( hasError ) ),
|
||||
incrementCalculating: () =>
|
||||
void dispatch( actions.incrementCalculating() ),
|
||||
decrementCalculating: () =>
|
||||
void dispatch( actions.decrementCalculating() ),
|
||||
setCustomerId: ( id ) =>
|
||||
void dispatch( actions.setCustomerId( id ) ),
|
||||
setOrderId: ( orderId ) =>
|
||||
void dispatch( actions.setOrderId( orderId ) ),
|
||||
setOrderNotes: ( orderNotes ) =>
|
||||
void dispatch( actions.setOrderNotes( orderNotes ) ),
|
||||
setExtensionData: ( extensionData ) =>
|
||||
void dispatch( actions.setExtensionData( extensionData ) ),
|
||||
setAfterProcessing: ( response ) => {
|
||||
const paymentResult = getPaymentResultFromCheckoutResponse(
|
||||
response
|
||||
);
|
||||
|
||||
if ( paymentResult.redirectUrl ) {
|
||||
dispatch(
|
||||
actions.setRedirectUrl( paymentResult.redirectUrl )
|
||||
);
|
||||
}
|
||||
dispatch( actions.setProcessingResponse( paymentResult ) );
|
||||
dispatch( actions.setAfterProcessing() );
|
||||
},
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
// emit events.
|
||||
useEffect( () => {
|
||||
const status = checkoutState.status;
|
||||
if ( status === STATUS.BEFORE_PROCESSING ) {
|
||||
removeNotices( 'error' );
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
|
||||
{}
|
||||
).then( ( response ) => {
|
||||
if ( response !== true ) {
|
||||
if ( Array.isArray( response ) ) {
|
||||
response.forEach(
|
||||
( { errorMessage, validationErrors } ) => {
|
||||
addErrorNotice( errorMessage );
|
||||
setValidationErrors( validationErrors );
|
||||
}
|
||||
);
|
||||
}
|
||||
dispatch( actions.setIdle() );
|
||||
dispatch( actions.setHasError() );
|
||||
} else {
|
||||
dispatch( actions.setProcessing() );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}, [
|
||||
checkoutState.status,
|
||||
setValidationErrors,
|
||||
addErrorNotice,
|
||||
removeNotices,
|
||||
dispatch,
|
||||
] );
|
||||
|
||||
const previousStatus = usePrevious( checkoutState.status );
|
||||
const previousHasError = usePrevious( checkoutState.hasError );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
checkoutState.status === previousStatus &&
|
||||
checkoutState.hasError === previousHasError
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleErrorResponse = ( observerResponses: unknown[] ) => {
|
||||
let errorResponse = null;
|
||||
observerResponses.forEach( ( response ) => {
|
||||
if (
|
||||
isErrorResponse( response ) ||
|
||||
isFailResponse( response )
|
||||
) {
|
||||
if ( response.message ) {
|
||||
const errorOptions = response.messageContext
|
||||
? { context: response.messageContext }
|
||||
: undefined;
|
||||
errorResponse = response;
|
||||
addErrorNotice( response.message, errorOptions );
|
||||
}
|
||||
}
|
||||
} );
|
||||
return errorResponse;
|
||||
};
|
||||
|
||||
if ( checkoutState.status === STATUS.AFTER_PROCESSING ) {
|
||||
const data = {
|
||||
redirectUrl: checkoutState.redirectUrl,
|
||||
orderId: checkoutState.orderId,
|
||||
customerId: checkoutState.customerId,
|
||||
orderNotes: checkoutState.orderNotes,
|
||||
processingResponse: checkoutState.processingResponse,
|
||||
};
|
||||
if ( checkoutState.hasError ) {
|
||||
// allow payment methods or other things to customize the error
|
||||
// with a fallback if nothing customizes it.
|
||||
emitEventWithAbort(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
data
|
||||
).then( ( observerResponses ) => {
|
||||
const errorResponse = handleErrorResponse(
|
||||
observerResponses
|
||||
);
|
||||
if ( errorResponse !== null ) {
|
||||
// irrecoverable error so set complete
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
dispatch( actions.setComplete( errorResponse ) );
|
||||
} else {
|
||||
dispatch( actions.setIdle() );
|
||||
}
|
||||
} else {
|
||||
const hasErrorNotices =
|
||||
checkoutNotices.some(
|
||||
( notice: { status: string } ) =>
|
||||
notice.status === 'error'
|
||||
) ||
|
||||
expressPaymentNotices.some(
|
||||
( notice: { status: string } ) =>
|
||||
notice.status === 'error'
|
||||
) ||
|
||||
paymentNotices.some(
|
||||
( notice: { status: string } ) =>
|
||||
notice.status === 'error'
|
||||
);
|
||||
if ( ! hasErrorNotices ) {
|
||||
// no error handling in place by anything so let's fall
|
||||
// back to default
|
||||
const message =
|
||||
data.processingResponse?.message ||
|
||||
__(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
addErrorNotice( message, {
|
||||
id: 'checkout',
|
||||
} );
|
||||
}
|
||||
|
||||
dispatch( actions.setIdle() );
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
emitEventWithAbort(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
data
|
||||
).then( ( observerResponses: unknown[] ) => {
|
||||
let successResponse = null as null | Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
let errorResponse = null as null | Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
observerResponses.forEach( ( response ) => {
|
||||
if ( isSuccessResponse( response ) ) {
|
||||
// the last observer response always "wins" for success.
|
||||
successResponse = response;
|
||||
}
|
||||
if (
|
||||
isErrorResponse( response ) ||
|
||||
isFailResponse( response )
|
||||
) {
|
||||
errorResponse = response;
|
||||
}
|
||||
} );
|
||||
|
||||
if ( successResponse && ! errorResponse ) {
|
||||
dispatch( actions.setComplete( successResponse ) );
|
||||
} else if ( isObject( errorResponse ) ) {
|
||||
if ( errorResponse.message ) {
|
||||
const errorOptions = errorResponse.messageContext
|
||||
? { context: errorResponse.messageContext }
|
||||
: undefined;
|
||||
addErrorNotice(
|
||||
errorResponse.message,
|
||||
errorOptions
|
||||
);
|
||||
}
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
dispatch( actions.setComplete( errorResponse ) );
|
||||
} else {
|
||||
// this will set an error which will end up
|
||||
// triggering the onCheckoutAfterProcessingWithError emitter.
|
||||
// and then setting checkout to IDLE state.
|
||||
dispatch( actions.setHasError( true ) );
|
||||
}
|
||||
} else {
|
||||
// nothing hooked in had any response type so let's just
|
||||
// consider successful
|
||||
dispatch( actions.setComplete() );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
checkoutState.status,
|
||||
checkoutState.hasError,
|
||||
checkoutState.redirectUrl,
|
||||
checkoutState.orderId,
|
||||
checkoutState.customerId,
|
||||
checkoutState.orderNotes,
|
||||
checkoutState.processingResponse,
|
||||
previousStatus,
|
||||
previousHasError,
|
||||
dispatchActions,
|
||||
addErrorNotice,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
isSuccessResponse,
|
||||
shouldRetry,
|
||||
checkoutNotices,
|
||||
expressPaymentNotices,
|
||||
paymentNotices,
|
||||
] );
|
||||
|
||||
const onSubmit = useCallback( () => {
|
||||
dispatchCheckoutEvent( 'submit' );
|
||||
dispatch( actions.setBeforeProcessing() );
|
||||
}, [ dispatchCheckoutEvent ] );
|
||||
|
||||
const checkoutData: CheckoutStateContextType = {
|
||||
onSubmit,
|
||||
isComplete: checkoutState.status === STATUS.COMPLETE,
|
||||
isIdle: checkoutState.status === STATUS.IDLE,
|
||||
isCalculating,
|
||||
isProcessing: checkoutState.status === STATUS.PROCESSING,
|
||||
isBeforeProcessing: checkoutState.status === STATUS.BEFORE_PROCESSING,
|
||||
isAfterProcessing: checkoutState.status === STATUS.AFTER_PROCESSING,
|
||||
hasError: checkoutState.hasError,
|
||||
redirectUrl: checkoutState.redirectUrl,
|
||||
onCheckoutBeforeProcessing,
|
||||
onCheckoutValidationBeforeProcessing,
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
dispatchActions,
|
||||
isCart,
|
||||
orderId: checkoutState.orderId,
|
||||
hasOrder: !! checkoutState.orderId,
|
||||
customerId: checkoutState.customerId,
|
||||
orderNotes: checkoutState.orderNotes,
|
||||
shouldCreateAccount: checkoutState.shouldCreateAccount,
|
||||
setShouldCreateAccount: ( value ) =>
|
||||
dispatch( actions.setShouldCreateAccount( value ) ),
|
||||
extensionData: checkoutState.extensionData,
|
||||
};
|
||||
return (
|
||||
<CheckoutContext.Provider value={ checkoutData }>
|
||||
{ children }
|
||||
</CheckoutContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { DEFAULT_STATE, STATUS } from './constants';
|
||||
import { ActionType, ACTION } from './actions';
|
||||
import type { CheckoutStateContextState, PaymentResultDataType } from './types';
|
||||
|
||||
/**
|
||||
* Reducer for the checkout state
|
||||
*/
|
||||
export const reducer = (
|
||||
state = DEFAULT_STATE,
|
||||
{
|
||||
redirectUrl,
|
||||
type,
|
||||
customerId,
|
||||
orderId,
|
||||
orderNotes,
|
||||
extensionData,
|
||||
shouldCreateAccount,
|
||||
data,
|
||||
}: ActionType
|
||||
): CheckoutStateContextState => {
|
||||
let newState = state;
|
||||
switch ( type ) {
|
||||
case ACTION.SET_PRISTINE:
|
||||
newState = DEFAULT_STATE;
|
||||
break;
|
||||
case ACTION.SET_IDLE:
|
||||
newState =
|
||||
state.status !== STATUS.IDLE
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.IDLE,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_REDIRECT_URL:
|
||||
newState =
|
||||
redirectUrl !== undefined && redirectUrl !== state.redirectUrl
|
||||
? {
|
||||
...state,
|
||||
redirectUrl,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_PROCESSING_RESPONSE:
|
||||
newState = {
|
||||
...state,
|
||||
processingResponse: data as PaymentResultDataType,
|
||||
};
|
||||
break;
|
||||
|
||||
case ACTION.SET_COMPLETE:
|
||||
newState =
|
||||
state.status !== STATUS.COMPLETE
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.COMPLETE,
|
||||
// @todo Investigate why redirectURL could be non-truthy and whether this would cause a bug if multiple gateways were used for payment e.g. 1st set the redirect URL but failed, and then the 2nd did not provide a redirect URL and succeeded.
|
||||
redirectUrl:
|
||||
data !== undefined &&
|
||||
typeof data.redirectUrl === 'string' &&
|
||||
data.redirectUrl
|
||||
? data.redirectUrl
|
||||
: state.redirectUrl,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_PROCESSING:
|
||||
newState =
|
||||
state.status !== STATUS.PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.PROCESSING,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
// clear any error state.
|
||||
newState =
|
||||
newState.hasError === false
|
||||
? newState
|
||||
: { ...newState, hasError: false };
|
||||
break;
|
||||
case ACTION.SET_BEFORE_PROCESSING:
|
||||
newState =
|
||||
state.status !== STATUS.BEFORE_PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.BEFORE_PROCESSING,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_AFTER_PROCESSING:
|
||||
newState =
|
||||
state.status !== STATUS.AFTER_PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.AFTER_PROCESSING,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_HAS_ERROR:
|
||||
newState = state.hasError
|
||||
? state
|
||||
: {
|
||||
...state,
|
||||
hasError: true,
|
||||
};
|
||||
newState =
|
||||
state.status === STATUS.PROCESSING ||
|
||||
state.status === STATUS.BEFORE_PROCESSING
|
||||
? {
|
||||
...newState,
|
||||
status: STATUS.IDLE,
|
||||
}
|
||||
: newState;
|
||||
break;
|
||||
case ACTION.SET_NO_ERROR:
|
||||
newState = state.hasError
|
||||
? {
|
||||
...state,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.INCREMENT_CALCULATING:
|
||||
newState = {
|
||||
...state,
|
||||
calculatingCount: state.calculatingCount + 1,
|
||||
};
|
||||
break;
|
||||
case ACTION.DECREMENT_CALCULATING:
|
||||
newState = {
|
||||
...state,
|
||||
calculatingCount: Math.max( 0, state.calculatingCount - 1 ),
|
||||
};
|
||||
break;
|
||||
case ACTION.SET_CUSTOMER_ID:
|
||||
newState =
|
||||
customerId !== undefined
|
||||
? {
|
||||
...state,
|
||||
customerId,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_ORDER_ID:
|
||||
newState =
|
||||
orderId !== undefined
|
||||
? {
|
||||
...state,
|
||||
orderId,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_SHOULD_CREATE_ACCOUNT:
|
||||
if (
|
||||
shouldCreateAccount !== undefined &&
|
||||
shouldCreateAccount !== state.shouldCreateAccount
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
shouldCreateAccount,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case ACTION.SET_ORDER_NOTES:
|
||||
if ( orderNotes !== undefined && state.orderNotes !== orderNotes ) {
|
||||
newState = {
|
||||
...state,
|
||||
orderNotes,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case ACTION.SET_EXTENSION_DATA:
|
||||
if (
|
||||
extensionData !== undefined &&
|
||||
state.extensionData !== extensionData
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
extensionData,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
// automatically update state to idle from pristine as soon as it
|
||||
// initially changes.
|
||||
if (
|
||||
newState !== state &&
|
||||
type !== ACTION.SET_PRISTINE &&
|
||||
newState.status === STATUS.PRISTINE
|
||||
) {
|
||||
newState.status = STATUS.IDLE;
|
||||
}
|
||||
return newState;
|
||||
};
|
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STATUS } from './constants';
|
||||
import type { emitterCallback } from '../../../event-emit';
|
||||
|
||||
export interface CheckoutResponseError {
|
||||
code: string;
|
||||
message: string;
|
||||
data: {
|
||||
status: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CheckoutResponseSuccess {
|
||||
// eslint-disable-next-line camelcase
|
||||
payment_result: {
|
||||
// eslint-disable-next-line camelcase
|
||||
payment_status: 'success' | 'failure' | 'pending' | 'error';
|
||||
// eslint-disable-next-line camelcase
|
||||
payment_details: Record< string, string > | Record< string, never >;
|
||||
// eslint-disable-next-line camelcase
|
||||
redirect_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CheckoutResponse = CheckoutResponseSuccess | CheckoutResponseError;
|
||||
|
||||
export interface PaymentResultDataType {
|
||||
message: string;
|
||||
paymentStatus: string;
|
||||
paymentDetails: Record< string, string > | Record< string, never >;
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
type extensionDataNamespace = string;
|
||||
type extensionDataItem = Record< string, unknown >;
|
||||
export type extensionData = Record< extensionDataNamespace, extensionDataItem >;
|
||||
|
||||
export interface CheckoutStateContextState {
|
||||
redirectUrl: string;
|
||||
status: STATUS;
|
||||
hasError: boolean;
|
||||
calculatingCount: number;
|
||||
orderId: number;
|
||||
orderNotes: string;
|
||||
customerId: number;
|
||||
shouldCreateAccount: boolean;
|
||||
processingResponse: PaymentResultDataType | null;
|
||||
extensionData: extensionData;
|
||||
}
|
||||
|
||||
export type CheckoutStateDispatchActions = {
|
||||
resetCheckout: () => void;
|
||||
setRedirectUrl: ( url: string ) => void;
|
||||
setHasError: ( hasError: boolean ) => void;
|
||||
setAfterProcessing: ( response: CheckoutResponse ) => void;
|
||||
incrementCalculating: () => void;
|
||||
decrementCalculating: () => void;
|
||||
setCustomerId: ( id: number ) => void;
|
||||
setOrderId: ( id: number ) => void;
|
||||
setOrderNotes: ( orderNotes: string ) => void;
|
||||
setExtensionData: ( extensionData: extensionData ) => void;
|
||||
};
|
||||
|
||||
export type CheckoutStateContextType = {
|
||||
// Dispatch actions to the checkout provider.
|
||||
dispatchActions: CheckoutStateDispatchActions;
|
||||
// Submits the checkout and begins processing.
|
||||
onSubmit: () => void;
|
||||
// True when checkout is complete and ready for redirect.
|
||||
isComplete: boolean;
|
||||
// True when the checkout state has changed and checkout has no activity.
|
||||
isIdle: boolean;
|
||||
// True when something in the checkout is resulting in totals being calculated.
|
||||
isCalculating: boolean;
|
||||
// True when checkout has been submitted and is being processed. Note, payment related processing happens during this state. When payment status is success, processing happens on the server.
|
||||
isProcessing: boolean;
|
||||
// True during any observers executing logic before checkout processing (eg. validation).
|
||||
isBeforeProcessing: boolean;
|
||||
// True when checkout status is AFTER_PROCESSING.
|
||||
isAfterProcessing: boolean;
|
||||
// Used to register a callback that will fire after checkout has been processed and there are no errors.
|
||||
onCheckoutAfterProcessingWithSuccess: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback that will fire when the checkout has been processed and has an error.
|
||||
onCheckoutAfterProcessingWithError: ReturnType< typeof emitterCallback >;
|
||||
// Deprecated in favour of onCheckoutValidationBeforeProcessing.
|
||||
onCheckoutBeforeProcessing: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback that will fire when the checkout has been submitted before being sent off to the server.
|
||||
onCheckoutValidationBeforeProcessing: ReturnType< typeof emitterCallback >;
|
||||
// Set if user account should be created.
|
||||
setShouldCreateAccount: ( shouldCreateAccount: boolean ) => void;
|
||||
// True when the checkout has a draft order from the API.
|
||||
hasOrder: boolean;
|
||||
// When true, means the provider is providing data for the cart.
|
||||
isCart: boolean;
|
||||
// True when the checkout is in an error state. Whatever caused the error (validation/payment method) will likely have triggered a notice.
|
||||
hasError: CheckoutStateContextState[ 'hasError' ];
|
||||
// This is the url that checkout will redirect to when it's ready.
|
||||
redirectUrl: CheckoutStateContextState[ 'redirectUrl' ];
|
||||
// This is the ID for the draft order if one exists.
|
||||
orderId: CheckoutStateContextState[ 'orderId' ];
|
||||
// Order notes introduced by the user in the checkout form.
|
||||
orderNotes: CheckoutStateContextState[ 'orderNotes' ];
|
||||
// This is the ID of the customer the draft order belongs to.
|
||||
customerId: CheckoutStateContextState[ 'customerId' ];
|
||||
// Should a user account be created?
|
||||
shouldCreateAccount: CheckoutStateContextState[ 'shouldCreateAccount' ];
|
||||
// Custom checkout data passed to the store API on processing.
|
||||
extensionData: CheckoutStateContextState[ 'extensionData' ];
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { PaymentResultDataType, CheckoutResponse } from './types';
|
||||
|
||||
/**
|
||||
* Prepares the payment_result data from the server checkout endpoint response.
|
||||
*/
|
||||
export const getPaymentResultFromCheckoutResponse = (
|
||||
response: CheckoutResponse
|
||||
): PaymentResultDataType => {
|
||||
const paymentResult = {
|
||||
message: '',
|
||||
paymentStatus: '',
|
||||
redirectUrl: '',
|
||||
paymentDetails: {},
|
||||
} as PaymentResultDataType;
|
||||
|
||||
// payment_result is present in successful responses.
|
||||
if ( 'payment_result' in response ) {
|
||||
paymentResult.paymentStatus = response.payment_result.payment_status;
|
||||
paymentResult.redirectUrl = response.payment_result.redirect_url;
|
||||
|
||||
if (
|
||||
response.payment_result.hasOwnProperty( 'payment_details' ) &&
|
||||
Array.isArray( response.payment_result.payment_details )
|
||||
) {
|
||||
response.payment_result.payment_details.forEach(
|
||||
( { key, value }: { key: string; value: string } ) => {
|
||||
paymentResult.paymentDetails[ key ] = decodeEntities(
|
||||
value
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// message is present in error responses.
|
||||
if ( 'message' in response ) {
|
||||
paymentResult.message = decodeEntities( response.message );
|
||||
}
|
||||
|
||||
// If there was an error code but no message, set a default message.
|
||||
if (
|
||||
! paymentResult.message &&
|
||||
'data' in response &&
|
||||
'status' in response.data &&
|
||||
response.data.status > 299
|
||||
) {
|
||||
paymentResult.message = __(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
}
|
||||
|
||||
return paymentResult;
|
||||
};
|
Reference in New Issue
Block a user