Move checkout state code into thunks and rename `CheckoutState` context to `CheckoutEvents` (https://github.com/woocommerce/woocommerce-blocks/pull/6455)
* Add checkout data store * wip on checkout data store * CheckoutContext now uses the checkout store * Investigated and removed setting the redirectUrl on the default state * update extension and address hooks to use checkout data store * use checkout data store in checkout-processor and use-checkout-button * trim useCheckoutContext from use-payment-method-interface && use-store-cart-item-quantity * Remove useCheckoutContext from shipping provider * Remove isCalculating from state * Removed useCheckoutContext from lots of places * Remove useCheckoutContext from checkout-payment-block * Remove useCheckoutContext in checkout-shipping-methods-block and checkout-shipping-address-block * add isCart selector and action and update the checkoutstate context * Fixed redirectUrl bug by using thunks * Remove dispatchActions from checkout-state * Change SET_HAS_ERROR action to be neater * Thomas' feedback * Tidy up * Oops, deleted things I shouldn't have * Typescript * Fix types * Fix tests * Remove isCart * Update docs and remove unecessary getRedirectUrl() selector * validate event emitter button * Added thunks in a separate file * Call thunks from checkout-state * Checkout logic tested and working * Remove dependency injection as much as poss, tidy up and fix some TS errors * Fix types in thunks.ts * Fixed some ts errors * WIP * Fixed bug * Shift side effects from checkout-state to checkout-processor * Revert "Shift side effects from checkout-state to checkout-processor" This reverts commit 059533da4eb34f9982f66cd4adacc7b2c24f939f. * Rename CheckoutState to CheckoutEvents * Move status check outside the thunk * remove duplicate EVENTS constant * remove temp buttons * Remove console logs * Augment @wordpress/data package with our new store types * Add correct type for CheckoutAction * Remove createErrorNotice arg from runCheckoutAfterProcessingWithErrorObservers * Remove createErrorNotice from emit event types * Use type keyword when importing types * Add correct types for dispatch and select in thunks * Update wordpress/data types * Replace store creation with new preferred method * Set correct action type on reducer * Remove unnecessary async from thunk * add CHECKOUT_ prefix to checkout events again * export EVENTS with eveything else in checkout0-events/event-emit * Remove duplicate SelectFromMap and TailParameters * Updated type for paymentStatus * TODO remove wp/data experimental thunks * Remove `setCustomerId` from events and `processCheckoutResponseHeaders` (https://github.com/woocommerce/woocommerce-blocks/pull/6586) * Prevent passing dispatch, instead get actions direct from store * Get setCustomerId from the store instead of passing it to processCheckoutResponseHeaders * Revert "Prevent passing dispatch, instead get actions direct from store" This reverts commit 4479a2ef5599d9c8d99c3629616b3d662210fc08. * Auto stash before revert of "Prevent passing dispatch, instead get actions direct from store" * Remove duplicate dispatch * Fix unit tests Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com> Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
This commit is contained in:
parent
3630b7b03e
commit
015291ccf3
|
@ -20,7 +20,7 @@ import { ValidationInputError } from '../../providers/validation';
|
|||
import { useStoreCart } from '../cart/use-store-cart';
|
||||
import { useStoreCartCoupons } from '../cart/use-store-cart-coupons';
|
||||
import { useEmitResponse } from '../use-emit-response';
|
||||
import { useCheckoutContext } from '../../providers/cart-checkout/checkout-state';
|
||||
import { useCheckoutEventsContext } from '../../providers/cart-checkout/checkout-events';
|
||||
import { usePaymentMethodDataContext } from '../../providers/cart-checkout/payment-methods';
|
||||
import { useShippingDataContext } from '../../providers/cart-checkout/shipping';
|
||||
import { useCustomerDataContext } from '../../providers/cart-checkout/customer';
|
||||
|
@ -37,7 +37,7 @@ export const usePaymentMethodInterface = (): PaymentMethodInterface => {
|
|||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
onSubmit,
|
||||
} = useCheckoutContext();
|
||||
} = useCheckoutEventsContext();
|
||||
const {
|
||||
isCalculating,
|
||||
isComplete,
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
config as checkoutStoreConfig,
|
||||
} from '../../../../data/checkout';
|
||||
|
||||
const mockUseCheckoutContext = {
|
||||
const mockUseCheckoutEventsContext = {
|
||||
onSubmit: jest.fn(),
|
||||
};
|
||||
const mockUsePaymentMethodDataContext = {
|
||||
|
@ -23,8 +23,8 @@ const mockUsePaymentMethodDataContext = {
|
|||
},
|
||||
};
|
||||
|
||||
jest.mock( '../../providers/cart-checkout/checkout-state', () => ( {
|
||||
useCheckoutContext: () => mockUseCheckoutContext,
|
||||
jest.mock( '../../providers/cart-checkout/checkout-events', () => ( {
|
||||
useCheckoutEventsContext: () => mockUseCheckoutEventsContext,
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../providers/cart-checkout/payment-methods', () => ( {
|
||||
|
@ -66,6 +66,8 @@ describe( 'useCheckoutSubmit', () => {
|
|||
|
||||
onSubmit();
|
||||
|
||||
expect( mockUseCheckoutContext.onSubmit ).toHaveBeenCalledTimes( 1 );
|
||||
expect( mockUseCheckoutEventsContext.onSubmit ).toHaveBeenCalledTimes(
|
||||
1
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -8,7 +8,7 @@ import { __ } from '@wordpress/i18n';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCheckoutContext } from '../providers';
|
||||
import { useCheckoutEventsContext } from '../providers';
|
||||
import { usePaymentMethodDataContext } from '../providers/cart-checkout/payment-methods';
|
||||
import { usePaymentMethods } from './payment-methods/use-payment-methods';
|
||||
|
||||
|
@ -36,7 +36,7 @@ export const useCheckoutSubmit = () => {
|
|||
};
|
||||
} );
|
||||
|
||||
const { onSubmit } = useCheckoutContext();
|
||||
const { onSubmit } = useCheckoutEventsContext();
|
||||
|
||||
const { paymentMethods = {} } = usePaymentMethods();
|
||||
const { activePaymentMethod, currentStatus: paymentStatus } =
|
||||
|
|
|
@ -14,7 +14,9 @@ import {
|
|||
ActionType,
|
||||
} from '../../../event-emit';
|
||||
|
||||
const EMIT_TYPES = {
|
||||
// These events are emitted when the Checkout status is BEFORE_PROCESSING and AFTER_PROCESSING
|
||||
// to enable third parties to hook into the checkout process
|
||||
const EVENTS = {
|
||||
CHECKOUT_VALIDATION_BEFORE_PROCESSING:
|
||||
'checkout_validation_before_processing',
|
||||
CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS:
|
||||
|
@ -42,15 +44,15 @@ const useEventEmitters = (
|
|||
const eventEmitters = useMemo(
|
||||
() => ( {
|
||||
onCheckoutAfterProcessingWithSuccess: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
EVENTS.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
observerDispatch
|
||||
),
|
||||
onCheckoutAfterProcessingWithError: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
EVENTS.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
observerDispatch
|
||||
),
|
||||
onCheckoutValidationBeforeProcessing: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
|
||||
EVENTS.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
|
||||
observerDispatch
|
||||
),
|
||||
} ),
|
||||
|
@ -59,4 +61,4 @@ const useEventEmitters = (
|
|||
return eventEmitters;
|
||||
};
|
||||
|
||||
export { EMIT_TYPES, useEventEmitters, reducer, emitEvent, emitEventWithAbort };
|
||||
export { EVENTS, useEventEmitters, reducer, emitEvent, emitEventWithAbort };
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useRef,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from '@wordpress/element';
|
||||
import { usePrevious } from '@woocommerce/base-hooks';
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { CheckoutEventsContextType } from './types';
|
||||
import { useEventEmitters, reducer as emitReducer } from './event-emit';
|
||||
import { STATUS } from '../../../../../data/checkout/constants';
|
||||
import { useValidationContext } from '../../validation';
|
||||
import { useStoreEvents } from '../../../hooks/use-store-events';
|
||||
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
|
||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||
import { CheckoutState } from '../../../../../data/checkout/default-state';
|
||||
|
||||
const CheckoutEventsContext = createContext( {
|
||||
onSubmit: () => void null,
|
||||
onCheckoutAfterProcessingWithSuccess: () => () => void null,
|
||||
onCheckoutAfterProcessingWithError: () => () => void null,
|
||||
onCheckoutBeforeProcessing: () => () => void null, // deprecated for onCheckoutValidationBeforeProcessing
|
||||
onCheckoutValidationBeforeProcessing: () => () => void null,
|
||||
} );
|
||||
|
||||
export const useCheckoutEventsContext = () => {
|
||||
return useContext( CheckoutEventsContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkout Events provider
|
||||
* Emit Checkout events and provide access to Checkout event handlers
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
export const CheckoutEventsProvider = ( {
|
||||
children,
|
||||
redirectUrl,
|
||||
}: {
|
||||
children: React.ReactChildren;
|
||||
redirectUrl: string;
|
||||
} ): JSX.Element => {
|
||||
const checkoutActions = useDispatch( CHECKOUT_STORE_KEY );
|
||||
const checkoutState: CheckoutState = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).getCheckoutState()
|
||||
);
|
||||
|
||||
if ( redirectUrl && redirectUrl !== checkoutState.redirectUrl ) {
|
||||
checkoutActions.setRedirectUrl( redirectUrl );
|
||||
}
|
||||
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
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 ] );
|
||||
|
||||
// Emit CHECKOUT_VALIDATE event and set the error state based on the response of
|
||||
// the registered callbacks
|
||||
useEffect( () => {
|
||||
if ( checkoutState.status === STATUS.BEFORE_PROCESSING ) {
|
||||
checkoutActions.emitValidateEvent( {
|
||||
observers: currentObservers.current,
|
||||
setValidationErrors,
|
||||
} );
|
||||
}
|
||||
}, [
|
||||
checkoutState.status,
|
||||
setValidationErrors,
|
||||
createErrorNotice,
|
||||
checkoutActions,
|
||||
] );
|
||||
|
||||
const previousStatus = usePrevious( checkoutState.status );
|
||||
const previousHasError = usePrevious( checkoutState.hasError );
|
||||
|
||||
// Emit CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS and CHECKOUT_AFTER_PROCESSING_WITH_ERROR events
|
||||
// and set checkout errors according to the callback responses
|
||||
useEffect( () => {
|
||||
if (
|
||||
checkoutState.status === previousStatus &&
|
||||
checkoutState.hasError === previousHasError
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( checkoutState.status === STATUS.AFTER_PROCESSING ) {
|
||||
checkoutActions.emitAfterProcessingEvents( {
|
||||
observers: currentObservers.current,
|
||||
notices: {
|
||||
checkoutNotices,
|
||||
paymentNotices,
|
||||
expressPaymentNotices,
|
||||
},
|
||||
} );
|
||||
}
|
||||
}, [
|
||||
checkoutState.status,
|
||||
checkoutState.hasError,
|
||||
checkoutState.redirectUrl,
|
||||
checkoutState.orderId,
|
||||
checkoutState.customerId,
|
||||
checkoutState.orderNotes,
|
||||
checkoutState.processingResponse,
|
||||
previousStatus,
|
||||
previousHasError,
|
||||
createErrorNotice,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
isSuccessResponse,
|
||||
shouldRetry,
|
||||
checkoutNotices,
|
||||
expressPaymentNotices,
|
||||
paymentNotices,
|
||||
checkoutActions,
|
||||
] );
|
||||
|
||||
const onSubmit = useCallback( () => {
|
||||
dispatchCheckoutEvent( 'submit' );
|
||||
checkoutActions.setBeforeProcessing();
|
||||
}, [ dispatchCheckoutEvent, checkoutActions ] );
|
||||
|
||||
const checkoutEventHandlers: CheckoutEventsContextType = {
|
||||
onSubmit,
|
||||
onCheckoutBeforeProcessing,
|
||||
onCheckoutValidationBeforeProcessing,
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
};
|
||||
return (
|
||||
<CheckoutEventsContext.Provider value={ checkoutEventHandlers }>
|
||||
{ children }
|
||||
</CheckoutEventsContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { emitterCallback } from '../../../event-emit';
|
||||
|
||||
export type CheckoutEventsContextType = {
|
||||
// Submits the checkout and begins processing.
|
||||
onSubmit: () => void;
|
||||
// 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 >;
|
||||
};
|
|
@ -21,7 +21,7 @@ import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { preparePaymentData, processCheckoutResponseHeaders } from './utils';
|
||||
import { useCheckoutContext } from './checkout-state';
|
||||
import { useCheckoutEventsContext } from './checkout-events';
|
||||
import { useShippingDataContext } from './shipping';
|
||||
import { useCustomerDataContext } from './customer';
|
||||
import { usePaymentMethodDataContext } from './payment-methods';
|
||||
|
@ -34,7 +34,7 @@ import { useStoreNoticesContext } from '../store-notices';
|
|||
* Subscribes to checkout context and triggers processing via the API.
|
||||
*/
|
||||
const CheckoutProcessor = () => {
|
||||
const { onCheckoutValidationBeforeProcessing } = useCheckoutContext();
|
||||
const { onCheckoutValidationBeforeProcessing } = useCheckoutEventsContext();
|
||||
|
||||
const {
|
||||
hasError: checkoutHasError,
|
||||
|
@ -55,9 +55,8 @@ const CheckoutProcessor = () => {
|
|||
};
|
||||
} );
|
||||
|
||||
const { setCustomerId, setHasError, processCheckoutResponse } = useDispatch(
|
||||
CHECKOUT_STORE_KEY
|
||||
);
|
||||
const { setHasError, processCheckoutResponse } =
|
||||
useDispatch( CHECKOUT_STORE_KEY );
|
||||
|
||||
const { hasValidationErrors } = useValidationContext();
|
||||
const { shippingErrorStatus } = useShippingDataContext();
|
||||
|
@ -80,7 +79,10 @@ const CheckoutProcessor = () => {
|
|||
const [ isProcessingOrder, setIsProcessingOrder ] = useState( false );
|
||||
|
||||
const paymentMethodId = useMemo( () => {
|
||||
const merged = { ...expressPaymentMethods, ...paymentMethods };
|
||||
const merged = {
|
||||
...expressPaymentMethods,
|
||||
...paymentMethods,
|
||||
};
|
||||
return merged?.[ activePaymentMethod ]?.paymentMethodId;
|
||||
}, [ activePaymentMethod, expressPaymentMethods, paymentMethods ] );
|
||||
|
||||
|
@ -118,6 +120,7 @@ const CheckoutProcessor = () => {
|
|||
setHasError,
|
||||
] );
|
||||
|
||||
// Keep the billing, shipping and redirectUrl current
|
||||
useEffect( () => {
|
||||
currentBillingAddress.current = billingAddress;
|
||||
currentShippingAddress.current = shippingAddress;
|
||||
|
@ -152,6 +155,7 @@ const CheckoutProcessor = () => {
|
|||
shippingErrorStatus.hasError,
|
||||
] );
|
||||
|
||||
// Validate the checkout using the CHECKOUT_VALIDATION_BEFORE_PROCESSING event
|
||||
useEffect( () => {
|
||||
let unsubscribeProcessing;
|
||||
if ( ! isExpressPaymentMethodActive ) {
|
||||
|
@ -171,13 +175,14 @@ const CheckoutProcessor = () => {
|
|||
isExpressPaymentMethodActive,
|
||||
] );
|
||||
|
||||
// redirect when checkout is complete and there is a redirect url.
|
||||
// Redirect when checkout is complete and there is a redirect url.
|
||||
useEffect( () => {
|
||||
if ( currentRedirectUrl.current ) {
|
||||
window.location.href = currentRedirectUrl.current;
|
||||
}
|
||||
}, [ checkoutIsComplete ] );
|
||||
|
||||
// POST to the Store API and process and display any errors, or set order complete
|
||||
const processOrder = useCallback( async () => {
|
||||
if ( isProcessingOrder ) {
|
||||
return;
|
||||
|
@ -220,10 +225,7 @@ const CheckoutProcessor = () => {
|
|||
parse: false,
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
processCheckoutResponseHeaders(
|
||||
response.headers,
|
||||
setCustomerId
|
||||
);
|
||||
processCheckoutResponseHeaders( response.headers );
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( response );
|
||||
}
|
||||
|
@ -236,10 +238,7 @@ const CheckoutProcessor = () => {
|
|||
.catch( ( errorResponse ) => {
|
||||
try {
|
||||
if ( errorResponse?.headers ) {
|
||||
processCheckoutResponseHeaders(
|
||||
errorResponse.headers,
|
||||
setCustomerId
|
||||
);
|
||||
processCheckoutResponseHeaders( errorResponse.headers );
|
||||
}
|
||||
// This attempts to parse a JSON error response where the status code was 4xx/5xx.
|
||||
errorResponse.json().then( ( response ) => {
|
||||
|
@ -306,10 +305,9 @@ const CheckoutProcessor = () => {
|
|||
receiveCart,
|
||||
setHasError,
|
||||
processCheckoutResponse,
|
||||
setCustomerId,
|
||||
] );
|
||||
|
||||
// process order if conditions are good.
|
||||
// Process order if conditions are good.
|
||||
useEffect( () => {
|
||||
if ( paidAndWithoutErrors && ! isProcessingOrder ) {
|
||||
processOrder();
|
||||
|
|
|
@ -10,7 +10,7 @@ import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundar
|
|||
import { PaymentMethodDataProvider } from './payment-methods';
|
||||
import { ShippingDataProvider } from './shipping';
|
||||
import { CustomerDataProvider } from './customer';
|
||||
import { CheckoutStateProvider } from './checkout-state';
|
||||
import { CheckoutEventsProvider } from './checkout-events';
|
||||
import CheckoutProcessor from './checkout-processor';
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ import CheckoutProcessor from './checkout-processor';
|
|||
*/
|
||||
export const CheckoutProvider = ( { children, redirectUrl } ) => {
|
||||
return (
|
||||
<CheckoutStateProvider redirectUrl={ redirectUrl }>
|
||||
<CheckoutEventsProvider redirectUrl={ redirectUrl }>
|
||||
<CustomerDataProvider>
|
||||
<ShippingDataProvider>
|
||||
<PaymentMethodDataProvider>
|
||||
|
@ -45,6 +45,6 @@ export const CheckoutProvider = ( { children, redirectUrl } ) => {
|
|||
</PaymentMethodDataProvider>
|
||||
</ShippingDataProvider>
|
||||
</CustomerDataProvider>
|
||||
</CheckoutStateProvider>
|
||||
</CheckoutEventsProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { PaymentResult } from '@woocommerce/types';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { 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_SHIPPING_ADDRESS_AS_BILLING_ADDRESS = 'set_shipping_address_as_billing_address',
|
||||
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 > | PaymentResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: PaymentResult ) =>
|
||||
( {
|
||||
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 ),
|
||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) =>
|
||||
( {
|
||||
type: ACTION.SET_SHIPPING_ADDRESS_AS_BILLING_ADDRESS,
|
||||
useShippingAsBilling,
|
||||
} 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 ),
|
||||
};
|
|
@ -1,66 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting, EnteredAddress } from '@woocommerce/settings';
|
||||
import { isSameAddress } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* 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 preloadedCheckoutData = getSetting( 'checkoutData', {} ) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const checkoutData = {
|
||||
order_id: 0,
|
||||
customer_id: 0,
|
||||
billing_address: {} as EnteredAddress,
|
||||
shipping_address: {} as EnteredAddress,
|
||||
...( preloadedCheckoutData || {} ),
|
||||
};
|
||||
|
||||
export const DEFAULT_CHECKOUT_STATE_DATA: CheckoutStateContextType = {
|
||||
onSubmit: () => void null,
|
||||
onCheckoutAfterProcessingWithSuccess: () => () => void null,
|
||||
onCheckoutAfterProcessingWithError: () => () => void null,
|
||||
onCheckoutBeforeProcessing: () => () => void null, // deprecated for onCheckoutValidationBeforeProcessing
|
||||
onCheckoutValidationBeforeProcessing: () => () => void null,
|
||||
};
|
||||
|
||||
export const DEFAULT_STATE: CheckoutStateContextState = {
|
||||
redirectUrl: '',
|
||||
status: STATUS.PRISTINE,
|
||||
hasError: false,
|
||||
calculatingCount: 0,
|
||||
orderId: checkoutData.order_id,
|
||||
orderNotes: '',
|
||||
customerId: checkoutData.customer_id,
|
||||
useShippingAsBilling: isSameAddress(
|
||||
checkoutData.billing_address,
|
||||
checkoutData.shipping_address
|
||||
),
|
||||
shouldCreateAccount: false,
|
||||
processingResponse: null,
|
||||
extensionData: {},
|
||||
};
|
|
@ -1,337 +0,0 @@
|
|||
/**
|
||||
* 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, isString } from '@woocommerce/types';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STATUS, DEFAULT_CHECKOUT_STATE_DATA } from './constants';
|
||||
import type { CheckoutStateContextType } from './types';
|
||||
import {
|
||||
EMIT_TYPES,
|
||||
useEventEmitters,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
reducer as emitReducer,
|
||||
} from './event-emit';
|
||||
import { useValidationContext } from '../../validation';
|
||||
import { useStoreEvents } from '../../../hooks/use-store-events';
|
||||
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
|
||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||
import { removeNoticesByStatus } from '../../../../../utils/notices';
|
||||
import { CheckoutState } from '../../../../../data/checkout/default-state';
|
||||
|
||||
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.
|
||||
*/
|
||||
export const CheckoutStateProvider = ( {
|
||||
children,
|
||||
redirectUrl,
|
||||
}: {
|
||||
children: React.ReactChildren;
|
||||
redirectUrl: string;
|
||||
} ): JSX.Element => {
|
||||
const checkoutActions = useDispatch( CHECKOUT_STORE_KEY );
|
||||
const checkoutState: CheckoutState = useSelect( ( select ) =>
|
||||
select( CHECKOUT_STORE_KEY ).getCheckoutState()
|
||||
);
|
||||
|
||||
if ( redirectUrl && redirectUrl !== checkoutState.redirectUrl ) {
|
||||
checkoutActions.setRedirectUrl( redirectUrl );
|
||||
}
|
||||
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
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 ] );
|
||||
|
||||
// emit events.
|
||||
useEffect( () => {
|
||||
const status = checkoutState.status;
|
||||
if ( status === STATUS.BEFORE_PROCESSING ) {
|
||||
removeNoticesByStatus( 'error' );
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
|
||||
{}
|
||||
).then( ( response ) => {
|
||||
if ( response !== true ) {
|
||||
if ( Array.isArray( response ) ) {
|
||||
response.forEach(
|
||||
( { errorMessage, validationErrors } ) => {
|
||||
createErrorNotice( errorMessage, {
|
||||
context: 'wc/checkout',
|
||||
} );
|
||||
setValidationErrors( validationErrors );
|
||||
}
|
||||
);
|
||||
}
|
||||
checkoutActions.setIdle();
|
||||
checkoutActions.setHasError();
|
||||
} else {
|
||||
checkoutActions.setProcessing();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}, [
|
||||
checkoutState.status,
|
||||
setValidationErrors,
|
||||
createErrorNotice,
|
||||
checkoutActions,
|
||||
] );
|
||||
|
||||
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 && isString( response.message ) ) {
|
||||
const errorOptions =
|
||||
response.messageContext &&
|
||||
isString( response.messageContent )
|
||||
? // The `as string` is OK here because of the type guard above.
|
||||
{ context: response.messageContext as string }
|
||||
: undefined;
|
||||
errorResponse = response;
|
||||
createErrorNotice( 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 ) ) {
|
||||
checkoutActions.setComplete( errorResponse );
|
||||
} else {
|
||||
checkoutActions.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 for assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
createErrorNotice( message, {
|
||||
id: 'checkout',
|
||||
context: 'wc/checkout',
|
||||
} );
|
||||
}
|
||||
|
||||
checkoutActions.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 ) {
|
||||
checkoutActions.setComplete( successResponse );
|
||||
} else if ( isObject( errorResponse ) ) {
|
||||
if (
|
||||
errorResponse.message &&
|
||||
isString( errorResponse.message )
|
||||
) {
|
||||
const errorOptions =
|
||||
errorResponse.messageContext &&
|
||||
isString( errorResponse.messageContext )
|
||||
? { context: errorResponse.messageContext }
|
||||
: undefined;
|
||||
createErrorNotice(
|
||||
errorResponse.message,
|
||||
errorOptions
|
||||
);
|
||||
}
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
checkoutActions.setComplete( errorResponse );
|
||||
} else {
|
||||
// this will set an error which will end up
|
||||
// triggering the onCheckoutAfterProcessingWithError emitter.
|
||||
// and then setting checkout to IDLE state.
|
||||
checkoutActions.setHasError( true );
|
||||
}
|
||||
} else {
|
||||
// nothing hooked in had any response type so let's just consider successful.
|
||||
checkoutActions.setComplete();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
checkoutState.status,
|
||||
checkoutState.hasError,
|
||||
checkoutState.redirectUrl,
|
||||
checkoutState.orderId,
|
||||
checkoutState.customerId,
|
||||
checkoutState.orderNotes,
|
||||
checkoutState.processingResponse,
|
||||
previousStatus,
|
||||
previousHasError,
|
||||
createErrorNotice,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
isSuccessResponse,
|
||||
shouldRetry,
|
||||
checkoutNotices,
|
||||
expressPaymentNotices,
|
||||
paymentNotices,
|
||||
checkoutActions,
|
||||
] );
|
||||
|
||||
const onSubmit = useCallback( () => {
|
||||
dispatchCheckoutEvent( 'submit' );
|
||||
checkoutActions.setBeforeProcessing();
|
||||
}, [ dispatchCheckoutEvent, checkoutActions ] );
|
||||
|
||||
const checkoutData: CheckoutStateContextType = {
|
||||
onSubmit,
|
||||
onCheckoutBeforeProcessing,
|
||||
onCheckoutValidationBeforeProcessing,
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
};
|
||||
return (
|
||||
<CheckoutContext.Provider value={ checkoutData }>
|
||||
{ children }
|
||||
</CheckoutContext.Provider>
|
||||
);
|
||||
};
|
|
@ -1,213 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { PaymentResult } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { DEFAULT_STATE, STATUS } from './constants';
|
||||
import { ActionType, ACTION } from './actions';
|
||||
import type { CheckoutStateContextState } from './types';
|
||||
|
||||
/**
|
||||
* Reducer for the checkout state
|
||||
*/
|
||||
export const reducer = (
|
||||
state = DEFAULT_STATE,
|
||||
{
|
||||
redirectUrl,
|
||||
type,
|
||||
customerId,
|
||||
orderId,
|
||||
orderNotes,
|
||||
extensionData,
|
||||
useShippingAsBilling,
|
||||
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 PaymentResult,
|
||||
};
|
||||
break;
|
||||
|
||||
case ACTION.SET_COMPLETE:
|
||||
newState =
|
||||
state.status !== STATUS.COMPLETE
|
||||
? {
|
||||
...state,
|
||||
status: STATUS.COMPLETE,
|
||||
redirectUrl:
|
||||
typeof data?.redirectUrl === 'string'
|
||||
? 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_SHIPPING_ADDRESS_AS_BILLING_ADDRESS:
|
||||
if (
|
||||
useShippingAsBilling !== undefined &&
|
||||
useShippingAsBilling !== state.useShippingAsBilling
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
useShippingAsBilling,
|
||||
};
|
||||
}
|
||||
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;
|
||||
};
|
|
@ -1,75 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { PaymentResult } from '@woocommerce/types';
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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;
|
||||
useShippingAsBilling: boolean;
|
||||
shouldCreateAccount: boolean;
|
||||
processingResponse: PaymentResult | 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 = {
|
||||
// Submits the checkout and begins processing.
|
||||
onSubmit: () => void;
|
||||
// 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 >;
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import type { PaymentResult, CheckoutResponse } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Prepares the payment_result data from the server checkout endpoint response.
|
||||
*/
|
||||
export const getPaymentResultFromCheckoutResponse = (
|
||||
response: CheckoutResponse
|
||||
): PaymentResult => {
|
||||
const paymentResult = {
|
||||
message: '',
|
||||
paymentStatus: '',
|
||||
redirectUrl: '',
|
||||
paymentDetails: {},
|
||||
} as PaymentResult;
|
||||
|
||||
// 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 for assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
}
|
||||
|
||||
return paymentResult;
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
export * from './payment-methods';
|
||||
export * from './shipping';
|
||||
export * from './customer';
|
||||
export * from './checkout-state';
|
||||
export * from './checkout-events';
|
||||
export * from './cart';
|
||||
export * from './checkout-processor';
|
||||
export * from './checkout-provider';
|
||||
|
|
|
@ -2,11 +2,8 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import triggerFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { setCustomerId as setCheckoutCustomerId } from '../../../../data/checkout/actions';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Utility function for preparing payment data for the request.
|
||||
|
@ -34,10 +31,8 @@ export const preparePaymentData = (
|
|||
/**
|
||||
* Process headers from an API response an dispatch updates.
|
||||
*/
|
||||
export const processCheckoutResponseHeaders = (
|
||||
headers: Headers,
|
||||
setCustomerId: typeof setCheckoutCustomerId
|
||||
): void => {
|
||||
export const processCheckoutResponseHeaders = ( headers: Headers ): void => {
|
||||
const { setCustomerId } = dispatch( CHECKOUT_STORE_KEY );
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore -- this does exist because it's monkey patched in
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CheckoutResponse, PaymentResult } from '@woocommerce/types';
|
||||
import { PaymentResult } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getPaymentResultFromCheckoutResponse } from '../../base/context/providers/cart-checkout/checkout-state/utils';
|
||||
import { ACTION_TYPES as types } from './action-types';
|
||||
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
|
||||
|
||||
// `Thunks are functions that can be dispatched, similar to actions creators
|
||||
export * from './thunks';
|
||||
|
||||
export const setPristine = () => ( {
|
||||
type: types.SET_PRISTINE,
|
||||
|
@ -44,15 +47,6 @@ export const setAfterProcessing = () => ( {
|
|||
type: types.SET_AFTER_PROCESSING,
|
||||
} );
|
||||
|
||||
export const processCheckoutResponse = ( response: CheckoutResponse ) => {
|
||||
return async ( { dispatch }: { dispatch: React.Dispatch< Action > } ) => {
|
||||
const paymentResult = getPaymentResultFromCheckoutResponse( response );
|
||||
dispatch( setRedirectUrl( paymentResult?.redirectUrl || '' ) );
|
||||
dispatch( setProcessingResponse( paymentResult ) );
|
||||
dispatch( setAfterProcessing() );
|
||||
};
|
||||
};
|
||||
|
||||
export const setHasError = ( hasError = true ) => ( {
|
||||
type: types.SET_HAS_ERROR,
|
||||
hasError,
|
||||
|
@ -98,7 +92,7 @@ export const setExtensionData = (
|
|||
extensionData,
|
||||
} );
|
||||
|
||||
type Action = ReturnType<
|
||||
export type CheckoutAction = ReturnOrGeneratorYieldUnion<
|
||||
| typeof setPristine
|
||||
| typeof setIdle
|
||||
| typeof setComplete
|
||||
|
|
|
@ -12,12 +12,12 @@ export enum STATUS {
|
|||
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 BEFORE_PROCESSING status emitters have finished successfully. Payment processing is started on this checkout status.
|
||||
PROCESSING = 'processing',
|
||||
// After server side checkout processing is completed this status is set
|
||||
AFTER_PROCESSING = 'after_processing',
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerStore } from '@wordpress/data';
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -10,14 +10,29 @@ import { STORE_KEY } from './constants';
|
|||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
import reducer from './reducers';
|
||||
import { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
||||
|
||||
export const config = {
|
||||
reducer,
|
||||
selectors,
|
||||
actions,
|
||||
// TODO: Gutenberg with Thunks was released in WP 6.0. Once 6.1 is released, remove the experimental flag here
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore We pass this in case there is an older version of Gutenberg running.
|
||||
__experimentalUseThunks: true,
|
||||
};
|
||||
|
||||
registerStore( STORE_KEY, config );
|
||||
const store = createReduxStore( STORE_KEY, config );
|
||||
register( store );
|
||||
|
||||
export const CHECKOUT_STORE_KEY = STORE_KEY;
|
||||
declare module '@wordpress/data' {
|
||||
function dispatch(
|
||||
key: typeof CHECKOUT_STORE_KEY
|
||||
): DispatchFromMap< typeof actions >;
|
||||
function select(
|
||||
key: typeof CHECKOUT_STORE_KEY
|
||||
): SelectFromMap< typeof selectors > & {
|
||||
hasFinishedResolution: ( selector: string ) => boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,8 +10,9 @@ import { PaymentResult } from '@woocommerce/types';
|
|||
import { ACTION_TYPES as types } from './action-types';
|
||||
import { STATUS } from './constants';
|
||||
import { defaultState } from './default-state';
|
||||
import { CheckoutAction } from './actions';
|
||||
|
||||
const reducer: Reducer = ( state = defaultState, action ) => {
|
||||
const reducer: Reducer = ( state = defaultState, action: CheckoutAction ) => {
|
||||
let newState = state;
|
||||
switch ( action.type ) {
|
||||
case types.SET_PRISTINE:
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { CheckoutResponse } from '@woocommerce/types';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { removeNoticesByStatus } from '../../utils/notices';
|
||||
import {
|
||||
getPaymentResultFromCheckoutResponse,
|
||||
runCheckoutAfterProcessingWithErrorObservers,
|
||||
runCheckoutAfterProcessingWithSuccessObservers,
|
||||
} from './utils';
|
||||
import {
|
||||
EVENTS,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
} from '../../base/context/providers/cart-checkout/checkout-events/event-emit';
|
||||
import type {
|
||||
emitValidateEventType,
|
||||
emitAfterProcessingEventsType,
|
||||
} from './types';
|
||||
import type { DispatchFromMap } from '../mapped-types';
|
||||
import * as actions from './actions';
|
||||
|
||||
/**
|
||||
* Based on the result of the payment, update the redirect url,
|
||||
* set the payment processing response in the checkout data store
|
||||
* and change the status to AFTER_PROCESSING
|
||||
*/
|
||||
export const processCheckoutResponse = ( response: CheckoutResponse ) => {
|
||||
return ( {
|
||||
dispatch,
|
||||
}: {
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
} ) => {
|
||||
const paymentResult = getPaymentResultFromCheckoutResponse( response );
|
||||
dispatch.setRedirectUrl( paymentResult?.redirectUrl || '' );
|
||||
dispatch.setProcessingResponse( paymentResult );
|
||||
dispatch.setAfterProcessing();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit the CHECKOUT_VALIDATION_BEFORE_PROCESSING event and process all
|
||||
* registered observers
|
||||
*/
|
||||
export const emitValidateEvent: emitValidateEventType = ( {
|
||||
observers,
|
||||
setValidationErrors, // TODO: Fix this type after we move to validation store
|
||||
} ) => {
|
||||
return ( { dispatch, registry } ) => {
|
||||
const { createErrorNotice } = registry.dispatch( noticesStore );
|
||||
removeNoticesByStatus( 'error' );
|
||||
emitEvent(
|
||||
observers,
|
||||
EVENTS.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
|
||||
{}
|
||||
).then( ( response ) => {
|
||||
if ( response !== true ) {
|
||||
if ( Array.isArray( response ) ) {
|
||||
response.forEach(
|
||||
( { errorMessage, validationErrors } ) => {
|
||||
createErrorNotice( errorMessage, {
|
||||
context: 'wc/checkout',
|
||||
} );
|
||||
setValidationErrors( validationErrors );
|
||||
}
|
||||
);
|
||||
}
|
||||
dispatch.setIdle();
|
||||
dispatch.setHasError();
|
||||
} else {
|
||||
dispatch.setProcessing();
|
||||
}
|
||||
} );
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit the CHECKOUT_AFTER_PROCESSING_WITH_ERROR if the checkout contains an error,
|
||||
* or the CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS if not. Set checkout errors according
|
||||
* to the observer responses
|
||||
*/
|
||||
export const emitAfterProcessingEvents: emitAfterProcessingEventsType = ( {
|
||||
observers,
|
||||
notices,
|
||||
} ) => {
|
||||
return ( { select, dispatch, registry } ) => {
|
||||
const { createErrorNotice } = registry.dispatch( noticesStore );
|
||||
const state = select.getCheckoutState();
|
||||
const data = {
|
||||
redirectUrl: state.redirectUrl,
|
||||
orderId: state.orderId,
|
||||
customerId: state.customerId,
|
||||
orderNotes: state.orderNotes,
|
||||
processingResponse: state.processingResponse,
|
||||
};
|
||||
if ( state.hasError ) {
|
||||
// allow payment methods or other things to customize the error
|
||||
// with a fallback if nothing customizes it.
|
||||
emitEventWithAbort(
|
||||
observers,
|
||||
EVENTS.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
data
|
||||
).then( ( observerResponses ) => {
|
||||
runCheckoutAfterProcessingWithErrorObservers( {
|
||||
observerResponses,
|
||||
notices,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
data,
|
||||
} );
|
||||
} );
|
||||
} else {
|
||||
emitEventWithAbort(
|
||||
observers,
|
||||
EVENTS.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
data
|
||||
).then( ( observerResponses: unknown[] ) => {
|
||||
runCheckoutAfterProcessingWithSuccessObservers( {
|
||||
observerResponses,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
} );
|
||||
} );
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Notice } from '@wordpress/notices/';
|
||||
import { DataRegistry } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { EventObserversType } from '../../base/context/event-emit/types';
|
||||
import type { CheckoutState } from './default-state';
|
||||
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
|
||||
export type CheckoutAfterProcessingWithErrorEventData = {
|
||||
redirectUrl: CheckoutState[ 'redirectUrl' ];
|
||||
orderId: CheckoutState[ 'orderId' ];
|
||||
customerId: CheckoutState[ 'customerId' ];
|
||||
orderNotes: CheckoutState[ 'orderNotes' ];
|
||||
processingResponse: CheckoutState[ 'processingResponse' ];
|
||||
};
|
||||
export type CheckoutAndPaymentNotices = {
|
||||
checkoutNotices: Notice[];
|
||||
paymentNotices: Notice[];
|
||||
expressPaymentNotices: Notice[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for emitAfterProcessingEventsType() thunk
|
||||
*/
|
||||
export type emitAfterProcessingEventsType = ( {
|
||||
observers,
|
||||
notices,
|
||||
}: {
|
||||
observers: EventObserversType;
|
||||
notices: CheckoutAndPaymentNotices;
|
||||
} ) => ( {
|
||||
select,
|
||||
dispatch,
|
||||
registry,
|
||||
}: {
|
||||
select: SelectFromMap< typeof selectors >;
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
registry: DataRegistry;
|
||||
} ) => void;
|
||||
|
||||
/**
|
||||
* Type for emitValidateEventType() thunk
|
||||
*/
|
||||
export type emitValidateEventType = ( {
|
||||
observers,
|
||||
setValidationErrors,
|
||||
}: {
|
||||
observers: EventObserversType;
|
||||
setValidationErrors: ( errors: Array< unknown > ) => void;
|
||||
} ) => ( {
|
||||
dispatch,
|
||||
registry,
|
||||
}: {
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
registry: DataRegistry;
|
||||
} ) => void;
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isString, isObject } from '@woocommerce/types';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import type { PaymentResult, CheckoutResponse } from '@woocommerce/types';
|
||||
import type { createErrorNotice as originalCreateErrorNotice } from '@wordpress/notices/store/actions';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useEmitResponse } from '../../base/context/hooks/use-emit-response';
|
||||
import {
|
||||
CheckoutAndPaymentNotices,
|
||||
CheckoutAfterProcessingWithErrorEventData,
|
||||
} from './types';
|
||||
import { DispatchFromMap } from '../mapped-types';
|
||||
import * as actions from './actions';
|
||||
|
||||
const {
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
isSuccessResponse,
|
||||
shouldRetry,
|
||||
} = useEmitResponse(); // eslint-disable-line react-hooks/rules-of-hooks
|
||||
|
||||
// TODO: `useEmitResponse` is not a react hook, it just exposes some functions as
|
||||
// properties of an object. Refactor this to not be a hook, we could simply import
|
||||
// those functions where needed
|
||||
|
||||
/**
|
||||
* Based on the given observers, create Error Notices where necessary
|
||||
* and return the error response of the last registered observer
|
||||
*/
|
||||
export const handleErrorResponse = ( {
|
||||
observerResponses,
|
||||
createErrorNotice,
|
||||
}: {
|
||||
observerResponses: unknown[];
|
||||
createErrorNotice: typeof originalCreateErrorNotice;
|
||||
} ) => {
|
||||
let errorResponse = null;
|
||||
observerResponses.forEach( ( response ) => {
|
||||
if ( isErrorResponse( response ) || isFailResponse( response ) ) {
|
||||
if ( response.message && isString( response.message ) ) {
|
||||
const errorOptions =
|
||||
response.messageContext &&
|
||||
isString( response.messageContext )
|
||||
? // The `as string` is OK here because of the type guard above.
|
||||
{
|
||||
context: response.messageContext as string,
|
||||
}
|
||||
: undefined;
|
||||
errorResponse = response;
|
||||
createErrorNotice( response.message, errorOptions );
|
||||
}
|
||||
}
|
||||
} );
|
||||
return errorResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* This functions runs after the CHECKOUT_AFTER_PROCESSING_WITH_ERROR event has been triggered and
|
||||
* all observers have been processed. It sets any Error Notices and the status of the Checkout
|
||||
* based on the observer responses
|
||||
*/
|
||||
export const runCheckoutAfterProcessingWithErrorObservers = ( {
|
||||
observerResponses,
|
||||
notices,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
data,
|
||||
}: {
|
||||
observerResponses: unknown[];
|
||||
notices: CheckoutAndPaymentNotices;
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
data: CheckoutAfterProcessingWithErrorEventData;
|
||||
createErrorNotice: typeof originalCreateErrorNotice;
|
||||
} ) => {
|
||||
const errorResponse = handleErrorResponse( {
|
||||
observerResponses,
|
||||
createErrorNotice,
|
||||
} );
|
||||
|
||||
if ( errorResponse !== null ) {
|
||||
// irrecoverable error so set complete
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
dispatch.setComplete( errorResponse );
|
||||
} else {
|
||||
dispatch.setIdle();
|
||||
}
|
||||
} else {
|
||||
const hasErrorNotices =
|
||||
notices.checkoutNotices.some(
|
||||
( notice: { status: string } ) => notice.status === 'error'
|
||||
) ||
|
||||
notices.expressPaymentNotices.some(
|
||||
( notice: { status: string } ) => notice.status === 'error'
|
||||
) ||
|
||||
notices.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'
|
||||
);
|
||||
createErrorNotice( message, {
|
||||
id: 'checkout',
|
||||
context: 'wc/checkout',
|
||||
} );
|
||||
}
|
||||
|
||||
dispatch.setIdle();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This functions runs after the CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS event has been triggered and
|
||||
* all observers have been processed. It sets any Error Notices and the status of the Checkout
|
||||
* based on the observer responses
|
||||
*/
|
||||
export const runCheckoutAfterProcessingWithSuccessObservers = ( {
|
||||
observerResponses,
|
||||
dispatch,
|
||||
createErrorNotice,
|
||||
}: {
|
||||
observerResponses: unknown[];
|
||||
dispatch: DispatchFromMap< typeof actions >;
|
||||
createErrorNotice: typeof originalCreateErrorNotice;
|
||||
} ) => {
|
||||
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.setComplete( successResponse );
|
||||
} else if ( isObject( errorResponse ) ) {
|
||||
if ( errorResponse.message && isString( errorResponse.message ) ) {
|
||||
const errorOptions =
|
||||
errorResponse.messageContext &&
|
||||
isString( errorResponse.messageContext )
|
||||
? {
|
||||
context: errorResponse.messageContext,
|
||||
}
|
||||
: undefined;
|
||||
createErrorNotice( errorResponse.message, errorOptions );
|
||||
}
|
||||
if ( ! shouldRetry( errorResponse ) ) {
|
||||
dispatch.setComplete( errorResponse );
|
||||
} else {
|
||||
// this will set an error which will end up
|
||||
// triggering the onCheckoutAfterProcessingWithError emitter.
|
||||
// and then setting checkout to IDLE state.
|
||||
dispatch.setHasError( true );
|
||||
}
|
||||
} else {
|
||||
// nothing hooked in had any response type so let's just consider successful.
|
||||
dispatch.setComplete();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares the payment_result data from the server checkout endpoint response.
|
||||
*/
|
||||
export const getPaymentResultFromCheckoutResponse = (
|
||||
response: CheckoutResponse
|
||||
): PaymentResult => {
|
||||
const paymentResult = {
|
||||
message: '',
|
||||
paymentStatus: 'not set',
|
||||
redirectUrl: '',
|
||||
paymentDetails: {},
|
||||
} as PaymentResult;
|
||||
|
||||
// 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;
|
||||
};
|
|
@ -7,6 +7,9 @@
|
|||
"../mapped-types.ts",
|
||||
"../settings/shared/index.js",
|
||||
"../settings/blocks/index.js",
|
||||
"../utils/notices.ts",
|
||||
"../base/context/providers/cart-checkout/checkout-events/**.ts",
|
||||
"../base/context/event-emit",
|
||||
"../base/context/providers/cart-checkout/checkout-state/utils.ts"
|
||||
],
|
||||
"exclude": [ "**/test/**" ]
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export interface Address {
|
||||
address_1: string;
|
||||
address_2?: string;
|
||||
city?: string;
|
||||
company?: string;
|
||||
country: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone?: string;
|
||||
postcode: string;
|
||||
state?: string;
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
* External dependencies
|
||||
*/
|
||||
import { EnteredAddress } from '../../settings/shared/default-address-fields';
|
||||
import { Address } from '@woocommerce/types';
|
||||
|
||||
export interface CheckoutResponseSuccess {
|
||||
billing_address: EnteredAddress;
|
||||
billing_address: Address;
|
||||
customer_id: number;
|
||||
customer_note: string;
|
||||
extensions: Record< string, unknown >;
|
||||
|
@ -16,7 +16,7 @@ export interface CheckoutResponseSuccess {
|
|||
payment_status: 'success' | 'failure' | 'pending' | 'error';
|
||||
redirect_url: string;
|
||||
};
|
||||
shipping_address: EnteredAddress;
|
||||
shipping_address: Address;
|
||||
status: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './addresses';
|
||||
export * from './blocks';
|
||||
export * from './cart';
|
||||
export * from './cart-response';
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
CartResponseShippingAddress,
|
||||
} from './cart-response';
|
||||
import type { EmptyObjectType } from './objects';
|
||||
import type { CheckoutResponseSuccess } from './checkout';
|
||||
|
||||
export interface SupportsConfiguration {
|
||||
showSavedCards?: boolean;
|
||||
|
@ -121,7 +122,9 @@ export interface ExpressPaymentMethodConfigInstance {
|
|||
|
||||
export interface PaymentResult {
|
||||
message: string;
|
||||
paymentStatus: string;
|
||||
paymentStatus:
|
||||
| CheckoutResponseSuccess[ 'payment_result' ][ 'payment_status' ]
|
||||
| 'not set';
|
||||
paymentDetails: Record< string, string > | Record< string, never >;
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ The following data is available:
|
|||
- `orderId`: The order id for the order attached to the current checkout.
|
||||
- `customerId`: The ID of the customer if the customer has an account, or `0` for guests.
|
||||
- `calculatingCount`: If any of the totals, taxes, shipping, etc need to be calculated, the count will be increased here.
|
||||
- `processingResponse`:The result of the payment processing
|
||||
- `processingResponse`: The result of the payment processing.
|
||||
- `useShippingAsBilling`: Should the billing form be hidden and inherit the shipping address?
|
||||
- `shouldCreateAccount`: Should a user account be created with this order?
|
||||
- `extensionData`: This is used by plugins that extend Cart & Checkout to pass custom data to the Store API on checkout processing
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -121,6 +121,7 @@
|
|||
"@types/wordpress__data": "^6.0.1",
|
||||
"@types/wordpress__data-controls": "2.2.0",
|
||||
"@types/wordpress__editor": "^11.0.0",
|
||||
"@types/wordpress__notices": "^3.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.5",
|
||||
"@typescript-eslint/parser": "5.35.1",
|
||||
"@woocommerce/api": "0.2.0",
|
||||
|
|
Loading…
Reference in New Issue