initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -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 ),
};

View File

@ -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: {},
};

View File

@ -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 };

View File

@ -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>
);
};

View File

@ -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;
};

View File

@ -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' ];
};

View File

@ -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;
};