From 5f84e7dabbc788b769646a9bcba5a15a57b03e33 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 18 May 2021 10:37:31 +0100 Subject: [PATCH] Convert checkout state context provider to Typescript (#4200) * git move to ts files * Type the checkout state provider * Restore for loop for error handling * Types not needed * Consolodate response handling * Unused import * Fix defaults for onCheckoutAfterProcessingWithSuccess etc * Type useEmitResponse and remove isObject checks * useEmitResponse as const * Check that redirectUrl is string * Define actions as const * data.redirectUrl should be truthy * Add redirectURL todo item as followup * remove null fallback --- .../base/context/hooks/use-emit-response.js | 59 ----- .../base/context/hooks/use-emit-response.ts | 66 +++++ .../cart-checkout/checkout-state/actions.js | 82 ------ .../cart-checkout/checkout-state/actions.ts | 110 ++++++++ .../cart-checkout/checkout-state/constants.js | 53 ---- .../cart-checkout/checkout-state/constants.ts | 84 +++++++ .../checkout-state/event-emit.js | 48 ---- .../checkout-state/event-emit.ts | 62 +++++ .../checkout-state/{index.js => index.tsx} | 200 ++++++--------- .../cart-checkout/checkout-state/reducer.js | 235 ------------------ .../cart-checkout/checkout-state/reducer.ts | 197 +++++++++++++++ .../cart-checkout/checkout-state/types.ts | 103 ++++++++ .../cart-checkout/checkout-state/utils.ts | 63 +++++ assets/js/type-defs/checkout.js | 26 -- .../extensibility/checkout-flow-and-events.md | 4 +- package-lock.json | 6 + package.json | 1 + 17 files changed, 770 insertions(+), 629 deletions(-) delete mode 100644 assets/js/base/context/hooks/use-emit-response.js create mode 100644 assets/js/base/context/hooks/use-emit-response.ts delete mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/actions.js create mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts delete mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/constants.js create mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts delete mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.js create mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.ts rename assets/js/base/context/providers/cart-checkout/checkout-state/{index.js => index.tsx} (72%) delete mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/reducer.js create mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/reducer.ts create mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/types.ts create mode 100644 assets/js/base/context/providers/cart-checkout/checkout-state/utils.ts delete mode 100644 assets/js/type-defs/checkout.js diff --git a/assets/js/base/context/hooks/use-emit-response.js b/assets/js/base/context/hooks/use-emit-response.js deleted file mode 100644 index 3b43c05121d..00000000000 --- a/assets/js/base/context/hooks/use-emit-response.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @typedef {import('@woocommerce/type-defs/hooks').EmitResponseTypes} EmitResponseTypes - * @typedef {import('@woocommerce/type-defs/hooks').NoticeContexts} NoticeContexts - * @typedef {import('@woocommerce/type-defs/hooks').EmitResponseApi} EmitResponseApi - */ - -const isResponseOf = ( response, type ) => { - return !! response.type && response.type === type; -}; - -/** - * @type {EmitResponseTypes} - */ -const responseTypes = { - SUCCESS: 'success', - FAIL: 'failure', - ERROR: 'error', -}; - -/** - * @type {NoticeContexts} - */ -const noticeContexts = { - PAYMENTS: 'wc/payment-area', - EXPRESS_PAYMENTS: 'wc/express-payment-area', -}; - -export const isSuccessResponse = ( response ) => { - return isResponseOf( response, responseTypes.SUCCESS ); -}; - -export const isErrorResponse = ( response ) => { - return isResponseOf( response, responseTypes.ERROR ); -}; - -export const isFailResponse = ( response ) => { - return isResponseOf( response, responseTypes.FAIL ); -}; - -export const shouldRetry = ( response ) => { - return typeof response.retry === 'undefined' || response.retry === true; -}; - -/** - * A custom hook exposing response utilities for emitters. - * - * @return {EmitResponseApi} Various interfaces for validating and implementing - * emitter response properties. - */ -export const useEmitResponse = () => { - return { - responseTypes, - noticeContexts, - shouldRetry, - isSuccessResponse, - isErrorResponse, - isFailResponse, - }; -}; diff --git a/assets/js/base/context/hooks/use-emit-response.ts b/assets/js/base/context/hooks/use-emit-response.ts new file mode 100644 index 00000000000..a57c7492f31 --- /dev/null +++ b/assets/js/base/context/hooks/use-emit-response.ts @@ -0,0 +1,66 @@ +/** + * Internal dependencies + */ +import { isObject } from '../../utils/type-guards'; + +export enum responseTypes { + SUCCESS = 'success', + FAIL = 'failure', + ERROR = 'error', +} + +export enum noticeContexts { + PAYMENTS = 'wc/payment-area', + EXPRESS_PAYMENTS = 'wc/express-payment-area', +} + +export interface ResponseType extends Record< string, unknown > { + type: responseTypes; + retry?: boolean; +} + +const isResponseOf = ( + response: unknown, + type: string +): response is ResponseType => { + return isObject( response ) && 'type' in response && response.type === type; +}; + +export const isSuccessResponse = ( + response: unknown +): response is ResponseType => { + return isResponseOf( response, responseTypes.SUCCESS ); +}; + +export const isErrorResponse = ( + response: unknown +): response is ResponseType => { + return isResponseOf( response, responseTypes.ERROR ); +}; + +export const isFailResponse = ( + response: unknown +): response is ResponseType => { + return isResponseOf( response, responseTypes.FAIL ); +}; + +export const shouldRetry = ( response: unknown ): boolean => { + return ( + ! isObject( response ) || + typeof response.retry === 'undefined' || + response.retry === true + ); +}; + +/** + * A custom hook exposing response utilities for emitters. + */ +export const useEmitResponse = () => + ( { + responseTypes, + noticeContexts, + shouldRetry, + isSuccessResponse, + isErrorResponse, + isFailResponse, + } as const ); diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/actions.js b/assets/js/base/context/providers/cart-checkout/checkout-state/actions.js deleted file mode 100644 index 1417db1f3c0..00000000000 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/actions.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Internal dependencies - */ -import { TYPES } from './constants'; - -const { - SET_PRISTINE, - SET_IDLE, - SET_PROCESSING, - SET_BEFORE_PROCESSING, - SET_AFTER_PROCESSING, - SET_PROCESSING_RESPONSE, - SET_REDIRECT_URL, - SET_COMPLETE, - SET_HAS_ERROR, - SET_NO_ERROR, - INCREMENT_CALCULATING, - DECREMENT_CALCULATING, - SET_CUSTOMER_ID, - SET_ORDER_ID, - SET_SHOULD_CREATE_ACCOUNT, - SET_ORDER_NOTES, -} = TYPES; - -/** - * All the actions that can be dispatched for the checkout. - */ -export const actions = { - setPristine: () => ( { - type: SET_PRISTINE, - } ), - setIdle: () => ( { - type: SET_IDLE, - } ), - setProcessing: () => ( { - type: SET_PROCESSING, - } ), - setRedirectUrl: ( url ) => ( { - type: SET_REDIRECT_URL, - url, - } ), - setProcessingResponse: ( data ) => ( { - type: SET_PROCESSING_RESPONSE, - data, - } ), - setComplete: ( data ) => ( { - type: SET_COMPLETE, - data, - } ), - setBeforeProcessing: () => ( { - type: SET_BEFORE_PROCESSING, - } ), - setAfterProcessing: () => ( { - type: SET_AFTER_PROCESSING, - } ), - setHasError: ( hasError = true ) => { - const type = hasError ? SET_HAS_ERROR : SET_NO_ERROR; - return { type }; - }, - incrementCalculating: () => ( { - type: INCREMENT_CALCULATING, - } ), - decrementCalculating: () => ( { - type: DECREMENT_CALCULATING, - } ), - setCustomerId: ( customerId ) => ( { - type: SET_CUSTOMER_ID, - customerId, - } ), - setOrderId: ( orderId ) => ( { - type: SET_ORDER_ID, - orderId, - } ), - setShouldCreateAccount: ( shouldCreateAccount ) => ( { - type: SET_SHOULD_CREATE_ACCOUNT, - shouldCreateAccount, - } ), - setOrderNotes: ( orderNotes ) => ( { - type: SET_ORDER_NOTES, - orderNotes, - } ), -}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts b/assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts new file mode 100644 index 00000000000..4c292ff2369 --- /dev/null +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts @@ -0,0 +1,110 @@ +/** + * Internal dependencies + */ +import type { PaymentResultDataType } 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', +} + +export interface ActionType { + type: ACTION; + data?: + | Record< string, unknown > + | Record< string, never > + | PaymentResultDataType; + url?: string; + customerId?: number; + orderId?: number; + shouldCreateAccount?: boolean; + hasError?: boolean; + orderNotes?: string; +} + +/** + * 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: ( url: string ) => + ( { + type: ACTION.SET_REDIRECT_URL, + url, + } 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 ), +}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/constants.js b/assets/js/base/context/providers/cart-checkout/checkout-state/constants.js deleted file mode 100644 index fc2d80a6568..00000000000 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/constants.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * External dependencies - */ -import { getSetting } from '@woocommerce/settings'; - -/** - * @type {import("@woocommerce/type-defs/checkout").CheckoutStatusConstants} - */ -export const STATUS = { - PRISTINE: 'pristine', - IDLE: 'idle', - PROCESSING: 'processing', - COMPLETE: 'complete', - BEFORE_PROCESSING: 'before_processing', - AFTER_PROCESSING: 'after_processing', -}; - -const preloadedApiRequests = getSetting( 'preloadedApiRequests', {} ); -const checkoutData = { - order_id: 0, - customer_id: 0, - ...( preloadedApiRequests[ '/wc/store/checkout' ]?.body || {} ), -}; - -export const DEFAULT_STATE = { - redirectUrl: '', - status: STATUS.PRISTINE, - hasError: false, - calculatingCount: 0, - orderId: checkoutData.order_id, - orderNotes: '', - customerId: checkoutData.customer_id, - shouldCreateAccount: false, - processingResponse: null, -}; - -export const TYPES = { - 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', -}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts b/assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts new file mode 100644 index 00000000000..d038e2cd098 --- /dev/null +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts @@ -0,0 +1,84 @@ +/** + * 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, + }, + 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, +}; + +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, +}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.js b/assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.js deleted file mode 100644 index 501f2bc33d6..00000000000 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Internal dependencies - */ -import { - emitterCallback, - reducer, - emitEvent, - emitEventWithAbort, -} 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', -}; - -/** - * Receives a reducer dispatcher and returns an object with the - * 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 checkout emmitter registration - */ -const emitterObservers = ( dispatcher ) => ( { - onCheckoutAfterProcessingWithSuccess: emitterCallback( - EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS, - dispatcher - ), - onCheckoutAfterProcessingWithError: emitterCallback( - EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR, - dispatcher - ), - onCheckoutValidationBeforeProcessing: emitterCallback( - EMIT_TYPES.CHECKOUT_VALIDATION_BEFORE_PROCESSING, - dispatcher - ), -} ); - -export { EMIT_TYPES, emitterObservers, reducer, emitEvent, emitEventWithAbort }; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.ts b/assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.ts new file mode 100644 index 00000000000..36bebeb2a0b --- /dev/null +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/event-emit.ts @@ -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 }; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/index.js b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx similarity index 72% rename from assets/js/base/context/providers/cart-checkout/checkout-state/index.js rename to assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx index 2b95b1c41d5..0b97d95e26f 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/index.js +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx @@ -13,15 +13,25 @@ import { import { __ } from '@wordpress/i18n'; import { usePrevious } from '@woocommerce/base-hooks'; import deprecated from '@wordpress/deprecated'; + /** * Internal dependencies */ import { actions } from './actions'; -import { reducer, prepareResponseData } from './reducer'; -import { DEFAULT_STATE, STATUS } from './constants'; +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, - emitterObservers, + useEventEmitters, emitEvent, emitEventWithAbort, reducer as emitReducer, @@ -31,51 +41,15 @@ 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'; +import { isObject } from '../../../../utils/type-guards'; /** - * @typedef {import('@woocommerce/type-defs/checkout').CheckoutDispatchActions} CheckoutDispatchActions * @typedef {import('@woocommerce/type-defs/contexts').CheckoutDataContext} CheckoutDataContext */ -const CheckoutContext = createContext( { - isComplete: false, - isIdle: false, - isCalculating: false, - isProcessing: false, - isBeforeProcessing: false, - isAfterProcessing: false, - hasError: false, - redirectUrl: '', - orderId: 0, - orderNotes: '', - customerId: 0, - onSubmit: () => void null, - // deprecated for onCheckoutValidationBeforeProcessing - onCheckoutBeforeProcessing: ( callback ) => void callback, - onCheckoutValidationBeforeProcessing: ( callback ) => void callback, - onCheckoutAfterProcessingWithSuccess: ( callback ) => void callback, - onCheckoutAfterProcessingWithError: ( callback ) => void callback, - 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, - }, - hasOrder: false, - isCart: false, - shouldCreateAccount: false, - setShouldCreateAccount: ( value ) => void value, -} ); +const CheckoutContext = createContext( DEFAULT_CHECKOUT_STATE_DATA ); -/** - * @return {CheckoutDataContext} Returns the checkout data context value - */ -export const useCheckoutContext = () => { +export const useCheckoutContext = (): CheckoutStateContextType => { return useContext( CheckoutContext ); }; @@ -93,13 +67,15 @@ 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 [ observers, observerDispatch ] = useReducer( emitReducer, {} ); - const currentObservers = useRef( observers ); const { setValidationErrors } = useValidationContext(); const { addErrorNotice, removeNotices } = useStoreNotices(); const { dispatchCheckoutEvent } = useStoreEvents(); @@ -116,57 +92,42 @@ export const CheckoutStateProvider = ( { 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 emitterObservers + * 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( () => { - const callback = emitterObservers( observerDispatch ) - .onCheckoutValidationBeforeProcessing; - - return function ( ...args ) { + return function ( + ...args: Parameters< typeof onCheckoutValidationBeforeProcessing > + ) { deprecated( 'onCheckoutBeforeProcessing', { alternative: 'onCheckoutValidationBeforeProcessing', plugin: 'WooCommerce Blocks', } ); - - return callback( ...args ); + return onCheckoutValidationBeforeProcessing( ...args ); }; - }, [ observerDispatch ] ); + }, [ onCheckoutValidationBeforeProcessing ] ); - const onCheckoutValidationBeforeProcessing = useMemo( - () => - emitterObservers( observerDispatch ) - .onCheckoutValidationBeforeProcessing, - [ observerDispatch ] - ); - const onCheckoutAfterProcessingWithSuccess = useMemo( - () => - emitterObservers( observerDispatch ) - .onCheckoutAfterProcessingWithSuccess, - [ observerDispatch ] - ); - const onCheckoutAfterProcessingWithError = useMemo( - () => - emitterObservers( observerDispatch ) - .onCheckoutAfterProcessingWithError, - [ observerDispatch ] - ); - - /** - * @type {CheckoutDispatchActions} - */ const dispatchActions = useMemo( - () => ( { + (): CheckoutStateDispatchActions => ( { resetCheckout: () => void dispatch( actions.setPristine() ), setRedirectUrl: ( url ) => void dispatch( actions.setRedirectUrl( url ) ), @@ -183,35 +144,17 @@ export const CheckoutStateProvider = ( { setOrderNotes: ( orderNotes ) => void dispatch( actions.setOrderNotes( orderNotes ) ), setAfterProcessing: ( response ) => { - // capture general error message if this is an error response. - if ( - ! response.payment_result && - response.message && - response?.data?.status !== 200 - ) { - response.payment_result = { - ...response.payment_result, - message: response.message, - }; - } - if ( response.payment_result ) { - if ( - // eslint-disable-next-line camelcase - response.payment_result?.redirect_url - ) { - dispatch( - actions.setRedirectUrl( - response.payment_result.redirect_url - ) - ); - } + const paymentResult = getPaymentResultFromCheckoutResponse( + response + ); + + if ( paymentResult.redirectUrl ) { dispatch( - actions.setProcessingResponse( - prepareResponseData( response.payment_result ) - ) + actions.setRedirectUrl( paymentResult.redirectUrl ) ); } - void dispatch( actions.setAfterProcessing() ); + dispatch( actions.setProcessingResponse( paymentResult ) ); + dispatch( actions.setAfterProcessing() ); }, } ), [] @@ -261,20 +204,20 @@ export const CheckoutStateProvider = ( { return; } - const handleErrorResponse = ( observerResponses ) => { + const handleErrorResponse = ( observerResponses: unknown[] ) => { let errorResponse = null; observerResponses.forEach( ( response ) => { - const { message, messageContext } = response; if ( - ( isErrorResponse( response ) || - isFailResponse( response ) ) && - message + isErrorResponse( response ) || + isFailResponse( response ) ) { - const errorOptions = messageContext - ? { context: messageContext } - : undefined; - errorResponse = response; - addErrorNotice( message, errorOptions ); + if ( response.message ) { + const errorOptions = response.messageContext + ? { context: response.messageContext } + : undefined; + errorResponse = response; + addErrorNotice( response.message, errorOptions ); + } } } ); return errorResponse; @@ -285,7 +228,7 @@ export const CheckoutStateProvider = ( { redirectUrl: checkoutState.redirectUrl, orderId: checkoutState.orderId, customerId: checkoutState.customerId, - customerNote: checkoutState.customerNote, + orderNotes: checkoutState.orderNotes, processingResponse: checkoutState.processingResponse, }; if ( checkoutState.hasError ) { @@ -309,13 +252,16 @@ export const CheckoutStateProvider = ( { } else { const hasErrorNotices = checkoutNotices.some( - ( notice ) => notice.status === 'error' + ( notice: { status: string } ) => + notice.status === 'error' ) || expressPaymentNotices.some( - ( notice ) => notice.status === 'error' + ( notice: { status: string } ) => + notice.status === 'error' ) || paymentNotices.some( - ( notice ) => notice.status === 'error' + ( notice: { status: string } ) => + notice.status === 'error' ); if ( ! hasErrorNotices ) { // no error handling in place by anything so let's fall @@ -339,8 +285,16 @@ export const CheckoutStateProvider = ( { currentObservers.current, EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS, data - ).then( ( observerResponses ) => { - let successResponse, errorResponse; + ).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. @@ -353,9 +307,10 @@ export const CheckoutStateProvider = ( { errorResponse = response; } } ); + if ( successResponse && ! errorResponse ) { dispatch( actions.setComplete( successResponse ) ); - } else if ( errorResponse ) { + } else if ( isObject( errorResponse ) ) { if ( errorResponse.message ) { const errorOptions = errorResponse.messageContext ? { context: errorResponse.messageContext } @@ -387,7 +342,7 @@ export const CheckoutStateProvider = ( { checkoutState.redirectUrl, checkoutState.orderId, checkoutState.customerId, - checkoutState.customerNote, + checkoutState.orderNotes, checkoutState.processingResponse, previousStatus, previousHasError, @@ -407,10 +362,7 @@ export const CheckoutStateProvider = ( { dispatch( actions.setBeforeProcessing() ); }, [ dispatchCheckoutEvent ] ); - /** - * @type {CheckoutDataContext} - */ - const checkoutData = { + const checkoutData: CheckoutStateContextType = { onSubmit, isComplete: checkoutState.status === STATUS.COMPLETE, isIdle: checkoutState.status === STATUS.IDLE, diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.js b/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.js deleted file mode 100644 index e06b4407bd2..00000000000 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * External dependencies - */ -import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import { TYPES, DEFAULT_STATE, STATUS } from './constants'; - -const { - SET_PRISTINE, - SET_IDLE, - SET_PROCESSING, - SET_BEFORE_PROCESSING, - SET_AFTER_PROCESSING, - SET_PROCESSING_RESPONSE, - SET_REDIRECT_URL, - SET_COMPLETE, - SET_HAS_ERROR, - SET_NO_ERROR, - INCREMENT_CALCULATING, - DECREMENT_CALCULATING, - SET_CUSTOMER_ID, - SET_ORDER_ID, - SET_ORDER_NOTES, - SET_SHOULD_CREATE_ACCOUNT, -} = TYPES; - -const { - PRISTINE, - IDLE, - PROCESSING, - BEFORE_PROCESSING, - AFTER_PROCESSING, - COMPLETE, -} = STATUS; - -/** - * Prepares the payment_result data from the server checkout endpoint response. - * - * @param {Object} data The value of `payment_result` from the checkout - * processing endpoint response. - * @param {string} data.message If there was a general error message it will appear - * on this property. - * @param {string} data.payment_status The payment status. One of 'success', 'failure', - * 'pending', 'error'. - * @param {Array} data.payment_details An array of Objects with a 'key' property that is a - * string and value property that is a string. These are - * converted to a flat object where the key becomes the - * object property and value the property value. - * - * @return {Object} A new object with 'paymentStatus', and 'paymentDetails' as the properties. - */ -export const prepareResponseData = ( data ) => { - const responseData = { - message: data?.message || '', - paymentStatus: data.payment_status, - paymentDetails: {}, - }; - if ( Array.isArray( data.payment_details ) ) { - data.payment_details.forEach( ( { key, value } ) => { - responseData.paymentDetails[ key ] = decodeEntities( value ); - } ); - } - return responseData; -}; - -/** - * Reducer for the checkout state - * - * @param {Object} state Current state. - * @param {Object} action Incoming action object. - * @param {string} action.url URL passed in. - * @param {string} action.type Type of action. - * @param {string} action.customerId Customer ID. - * @param {string} action.orderId Order ID. - * @param {Array} action.orderNotes Order notes. - * @param {boolean} action.shouldCreateAccount True if shopper has requested a user account (signup checkbox). - * @param {Object} action.data Other action payload. - */ -export const reducer = ( - state = DEFAULT_STATE, - { url, type, customerId, orderId, orderNotes, shouldCreateAccount, data } -) => { - let newState = state; - switch ( type ) { - case SET_PRISTINE: - newState = DEFAULT_STATE; - break; - case SET_IDLE: - newState = - state.status !== IDLE - ? { - ...state, - status: IDLE, - } - : state; - break; - case SET_REDIRECT_URL: - newState = - url !== state.url - ? { - ...state, - redirectUrl: url, - } - : state; - break; - case SET_PROCESSING_RESPONSE: - newState = { - ...state, - processingResponse: data, - }; - break; - - case SET_COMPLETE: - newState = - state.status !== COMPLETE - ? { - ...state, - status: COMPLETE, - redirectUrl: data?.redirectUrl || state.redirectUrl, - } - : state; - break; - case SET_PROCESSING: - newState = - state.status !== PROCESSING - ? { - ...state, - status: PROCESSING, - hasError: false, - } - : state; - // clear any error state. - newState = - newState.hasError === false - ? newState - : { ...newState, hasError: false }; - break; - case SET_BEFORE_PROCESSING: - newState = - state.status !== BEFORE_PROCESSING - ? { - ...state, - status: BEFORE_PROCESSING, - hasError: false, - } - : state; - break; - case SET_AFTER_PROCESSING: - newState = - state.status !== AFTER_PROCESSING - ? { - ...state, - status: AFTER_PROCESSING, - } - : state; - break; - case SET_HAS_ERROR: - newState = state.hasError - ? state - : { - ...state, - hasError: true, - }; - newState = - state.status === PROCESSING || - state.status === BEFORE_PROCESSING - ? { - ...newState, - status: IDLE, - } - : newState; - break; - case SET_NO_ERROR: - newState = state.hasError - ? { - ...state, - hasError: false, - } - : state; - break; - case INCREMENT_CALCULATING: - newState = { - ...state, - calculatingCount: state.calculatingCount + 1, - }; - break; - case DECREMENT_CALCULATING: - newState = { - ...state, - calculatingCount: Math.max( 0, state.calculatingCount - 1 ), - }; - break; - case SET_CUSTOMER_ID: - newState = { - ...state, - customerId, - }; - break; - case SET_ORDER_ID: - newState = { - ...state, - orderId, - }; - break; - case SET_SHOULD_CREATE_ACCOUNT: - if ( shouldCreateAccount !== state.shouldCreateAccount ) { - newState = { - ...state, - shouldCreateAccount, - }; - } - break; - case SET_ORDER_NOTES: - if ( state.orderNotes !== orderNotes ) { - newState = { - ...state, - orderNotes, - }; - } - 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/providers/cart-checkout/checkout-state/reducer.ts b/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.ts new file mode 100644 index 00000000000..a6ddbc08053 --- /dev/null +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.ts @@ -0,0 +1,197 @@ +/** + * Internal dependencies + */ +import { DEFAULT_STATE, STATUS } from './constants'; +import { ActionType, ACTION } from './actions'; +import type { CheckoutStateContextState, PaymentResultDataType } from './types'; + +/** + * Reducer for the checkout state + * + * @param {Object} state Current state. + * @param {Object} action Incoming action object. + * @param {string} action.url URL passed in. + * @param {string} action.type Type of action. + * @param {string} action.customerId Customer ID. + * @param {string} action.orderId Order ID. + * @param {Array} action.orderNotes Order notes. + * @param {boolean} action.shouldCreateAccount True if shopper has requested a user account (sign-up checkbox). + * @param {Object} action.data Other action payload. + */ +export const reducer = ( + state = DEFAULT_STATE, + { + url, + type, + customerId, + orderId, + orderNotes, + 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 = + url !== undefined && url !== state.redirectUrl + ? { + ...state, + redirectUrl: url, + } + : 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; + } + // 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; +}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/types.ts b/assets/js/base/context/providers/cart-checkout/checkout-state/types.ts new file mode 100644 index 00000000000..b6d5bc9d85f --- /dev/null +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/types.ts @@ -0,0 +1,103 @@ +/** + * 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; +} + +export type CheckoutStateContextState = { + redirectUrl: string; + status: STATUS; + hasError: boolean; + calculatingCount: number; + orderId: number; + orderNotes: string; + customerId: number; + shouldCreateAccount: boolean; + processingResponse: PaymentResultDataType | null; +}; + +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; +}; + +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; + // True when the checkout is in an error state. Whatever caused the error (validation/payment method) will likely have triggered a notice. + hasError: boolean; + // This is the url that checkout will redirect to when it's ready. + redirectUrl: string; + // This is the ID for the draft order if one exists. + orderId: number; + // Order notes introduced by the user in the checkout form. + orderNotes: string; + // This is the ID of the customer the draft order belongs to. + customerId: number; + // 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 >; + // 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; + // Should a user account be created? + shouldCreateAccount: boolean; + // Set if user account should be created. + setShouldCreateAccount: ( shouldCreateAccount: boolean ) => void; +}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/utils.ts b/assets/js/base/context/providers/cart-checkout/checkout-state/utils.ts new file mode 100644 index 00000000000..56c74eeeca6 --- /dev/null +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/utils.ts @@ -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; +}; diff --git a/assets/js/type-defs/checkout.js b/assets/js/type-defs/checkout.js deleted file mode 100644 index 4bff107e180..00000000000 --- a/assets/js/type-defs/checkout.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @typedef {Object} CheckoutDispatchActions - * - * @property {function():void} resetCheckout Dispatches an action that resets the checkout to a pristine state. - * @property {function(string):void} setRedirectUrl Dispatches an action that sets the redirectUrl. - * @property {function(boolean=):void} setHasError Dispatches an action that sets the checkout status to having an error. - * @property {function(Object):void} setAfterProcessing Dispatches an action that sets the checkout status to after processing and also sets the response data accordingly. - * @property {function():void} incrementCalculating Dispatches an action that increments the calculating state for checkout by one. - * @property {function():void} decrementCalculating Dispatches an action that decrements the calculating state for checkout by one. - * @property {function(number|string):void} setOrderId Dispatches an action that stores the draft order ID and key. - * @property {function(string):void} setOrderNotes Dispatches an action that sets the order notes. - * @property {function(number):void} setCustomerId Dispatches an action that stores the customer ID. - */ - -/** - * @typedef {Object} CheckoutStatusConstants - * - * @property {string} PRISTINE Checkout is in it's initialized state. - * @property {string} IDLE When checkout state has changed but there is no activity happening. - * @property {string} BEFORE_PROCESSING This is the state before checkout processing begins after the checkout button has been pressed/submitted. - * @property {string} PROCESSING After BEFORE_PROCESSING status emitters have finished successfully. Payment processing is started on this checkout status. - * @property {string} AFTER_PROCESSING After server side checkout processing is completed this status is set. - * @property {string} COMPLETE After the AFTER_PROCESSING event emitters have completed. This status triggers the checkout redirect. - */ - -export {}; diff --git a/docs/extensibility/checkout-flow-and-events.md b/docs/extensibility/checkout-flow-and-events.md index e654da57a8e..9231a2d47c0 100644 --- a/docs/extensibility/checkout-flow-and-events.md +++ b/docs/extensibility/checkout-flow-and-events.md @@ -345,7 +345,7 @@ const onCheckoutProcessingData = { redirectUrl, orderId, customerId, - customerNote, + orderNotes, processingResponse, }; ``` @@ -355,7 +355,7 @@ The properties are: - `redirectUrl`: This is a string that is the url the checkout will redirect to as returned by the processing on the server. - `orderId`: Is the id of the current order being processed. - `customerId`: Is the id for the customer making the purchase (that is attached to the order). -- `customerNote`: This will be any custom note the customer left on the order. +- `orderNotes`: This will be any custom note the customer left on the order. - `processingResponse`: This is the value of [`payment_result` from the checkout response](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/src/RestApi/StoreApi/Schemas/CheckoutSchema.php#L103-L138). The data exposed on this object is (via the object properties): - `paymentStatus`: Whatever the status is for the payment after it was processed server side. Will be one of `success`, `failure`, `pending`, `error`. - `paymentDetails`: This will be an arbitrary object that contains any data the payment method processing server side sends back to the client in the checkout processing response. Payment methods are able to hook in on the processing server side and set this data for returning. diff --git a/package-lock.json b/package-lock.json index c79972fde36..74de73b5c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4300,6 +4300,12 @@ "@types/wordpress__data": "*" } }, + "@types/wordpress__deprecated": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/wordpress__deprecated/-/wordpress__deprecated-2.4.2.tgz", + "integrity": "sha512-ModELmWf8HCEQknjUfjZIrg9vFMmllJm/wc4/RYzBmsiTRSZc05MfAkYRTY7rlOcFrN1q6bc7ujDiegNKv+z9g==", + "dev": true + }, "@types/wordpress__element": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/wordpress__element/-/wordpress__element-2.4.1.tgz", diff --git a/package.json b/package.json index e568d3b9950..f87f11ec733 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@types/react": "16.14.6", "@types/wordpress__data": "4.6.9", "@types/wordpress__data-controls": "1.0.4", + "@types/wordpress__deprecated": "^2.4.2", "@types/wordpress__element": "2.4.1", "@types/wordpress__keycodes": "2.3.1", "@typescript-eslint/eslint-plugin": "4.14.1",