From 10518e64a856668b3de3fab660b375693d130283 Mon Sep 17 00:00:00 2001 From: Alex Florisca Date: Fri, 10 Jun 2022 19:33:15 +0300 Subject: [PATCH] Convert checkout context to data store - part 1 (#6232) * 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 * set correct type for preloadedCheckoutData * Remove duplicate Address type * Fix missing addresses from type-defs index * Update docs/block-client-apis/checkout/checkout-api.md Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Update docs/block-client-apis/checkout/checkout-api.md Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Update docs * Update docs/block-client-apis/checkout/checkout-api.md Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Update docs/block-client-apis/checkout/checkout-api.md Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Revert feedback changes * REvert feedback formatting * Update docs formatting * Delete empty types.ts file * remove merge conflict from docs * Correct linting in docs Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> --- .../cart/test/use-store-cart-item-quantity.js | 13 +- .../cart/use-store-cart-item-quantity.ts | 36 ++-- .../use-payment-method-interface.ts | 24 ++- .../context/hooks/test/use-checkout-submit.js | 8 +- .../context/hooks/use-checkout-address.ts | 9 +- .../hooks/use-checkout-extension-data.ts | 16 +- .../base/context/hooks/use-checkout-submit.js | 20 +- .../providers/cart-checkout/cart/index.js | 2 +- .../cart-checkout/checkout-processor.js | 40 ++-- .../cart-checkout/checkout-provider.js | 21 +- .../cart-checkout/checkout-state/actions.ts | 13 +- .../cart-checkout/checkout-state/constants.ts | 30 --- .../cart-checkout/checkout-state/index.tsx | 127 +++--------- .../cart-checkout/checkout-state/reducer.ts | 9 +- .../cart-checkout/checkout-state/types.ts | 51 +---- .../cart-checkout/checkout-state/utils.ts | 10 +- .../payment-method-data-context.tsx | 14 +- .../providers/cart-checkout/shipping/index.js | 19 +- .../context/providers/cart-checkout/utils.ts | 8 +- assets/js/base/context/tsconfig.json | 3 +- .../express-payment/cart-express-payment.js | 15 +- .../checkout-express-payment.js | 15 +- .../payment-methods/payment-method-card.js | 14 +- .../proceed-to-checkout-block/block.tsx | 13 +- assets/js/blocks/checkout/block.tsx | 28 ++- .../frontend.tsx | 7 +- .../block.tsx | 17 +- .../frontend.tsx | 7 +- .../login-prompt.js | 7 +- .../checkout-order-note-block/block.tsx | 19 +- .../checkout-payment-block/frontend.tsx | 7 +- .../frontend.tsx | 8 +- .../frontend.tsx | 7 +- assets/js/data/checkout/action-types.ts | 21 ++ assets/js/data/checkout/actions.ts | 119 +++++++++++ assets/js/data/checkout/constants.ts | 35 ++++ assets/js/data/checkout/default-state.ts | 52 +++++ assets/js/data/checkout/index.ts | 23 +++ assets/js/data/checkout/reducers.ts | 185 ++++++++++++++++++ assets/js/data/checkout/selectors.ts | 47 +++++ assets/js/data/index.ts | 2 + assets/js/data/tsconfig.json | 3 +- assets/js/types/type-defs/checkout.ts | 31 +++ assets/js/types/type-defs/index.ts | 11 +- assets/js/types/type-defs/payments.ts | 7 + .../checkout/checkout-api.md | 89 +++++++-- .../checkout/checkout-flow-and-events.md | 13 +- 47 files changed, 942 insertions(+), 333 deletions(-) create mode 100644 assets/js/data/checkout/action-types.ts create mode 100644 assets/js/data/checkout/actions.ts create mode 100644 assets/js/data/checkout/constants.ts create mode 100644 assets/js/data/checkout/default-state.ts create mode 100644 assets/js/data/checkout/index.ts create mode 100644 assets/js/data/checkout/reducers.ts create mode 100644 assets/js/data/checkout/selectors.ts create mode 100644 assets/js/types/type-defs/checkout.ts diff --git a/assets/js/base/context/hooks/cart/test/use-store-cart-item-quantity.js b/assets/js/base/context/hooks/cart/test/use-store-cart-item-quantity.js index 2fe44a1e3de..4c3baf5d110 100644 --- a/assets/js/base/context/hooks/cart/test/use-store-cart-item-quantity.js +++ b/assets/js/base/context/hooks/cart/test/use-store-cart-item-quantity.js @@ -3,13 +3,14 @@ */ import TestRenderer, { act } from 'react-test-renderer'; import { createRegistry, RegistryProvider } from '@wordpress/data'; -import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ import * as mockUseStoreCart from '../use-store-cart'; import { useStoreCartItemQuantity } from '../use-store-cart-item-quantity'; +import { config as checkoutStoreConfig } from '../../../../../data/checkout'; jest.mock( '../use-store-cart', () => ( { useStoreCart: jest.fn(), @@ -17,7 +18,8 @@ jest.mock( '../use-store-cart', () => ( { jest.mock( '@woocommerce/block-data', () => ( { __esModule: true, - CART_STORE_KEY: 'test/store', + CART_STORE_KEY: 'test/cart/store', + CHECKOUT_STORE_KEY: 'test/checkout/store', } ) ); // Make debounce instantaneous. @@ -42,13 +44,15 @@ describe( 'useStoreCartItemQuantity', () => { let mockRemoveItemFromCart; let mockChangeCartItemQuantity; const setupMocks = ( { isPendingDelete, isPendingQuantity } ) => { + // Register mock cart store mockRemoveItemFromCart = jest .fn() .mockReturnValue( { type: 'removeItemFromCartAction' } ); mockChangeCartItemQuantity = jest .fn() .mockReturnValue( { type: 'changeCartItemQuantityAction' } ); - registry.registerStore( storeKey, { + + registry.registerStore( CART_STORE_KEY, { reducer: () => ( {} ), actions: { removeItemFromCart: mockRemoveItemFromCart, @@ -63,6 +67,9 @@ describe( 'useStoreCartItemQuantity', () => { .mockReturnValue( isPendingQuantity ), }, } ); + + // Register actual checkout store + registry.registerStore( CHECKOUT_STORE_KEY, checkoutStoreConfig ); }; beforeEach( () => { diff --git a/assets/js/base/context/hooks/cart/use-store-cart-item-quantity.ts b/assets/js/base/context/hooks/cart/use-store-cart-item-quantity.ts index d5d96fa1d22..300d98c0358 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart-item-quantity.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart-item-quantity.ts @@ -3,7 +3,7 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useState, useEffect } from '@wordpress/element'; -import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data'; +import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; import { useDebounce } from 'use-debounce'; import { usePrevious } from '@woocommerce/base-hooks'; import { triggerFragmentRefresh } from '@woocommerce/base-utils'; @@ -20,7 +20,6 @@ import { * Internal dependencies */ import { useStoreCart } from './use-store-cart'; -import { useCheckoutContext } from '../../providers/cart-checkout'; /** * Ensures the object passed has props key: string and quantity: number @@ -54,14 +53,17 @@ export const useStoreCartItemQuantity = ( const { key: cartItemKey = '', quantity: cartItemQuantity = 1 } = verifiedCartItem; const { cartErrors } = useStoreCart(); - const { dispatchActions } = useCheckoutContext(); + const { incrementCalculating, decrementCalculating } = useDispatch( + CHECKOUT_STORE_KEY + ); // Store quantity in hook state. This is used to keep the UI updated while server request is updated. const [ quantity, setQuantity ] = useState< number >( cartItemQuantity ); const [ debouncedQuantity ] = useDebounce< number >( quantity, 400 ); const previousDebouncedQuantity = usePrevious( debouncedQuantity ); - const { removeItemFromCart, changeCartItemQuantity } = - useDispatch( storeKey ); + const { removeItemFromCart, changeCartItemQuantity } = useDispatch( + CART_STORE_KEY + ); // Update local state when server updates. useEffect( () => setQuantity( cartItemQuantity ), [ cartItemQuantity ] ); @@ -75,7 +77,7 @@ export const useStoreCartItemQuantity = ( delete: false, }; } - const store = select( storeKey ); + const store = select( CART_STORE_KEY ); return { quantity: store.isItemPendingQuantity( cartItemKey ), delete: store.isItemPendingDelete( cartItemKey ), @@ -112,29 +114,35 @@ export const useStoreCartItemQuantity = ( useEffect( () => { if ( isPending.delete ) { - dispatchActions.incrementCalculating(); + incrementCalculating(); } else { - dispatchActions.decrementCalculating(); + decrementCalculating(); } return () => { if ( isPending.delete ) { - dispatchActions.decrementCalculating(); + decrementCalculating(); } }; - }, [ dispatchActions, isPending.delete ] ); + }, [ decrementCalculating, incrementCalculating, isPending.delete ] ); useEffect( () => { if ( isPending.quantity || debouncedQuantity !== quantity ) { - dispatchActions.incrementCalculating(); + incrementCalculating(); } else { - dispatchActions.decrementCalculating(); + decrementCalculating(); } return () => { if ( isPending.quantity || debouncedQuantity !== quantity ) { - dispatchActions.decrementCalculating(); + decrementCalculating(); } }; - }, [ dispatchActions, isPending.quantity, debouncedQuantity, quantity ] ); + }, [ + incrementCalculating, + decrementCalculating, + isPending.quantity, + debouncedQuantity, + quantity, + ] ); return { isPendingDelete: isPending.delete, diff --git a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts index 7e460a3eb62..c0bbee807cc 100644 --- a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts +++ b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts @@ -10,6 +10,8 @@ import { getSetting } from '@woocommerce/settings'; import deprecated from '@wordpress/deprecated'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import type { PaymentMethodInterface } from '@woocommerce/types'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -30,17 +32,29 @@ import { useShippingData } from '../shipping/use-shipping-data'; */ export const usePaymentMethodInterface = (): PaymentMethodInterface => { const { - isCalculating, - isComplete, - isIdle, - isProcessing, onCheckoutBeforeProcessing, onCheckoutValidationBeforeProcessing, onCheckoutAfterProcessingWithSuccess, onCheckoutAfterProcessingWithError, onSubmit, - customerId, } = useCheckoutContext(); + const { + isCalculating, + isComplete, + isIdle, + isProcessing, + customerId, + } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isComplete: store.isComplete(), + isIdle: store.isIdle(), + isProcessing: store.isProcessing(), + customerId: store.getCustomerId(), + isCalculating: store.isCalculating(), + }; + } ); + const { currentStatus, activePaymentMethod, diff --git a/assets/js/base/context/hooks/test/use-checkout-submit.js b/assets/js/base/context/hooks/test/use-checkout-submit.js index ff9344c657d..3309dc42855 100644 --- a/assets/js/base/context/hooks/test/use-checkout-submit.js +++ b/assets/js/base/context/hooks/test/use-checkout-submit.js @@ -8,6 +8,10 @@ import { createRegistry, RegistryProvider } from '@wordpress/data'; * Internal dependencies */ import { useCheckoutSubmit } from '../use-checkout-submit'; +import { + CHECKOUT_STORE_KEY, + config as checkoutStoreConfig, +} from '../../../../data/checkout'; const mockUseCheckoutContext = { onSubmit: jest.fn(), @@ -42,7 +46,9 @@ describe( 'useCheckoutSubmit', () => { }; beforeEach( () => { - registry = createRegistry(); + registry = createRegistry( { + [ CHECKOUT_STORE_KEY ]: checkoutStoreConfig, + } ); renderer = null; } ); diff --git a/assets/js/base/context/hooks/use-checkout-address.ts b/assets/js/base/context/hooks/use-checkout-address.ts index 4b06b7fcb0c..8bf7058c4e8 100644 --- a/assets/js/base/context/hooks/use-checkout-address.ts +++ b/assets/js/base/context/hooks/use-checkout-address.ts @@ -9,11 +9,12 @@ import { BillingAddress, } from '@woocommerce/settings'; import { useCallback } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ -import { useCheckoutContext } from '../providers/cart-checkout'; import { useCustomerData } from './use-customer-data'; import { useShippingData } from './shipping/use-shipping-data'; @@ -37,8 +38,10 @@ interface CheckoutAddress { */ export const useCheckoutAddress = (): CheckoutAddress => { const { needsShipping } = useShippingData(); - const { useShippingAsBilling, setUseShippingAsBilling } = - useCheckoutContext(); + const { useShippingAsBilling } = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).getCheckoutState() + ); + const { setUseShippingAsBilling } = useDispatch( CHECKOUT_STORE_KEY ); const { billingAddress, setBillingAddress, diff --git a/assets/js/base/context/hooks/use-checkout-extension-data.ts b/assets/js/base/context/hooks/use-checkout-extension-data.ts index a91f8e1eb20..c36b09a9fa2 100644 --- a/assets/js/base/context/hooks/use-checkout-extension-data.ts +++ b/assets/js/base/context/hooks/use-checkout-extension-data.ts @@ -1,27 +1,31 @@ /** * External dependencies */ +import { useDispatch, useSelect } from '@wordpress/data'; import { useCallback, useEffect, useRef } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ -import { useCheckoutContext } from '../providers/cart-checkout/checkout-state'; -import type { CheckoutStateContextState } from '../providers/cart-checkout/checkout-state/types'; +import type { CheckoutState } from '../../../data/checkout/types'; /** * Custom hook for setting custom checkout data which is passed to the wc/store/checkout endpoint when processing orders. */ export const useCheckoutExtensionData = (): { - extensionData: CheckoutStateContextState[ 'extensionData' ]; + extensionData: CheckoutState[ 'extensionData' ]; setExtensionData: ( namespace: string, key: string, value: unknown ) => void; } => { - const { dispatchActions, extensionData } = useCheckoutContext(); + const { setExtensionData } = useDispatch( CHECKOUT_STORE_KEY ); + const { extensionData } = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).getCheckoutState() + ); const extensionDataRef = useRef( extensionData ); useEffect( () => { @@ -33,7 +37,7 @@ export const useCheckoutExtensionData = (): { const setExtensionDataWithNamespace = useCallback( ( namespace, key, value ) => { const currentData = extensionDataRef.current[ namespace ] || {}; - dispatchActions.setExtensionData( { + setExtensionData( { ...extensionDataRef.current, [ namespace ]: { ...currentData, @@ -41,7 +45,7 @@ export const useCheckoutExtensionData = (): { }, } ); }, - [ dispatchActions ] + [ setExtensionData ] ); return { diff --git a/assets/js/base/context/hooks/use-checkout-submit.js b/assets/js/base/context/hooks/use-checkout-submit.js index 391749dd211..88737477014 100644 --- a/assets/js/base/context/hooks/use-checkout-submit.js +++ b/assets/js/base/context/hooks/use-checkout-submit.js @@ -1,12 +1,14 @@ /** * External dependencies */ +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { useCheckoutContext } from '../providers/cart-checkout/checkout-state'; +import { useCheckoutContext } from '../providers'; import { usePaymentMethodDataContext } from '../providers/cart-checkout/payment-methods'; import { usePaymentMethods } from './payment-methods/use-payment-methods'; @@ -16,14 +18,26 @@ import { usePaymentMethods } from './payment-methods/use-payment-methods'; */ export const useCheckoutSubmit = () => { const { - onSubmit, isCalculating, isBeforeProcessing, isProcessing, isAfterProcessing, isComplete, hasError, - } = useCheckoutContext(); + } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isCalculating: store.isCalculating(), + isBeforeProcessing: store.isBeforeProcessing(), + isProcessing: store.isProcessing(), + isAfterProcessing: store.isAfterProcessing(), + isComplete: store.isComplete(), + hasError: store.hasError(), + }; + } ); + + const { onSubmit } = useCheckoutContext(); + const { paymentMethods = {} } = usePaymentMethods(); const { activePaymentMethod, currentStatus: paymentStatus } = usePaymentMethodDataContext(); diff --git a/assets/js/base/context/providers/cart-checkout/cart/index.js b/assets/js/base/context/providers/cart-checkout/cart/index.js index d7cc91601ae..dfa4307b157 100644 --- a/assets/js/base/context/providers/cart-checkout/cart/index.js +++ b/assets/js/base/context/providers/cart-checkout/cart/index.js @@ -16,7 +16,7 @@ import { CheckoutProvider } from '../checkout-provider'; */ export const CartProvider = ( { children, redirectUrl } ) => { return ( - + { children } ); diff --git a/assets/js/base/context/providers/cart-checkout/checkout-processor.js b/assets/js/base/context/providers/cart-checkout/checkout-processor.js index 898d7a3ad90..07dbdfbe1aa 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-processor.js +++ b/assets/js/base/context/providers/cart-checkout/checkout-processor.js @@ -14,7 +14,8 @@ import { emptyHiddenAddressFields, formatStoreApiErrorMessage, } from '@woocommerce/base-utils'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -33,10 +34,10 @@ import { useStoreNoticesContext } from '../store-notices'; * Subscribes to checkout context and triggers processing via the API. */ const CheckoutProcessor = () => { + const { onCheckoutValidationBeforeProcessing } = useCheckoutContext(); + const { hasError: checkoutHasError, - onCheckoutValidationBeforeProcessing, - dispatchActions, redirectUrl, isProcessing: checkoutIsProcessing, isBeforeProcessing: checkoutIsBeforeProcessing, @@ -44,7 +45,20 @@ const CheckoutProcessor = () => { orderNotes, shouldCreateAccount, extensionData, - } = useCheckoutContext(); + } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + ...store.getCheckoutState(), + isProcessing: store.isProcessing(), + isBeforeProcessing: store.isBeforeProcessing(), + isComplete: store.isComplete(), + }; + } ); + + const { setCustomerId, setHasError, processCheckoutResponse } = useDispatch( + CHECKOUT_STORE_KEY + ); + const { hasValidationErrors } = useValidationContext(); const { shippingErrorStatus } = useShippingDataContext(); const { billingAddress, shippingAddress } = useCustomerDataContext(); @@ -93,7 +107,7 @@ const CheckoutProcessor = () => { ( checkoutIsProcessing || checkoutIsBeforeProcessing ) && ! isExpressPaymentMethodActive ) { - dispatchActions.setHasError( checkoutWillHaveError ); + setHasError( checkoutWillHaveError ); } }, [ checkoutWillHaveError, @@ -101,7 +115,7 @@ const CheckoutProcessor = () => { checkoutIsProcessing, checkoutIsBeforeProcessing, isExpressPaymentMethodActive, - dispatchActions, + setHasError, ] ); useEffect( () => { @@ -208,7 +222,7 @@ const CheckoutProcessor = () => { .then( ( response ) => { processCheckoutResponseHeaders( response.headers, - dispatchActions + setCustomerId ); if ( ! response.ok ) { throw new Error( response ); @@ -216,7 +230,7 @@ const CheckoutProcessor = () => { return response.json(); } ) .then( ( responseJson ) => { - dispatchActions.setAfterProcessing( responseJson ); + processCheckoutResponse( responseJson ); setIsProcessingOrder( false ); } ) .catch( ( errorResponse ) => { @@ -224,7 +238,7 @@ const CheckoutProcessor = () => { if ( errorResponse?.headers ) { processCheckoutResponseHeaders( errorResponse.headers, - dispatchActions + setCustomerId ); } // This attempts to parse a JSON error response where the status code was 4xx/5xx. @@ -250,7 +264,7 @@ const CheckoutProcessor = () => { } ); } ); - dispatchActions.setAfterProcessing( response ); + processCheckoutResponse( response ); } ); } catch { createErrorNotice( @@ -273,7 +287,7 @@ const CheckoutProcessor = () => { } ); } - dispatchActions.setHasError( true ); + setHasError( true ); setIsProcessingOrder( false ); } ); }, [ @@ -288,9 +302,11 @@ const CheckoutProcessor = () => { shouldCreateAccount, extensionData, cartNeedsShipping, - dispatchActions, createErrorNotice, receiveCart, + setHasError, + processCheckoutResponse, + setCustomerId, ] ); // process order if conditions are good. diff --git a/assets/js/base/context/providers/cart-checkout/checkout-provider.js b/assets/js/base/context/providers/cart-checkout/checkout-provider.js index 6527db49000..04dd8d6601a 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-provider.js +++ b/assets/js/base/context/providers/cart-checkout/checkout-provider.js @@ -18,21 +18,16 @@ import CheckoutProcessor from './checkout-processor'; * This wraps the checkout and provides an api interface for the checkout to * children via various hooks. * - * @param {Object} props Incoming props for the provider. - * @param {Object} props.children The children being wrapped. - * @param {boolean} [props.isCart] Whether it's rendered in the Cart - * component. - * @param {string} [props.redirectUrl] Initialize what the checkout will - * redirect to after successful - * submit. + * @param {Object} props Incoming props for the provider. + * @param {Object} props.children The children being wrapped. + * component. + * @param {string} [props.redirectUrl] Initialize what the checkout will + * redirect to after successful + * submit. */ -export const CheckoutProvider = ( { - children, - isCart = false, - redirectUrl, -} ) => { +export const CheckoutProvider = ( { children, redirectUrl } ) => { return ( - + 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 index e934a3a1361..f1b8f97c1e0 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/actions.ts @@ -1,7 +1,11 @@ +/** + * External dependencies + */ +import { PaymentResult } from '@woocommerce/types'; /** * Internal dependencies */ -import type { PaymentResultDataType, CheckoutStateContextState } from './types'; +import type { CheckoutStateContextState } from './types'; export enum ACTION { SET_IDLE = 'set_idle', @@ -26,10 +30,7 @@ export enum ACTION { export interface ActionType extends Partial< CheckoutStateContextState > { type: ACTION; - data?: - | Record< string, unknown > - | Record< string, never > - | PaymentResultDataType; + data?: Record< string, unknown > | Record< string, never > | PaymentResult; } /** @@ -53,7 +54,7 @@ export const actions = { type: ACTION.SET_REDIRECT_URL, redirectUrl, } as const ), - setProcessingResponse: ( data: PaymentResultDataType ) => + setProcessingResponse: ( data: PaymentResult ) => ( { type: ACTION.SET_PROCESSING_RESPONSE, data, 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 index 60e2eaa1188..8ab0ff575f4 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/constants.ts @@ -41,41 +41,11 @@ const checkoutData = { }; export const DEFAULT_CHECKOUT_STATE_DATA: CheckoutStateContextType = { - dispatchActions: { - resetCheckout: () => void null, - setRedirectUrl: ( url ) => void url, - setHasError: ( hasError ) => void hasError, - setAfterProcessing: ( response ) => void response, - incrementCalculating: () => void null, - decrementCalculating: () => void null, - setCustomerId: ( id ) => void id, - setOrderId: ( id ) => void id, - setOrderNotes: ( orderNotes ) => void orderNotes, - setExtensionData: ( extensionData ) => void extensionData, - }, onSubmit: () => void null, - isComplete: false, - isIdle: false, - isCalculating: false, - isProcessing: false, - isBeforeProcessing: false, - isAfterProcessing: false, - hasError: false, - redirectUrl: '', - orderId: 0, - orderNotes: '', - customerId: 0, onCheckoutAfterProcessingWithSuccess: () => () => void null, onCheckoutAfterProcessingWithError: () => () => void null, onCheckoutBeforeProcessing: () => () => void null, // deprecated for onCheckoutValidationBeforeProcessing onCheckoutValidationBeforeProcessing: () => () => void null, - hasOrder: false, - isCart: false, - useShippingAsBilling: false, - setUseShippingAsBilling: ( value ) => void value, - shouldCreateAccount: false, - setShouldCreateAccount: ( value ) => void value, - extensionData: {}, }; export const DEFAULT_STATE: CheckoutStateContextState = { diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx index b516c1d4cd9..6b77cd5f3c1 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ + import { createContext, useContext, @@ -14,23 +15,14 @@ import { __ } from '@wordpress/i18n'; import { usePrevious } from '@woocommerce/base-hooks'; import deprecated from '@wordpress/deprecated'; import { isObject, isString } from '@woocommerce/types'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ -import { actions } from './actions'; -import { reducer } from './reducer'; -import { getPaymentResultFromCheckoutResponse } from './utils'; -import { - DEFAULT_STATE, - STATUS, - DEFAULT_CHECKOUT_STATE_DATA, -} from './constants'; -import type { - CheckoutStateDispatchActions, - CheckoutStateContextType, -} from './types'; +import { STATUS, DEFAULT_CHECKOUT_STATE_DATA } from './constants'; +import type { CheckoutStateContextType } from './types'; import { EMIT_TYPES, useEventEmitters, @@ -43,10 +35,7 @@ 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'; - -/** - * @typedef {import('@woocommerce/type-defs/contexts').CheckoutDataContext} CheckoutDataContext - */ +import { CheckoutState } from '../../../../../data/checkout/default-state'; const CheckoutContext = createContext( DEFAULT_CHECKOUT_STATE_DATA ); @@ -58,29 +47,30 @@ export const useCheckoutContext = (): CheckoutStateContextType => { * Checkout state provider * This provides an API interface exposing checkout state for use with cart or checkout blocks. * - * @param {Object} props Incoming props for the provider. - * @param {Object} props.children The children being wrapped. - * @param {string} props.redirectUrl Initialize what the checkout will redirect to after successful submit. - * @param {boolean} props.isCart If context provider is being used in cart context. + * @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, - 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 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 isCalculating = checkoutState.calculatingCount > 0; const { isSuccessResponse, isErrorResponse, isFailResponse, shouldRetry } = useEmitResponse(); const { checkoutNotices, paymentNotices, expressPaymentNotices } = @@ -120,38 +110,6 @@ export const CheckoutStateProvider = ( { }; }, [ onCheckoutValidationBeforeProcessing ] ); - const dispatchActions = useMemo( - (): CheckoutStateDispatchActions => ( { - resetCheckout: () => void dispatch( actions.setPristine() ), - setRedirectUrl: ( url ) => - void dispatch( actions.setRedirectUrl( url ) ), - setHasError: ( hasError ) => - void dispatch( actions.setHasError( hasError ) ), - incrementCalculating: () => - void dispatch( actions.incrementCalculating() ), - decrementCalculating: () => - void dispatch( actions.decrementCalculating() ), - setCustomerId: ( id ) => - void dispatch( actions.setCustomerId( id ) ), - setOrderId: ( orderId ) => - void dispatch( actions.setOrderId( orderId ) ), - setOrderNotes: ( orderNotes ) => - void dispatch( actions.setOrderNotes( orderNotes ) ), - setExtensionData: ( extensionData ) => - void dispatch( actions.setExtensionData( extensionData ) ), - setAfterProcessing: ( response ) => { - const paymentResult = - getPaymentResultFromCheckoutResponse( response ); - dispatch( - actions.setRedirectUrl( paymentResult?.redirectUrl || '' ) - ); - dispatch( actions.setProcessingResponse( paymentResult ) ); - dispatch( actions.setAfterProcessing() ); - }, - } ), - [] - ); - // emit events. useEffect( () => { const status = checkoutState.status; @@ -173,10 +131,10 @@ export const CheckoutStateProvider = ( { } ); } - dispatch( actions.setIdle() ); - dispatch( actions.setHasError() ); + checkoutActions.setIdle(); + checkoutActions.setHasError(); } else { - dispatch( actions.setProcessing() ); + checkoutActions.setProcessing(); } } ); } @@ -184,7 +142,7 @@ export const CheckoutStateProvider = ( { checkoutState.status, setValidationErrors, createErrorNotice, - dispatch, + checkoutActions, ] ); const previousStatus = usePrevious( checkoutState.status ); @@ -241,9 +199,9 @@ export const CheckoutStateProvider = ( { if ( errorResponse !== null ) { // irrecoverable error so set complete if ( ! shouldRetry( errorResponse ) ) { - dispatch( actions.setComplete( errorResponse ) ); + checkoutActions.setComplete( errorResponse ); } else { - dispatch( actions.setIdle() ); + checkoutActions.setIdle(); } } else { const hasErrorNotices = @@ -274,7 +232,7 @@ export const CheckoutStateProvider = ( { } ); } - dispatch( actions.setIdle() ); + checkoutActions.setIdle(); } } ); } else { @@ -307,7 +265,7 @@ export const CheckoutStateProvider = ( { } ); if ( successResponse && ! errorResponse ) { - dispatch( actions.setComplete( successResponse ) ); + checkoutActions.setComplete( successResponse ); } else if ( isObject( errorResponse ) ) { if ( errorResponse.message && @@ -324,16 +282,16 @@ export const CheckoutStateProvider = ( { ); } if ( ! shouldRetry( errorResponse ) ) { - dispatch( actions.setComplete( 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. - dispatch( actions.setHasError( true ) ); + checkoutActions.setHasError( true ); } } else { // nothing hooked in had any response type so let's just consider successful. - dispatch( actions.setComplete() ); + checkoutActions.setComplete(); } } ); } @@ -348,7 +306,6 @@ export const CheckoutStateProvider = ( { checkoutState.processingResponse, previousStatus, previousHasError, - dispatchActions, createErrorNotice, isErrorResponse, isFailResponse, @@ -357,40 +314,20 @@ export const CheckoutStateProvider = ( { checkoutNotices, expressPaymentNotices, paymentNotices, + checkoutActions, ] ); const onSubmit = useCallback( () => { dispatchCheckoutEvent( 'submit' ); - dispatch( actions.setBeforeProcessing() ); - }, [ dispatchCheckoutEvent ] ); + checkoutActions.setBeforeProcessing(); + }, [ dispatchCheckoutEvent, checkoutActions ] ); const checkoutData: CheckoutStateContextType = { onSubmit, - isComplete: checkoutState.status === STATUS.COMPLETE, - isIdle: checkoutState.status === STATUS.IDLE, - isCalculating, - isProcessing: checkoutState.status === STATUS.PROCESSING, - isBeforeProcessing: checkoutState.status === STATUS.BEFORE_PROCESSING, - isAfterProcessing: checkoutState.status === STATUS.AFTER_PROCESSING, - hasError: checkoutState.hasError, - redirectUrl: checkoutState.redirectUrl, onCheckoutBeforeProcessing, onCheckoutValidationBeforeProcessing, onCheckoutAfterProcessingWithSuccess, onCheckoutAfterProcessingWithError, - dispatchActions, - isCart, - orderId: checkoutState.orderId, - hasOrder: !! checkoutState.orderId, - customerId: checkoutState.customerId, - orderNotes: checkoutState.orderNotes, - useShippingAsBilling: checkoutState.useShippingAsBilling, - setUseShippingAsBilling: ( value ) => - dispatch( actions.setUseShippingAsBilling( value ) ), - shouldCreateAccount: checkoutState.shouldCreateAccount, - setShouldCreateAccount: ( value ) => - dispatch( actions.setShouldCreateAccount( value ) ), - extensionData: checkoutState.extensionData, }; return ( 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 index 6bcf80e8ab6..b447105dfb0 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.ts +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/reducer.ts @@ -1,9 +1,14 @@ +/** + * External dependencies + */ +import type { PaymentResult } from '@woocommerce/types'; + /** * Internal dependencies */ import { DEFAULT_STATE, STATUS } from './constants'; import { ActionType, ACTION } from './actions'; -import type { CheckoutStateContextState, PaymentResultDataType } from './types'; +import type { CheckoutStateContextState } from './types'; /** * Reducer for the checkout state @@ -48,7 +53,7 @@ export const reducer = ( case ACTION.SET_PROCESSING_RESPONSE: newState = { ...state, - processingResponse: data as PaymentResultDataType, + processingResponse: data as PaymentResult, }; break; 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 index aa6d71f520a..a00f57277cd 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/types.ts +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/types.ts @@ -1,3 +1,7 @@ +/** + * External dependencies + */ +import { PaymentResult } from '@woocommerce/types'; /** * Internal dependencies */ @@ -26,13 +30,6 @@ export interface CheckoutResponseSuccess { export type CheckoutResponse = CheckoutResponseSuccess | CheckoutResponseError; -export interface PaymentResultDataType { - message: string; - paymentStatus: string; - paymentDetails: Record< string, string > | Record< string, never >; - redirectUrl: string; -} - type extensionDataNamespace = string; type extensionDataItem = Record< string, unknown >; export type extensionData = Record< extensionDataNamespace, extensionDataItem >; @@ -47,7 +44,7 @@ export interface CheckoutStateContextState { customerId: number; useShippingAsBilling: boolean; shouldCreateAccount: boolean; - processingResponse: PaymentResultDataType | null; + processingResponse: PaymentResult | null; extensionData: extensionData; } @@ -65,22 +62,8 @@ export type CheckoutStateDispatchActions = { }; export type CheckoutStateContextType = { - // Dispatch actions to the checkout provider. - dispatchActions: CheckoutStateDispatchActions; // Submits the checkout and begins processing. onSubmit: () => void; - // True when checkout is complete and ready for redirect. - isComplete: boolean; - // True when the checkout state has changed and checkout has no activity. - isIdle: boolean; - // True when something in the checkout is resulting in totals being calculated. - isCalculating: boolean; - // True when checkout has been submitted and is being processed. Note, payment related processing happens during this state. When payment status is success, processing happens on the server. - isProcessing: boolean; - // True during any observers executing logic before checkout processing (eg. validation). - isBeforeProcessing: boolean; - // True when checkout status is AFTER_PROCESSING. - isAfterProcessing: boolean; // Used to register a callback that will fire after checkout has been processed and there are no errors. onCheckoutAfterProcessingWithSuccess: ReturnType< typeof emitterCallback >; // Used to register a callback that will fire when the checkout has been processed and has an error. @@ -89,28 +72,4 @@ export type CheckoutStateContextType = { 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 >; - // Toggle using shipping address as billing address. - setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void; - // Set if user account should be created. - setShouldCreateAccount: ( shouldCreateAccount: boolean ) => void; - // True when the checkout has a draft order from the API. - hasOrder: boolean; - // When true, means the provider is providing data for the cart. - isCart: boolean; - // True when the checkout is in an error state. Whatever caused the error (validation/payment method) will likely have triggered a notice. - hasError: CheckoutStateContextState[ 'hasError' ]; - // This is the url that checkout will redirect to when it's ready. - redirectUrl: CheckoutStateContextState[ 'redirectUrl' ]; - // This is the ID for the draft order if one exists. - orderId: CheckoutStateContextState[ 'orderId' ]; - // Order notes introduced by the user in the checkout form. - orderNotes: CheckoutStateContextState[ 'orderNotes' ]; - // This is the ID of the customer the draft order belongs to. - customerId: CheckoutStateContextState[ 'customerId' ]; - // Should the billing form be hidden and inherit the shipping address? - useShippingAsBilling: CheckoutStateContextState[ 'useShippingAsBilling' ]; - // Should a user account be created? - shouldCreateAccount: CheckoutStateContextState[ 'shouldCreateAccount' ]; - // Custom checkout data passed to the store API on processing. - extensionData: CheckoutStateContextState[ 'extensionData' ]; }; 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 index f6402578b29..e579b166e51 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/utils.ts +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/utils.ts @@ -3,24 +3,20 @@ */ import { __ } from '@wordpress/i18n'; import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import type { PaymentResultDataType, CheckoutResponse } from './types'; +import type { PaymentResult, CheckoutResponse } from '@woocommerce/types'; /** * Prepares the payment_result data from the server checkout endpoint response. */ export const getPaymentResultFromCheckoutResponse = ( response: CheckoutResponse -): PaymentResultDataType => { +): PaymentResult => { const paymentResult = { message: '', paymentStatus: '', redirectUrl: '', paymentDetails: {}, - } as PaymentResultDataType; + } as PaymentResult; // payment_result is present in successful responses. if ( 'payment_result' in response ) { diff --git a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx index 833f71fe0e4..11c334872e4 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx +++ b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx @@ -11,7 +11,8 @@ import { useMemo, } from '@wordpress/element'; import { objectHasProp } from '@woocommerce/types'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -31,7 +32,6 @@ import { useExpressPaymentMethods, } from './use-payment-method-registration'; import { usePaymentMethodDataDispatchers } from './use-payment-method-dispatchers'; -import { useCheckoutContext } from '../checkout-state'; import { useEditorContext } from '../../editor-context'; import { EMIT_TYPES, @@ -67,7 +67,15 @@ export const PaymentMethodDataProvider = ( { isIdle: checkoutIsIdle, isCalculating: checkoutIsCalculating, hasError: checkoutHasError, - } = useCheckoutContext(); + } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isProcessing: store.isProcessing(), + isIdle: store.isIdle(), + hasError: store.hasError(), + isCalculating: store.isCalculating(), + }; + } ); const { isEditor, getPreviewData } = useEditorContext(); const { setValidationErrors } = useValidationContext(); const { createErrorNotice: addErrorNotice, removeNotice } = diff --git a/assets/js/base/context/providers/cart-checkout/shipping/index.js b/assets/js/base/context/providers/cart-checkout/shipping/index.js index e17b2ae15c4..37e31174723 100644 --- a/assets/js/base/context/providers/cart-checkout/shipping/index.js +++ b/assets/js/base/context/providers/cart-checkout/shipping/index.js @@ -9,6 +9,8 @@ import { useMemo, useRef, } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -22,7 +24,6 @@ import { reducer as emitReducer, emitEvent, } from './event-emit'; -import { useCheckoutContext } from '../checkout-state'; import { useStoreCart } from '../../../hooks/cart/use-store-cart'; import { useSelectShippingRate } from '../../../hooks/shipping/use-select-shipping-rate'; import { useShippingData } from '../../../hooks/shipping/use-shipping-data'; @@ -49,7 +50,9 @@ export const useShippingDataContext = () => { * @param {React.ReactElement} props.children */ export const ShippingDataProvider = ( { children } ) => { - const { dispatchActions } = useCheckoutContext(); + const { incrementCalculating, decrementCalculating } = useDispatch( + CHECKOUT_STORE_KEY + ); const { shippingRates, isLoadingRates, cartErrors } = useStoreCart(); const { isSelectingRate } = useSelectShippingRate(); const { selectedRates } = useShippingData(); @@ -80,20 +83,20 @@ export const ShippingDataProvider = ( { children } ) => { // increment/decrement checkout calculating counts when shipping is loading. useEffect( () => { if ( isLoadingRates ) { - dispatchActions.incrementCalculating(); + incrementCalculating(); } else { - dispatchActions.decrementCalculating(); + decrementCalculating(); } - }, [ isLoadingRates, dispatchActions ] ); + }, [ isLoadingRates, incrementCalculating, decrementCalculating ] ); // increment/decrement checkout calculating counts when shipping rates are being selected. useEffect( () => { if ( isSelectingRate ) { - dispatchActions.incrementCalculating(); + incrementCalculating(); } else { - dispatchActions.decrementCalculating(); + decrementCalculating(); } - }, [ isSelectingRate, dispatchActions ] ); + }, [ incrementCalculating, decrementCalculating, isSelectingRate ] ); // set shipping error status if there are shipping error codes useEffect( () => { diff --git a/assets/js/base/context/providers/cart-checkout/utils.ts b/assets/js/base/context/providers/cart-checkout/utils.ts index 8d3058bd911..8e636f5b3e8 100644 --- a/assets/js/base/context/providers/cart-checkout/utils.ts +++ b/assets/js/base/context/providers/cart-checkout/utils.ts @@ -6,7 +6,7 @@ import triggerFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import type { CheckoutStateDispatchActions } from './checkout-state/types'; +import type { setCustomerId as setCheckoutCustomerId } from '../../../../data/checkout/actions'; /** * Utility function for preparing payment data for the request. @@ -36,7 +36,7 @@ export const preparePaymentData = ( */ export const processCheckoutResponseHeaders = ( headers: Headers, - dispatchActions: CheckoutStateDispatchActions + setCustomerId: typeof setCheckoutCustomerId ): void => { if ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -56,8 +56,6 @@ export const processCheckoutResponseHeaders = ( // Update user using headers. if ( headers?.get( 'User-ID' ) ) { - dispatchActions.setCustomerId( - parseInt( headers.get( 'User-ID' ) || '0', 10 ) - ); + setCustomerId( parseInt( headers.get( 'User-ID' ) || '0', 10 ) ); } }; diff --git a/assets/js/base/context/tsconfig.json b/assets/js/base/context/tsconfig.json index 8de56c5a7d5..78b3b4d4549 100644 --- a/assets/js/base/context/tsconfig.json +++ b/assets/js/base/context/tsconfig.json @@ -12,7 +12,8 @@ "../../data/", "../../types/", "../components", - "../../blocks/cart-checkout-shared/payment-methods" + "../../blocks/cart-checkout-shared/payment-methods", + "../../settings/shared/default-address-fields.ts" ], "exclude": [ "**/test/**" ] } diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js index aa3509613fb..2165649d7b9 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js @@ -8,10 +8,11 @@ import { } from '@woocommerce/base-context/hooks'; import { StoreNoticesContainer, - useCheckoutContext, usePaymentMethodDataContext, } from '@woocommerce/base-context'; import LoadingMask from '@woocommerce/base-components/loading-mask'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -29,7 +30,17 @@ const CartExpressPayment = () => { isBeforeProcessing, isComplete, hasError, - } = useCheckoutContext(); + } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isCalculating: store.isCalculating(), + isProcessing: store.isProcessing(), + isAfterProcessing: store.isAfterProcessing(), + isBeforeProcessing: store.isBeforeProcessing(), + isComplete: store.isComplete(), + hasError: store.hasError(), + }; + } ); const { currentStatus: paymentStatus } = usePaymentMethodDataContext(); if ( diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js index 7f83132eedf..472f4790ef7 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js @@ -8,13 +8,14 @@ import { } from '@woocommerce/base-context/hooks'; import { StoreNoticesContainer, - useCheckoutContext, usePaymentMethodDataContext, useEditorContext, } from '@woocommerce/base-context'; import Title from '@woocommerce/base-components/title'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -30,7 +31,17 @@ const CheckoutExpressPayment = () => { isBeforeProcessing, isComplete, hasError, - } = useCheckoutContext(); + } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isCalculating: store.isCalculating(), + isProcessing: store.isProcessing(), + isAfterProcessing: store.isAfterProcessing(), + isBeforeProcessing: store.isBeforeProcessing(), + isComplete: store.isComplete(), + hasError: store.hasError(), + }; + } ); const { currentStatus: paymentStatus } = usePaymentMethodDataContext(); const { paymentMethods, isInitialized } = useExpressPaymentMethods(); const { isEditor } = useEditorContext(); diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-card.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-card.js index 57210eef3d6..9d9471cb6e4 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-card.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-card.js @@ -3,12 +3,13 @@ */ import { __ } from '@wordpress/i18n'; import { - useCheckoutContext, useEditorContext, usePaymentMethodDataContext, } from '@woocommerce/base-context'; import { CheckboxControl } from '@woocommerce/blocks-checkout'; import PropTypes from 'prop-types'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -27,10 +28,13 @@ import PaymentMethodErrorBoundary from './payment-method-error-boundary'; */ const PaymentMethodCard = ( { children, showSaveOption } ) => { const { isEditor } = useEditorContext(); - const { shouldSavePayment, setShouldSavePayment } = - usePaymentMethodDataContext(); - const { customerId } = useCheckoutContext(); - + const { + shouldSavePayment, + setShouldSavePayment, + } = usePaymentMethodDataContext(); + const customerId = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).getCustomerId() + ); return ( { children } diff --git a/assets/js/blocks/cart/inner-blocks/proceed-to-checkout-block/block.tsx b/assets/js/blocks/cart/inner-blocks/proceed-to-checkout-block/block.tsx index c2aea4d84bb..fb6db923c85 100644 --- a/assets/js/blocks/cart/inner-blocks/proceed-to-checkout-block/block.tsx +++ b/assets/js/blocks/cart/inner-blocks/proceed-to-checkout-block/block.tsx @@ -6,9 +6,10 @@ import classnames from 'classnames'; import { useState, useEffect } from '@wordpress/element'; import Button from '@woocommerce/base-components/button'; import { CHECKOUT_URL } from '@woocommerce/block-settings'; -import { useCheckoutContext } from '@woocommerce/base-context'; import { usePositionRelativeToViewport } from '@woocommerce/base-hooks'; import { getSetting } from '@woocommerce/settings'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -26,9 +27,13 @@ const Block = ( { className: string; } ): JSX.Element => { const link = getSetting( 'page-' + checkoutPageId, false ); - const { isCalculating } = useCheckoutContext(); - const [ positionReferenceElement, positionRelativeToViewport ] = - usePositionRelativeToViewport(); + const isCalculating = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isCalculating() + ); + const [ + positionReferenceElement, + positionRelativeToViewport, + ] = usePositionRelativeToViewport(); const [ showSpinner, setShowSpinner ] = useState( false ); useEffect( () => { diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index cb4471b6665..85a700d3051 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -6,7 +6,6 @@ import classnames from 'classnames'; import { createInterpolateElement, useEffect } from '@wordpress/element'; import { useStoreCart } from '@woocommerce/base-context/hooks'; import { - useCheckoutContext, useValidationContext, ValidationContextProvider, CheckoutProvider, @@ -18,6 +17,8 @@ import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout'; import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings'; import { SlotFillProvider } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -55,7 +56,13 @@ const Checkout = ( { attributes: Attributes; children: React.ReactChildren; } ): JSX.Element => { - const { hasOrder, customerId } = useCheckoutContext(); + const { hasOrder, customerId } = useSelect( ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + hasOrder: store.hasOrder(), + customerId: store.getCustomerId(), + }; + } ); const { cartItems, cartIsLoading } = useStoreCart(); const { @@ -104,10 +111,19 @@ const ScrollOnError = ( { }: { scrollToTop: ( props: Record< string, unknown > ) => void; } ): null => { - const { hasError: checkoutHasError, isIdle: checkoutIsIdle } = - useCheckoutContext(); - const { hasValidationErrors, showAllValidationErrors } = - useValidationContext(); + const { hasError: checkoutHasError, isIdle: checkoutIsIdle } = useSelect( + ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isIdle: store.isIdle(), + hasError: store.hasError(), + }; + } + ); + const { + hasValidationErrors, + showAllValidationErrors, + } = useValidationContext(); const hasErrorsToDisplay = checkoutIsIdle && diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/frontend.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/frontend.tsx index cdb8f702e99..86b4ed1c746 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/frontend.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-billing-address-block/frontend.tsx @@ -4,8 +4,9 @@ import classnames from 'classnames'; import { withFilteredAttributes } from '@woocommerce/shared-hocs'; import { FormStep } from '@woocommerce/base-components/cart-checkout'; -import { useCheckoutContext } from '@woocommerce/base-context'; import { useCheckoutAddress } from '@woocommerce/base-context/hooks'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -27,7 +28,9 @@ const FrontendBlock = ( { children: JSX.Element; className?: string; } ): JSX.Element | null => { - const { isProcessing: checkoutIsProcessing } = useCheckoutContext(); + const checkoutIsProcessing = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isProcessing() + ); const { requireCompanyField, requirePhoneField, diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx index 9c1d5b3e27e..b1bac5c1599 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx @@ -3,13 +3,11 @@ */ import { __ } from '@wordpress/i18n'; import { ValidatedTextInput } from '@woocommerce/base-components/text-input'; -import { - useCheckoutContext, - useCheckoutAddress, - useStoreEvents, -} from '@woocommerce/base-context'; +import { useCheckoutAddress, useStoreEvents } from '@woocommerce/base-context'; import { getSetting } from '@woocommerce/settings'; import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -20,12 +18,15 @@ const Block = ( { }: { allowCreateAccount: boolean; } ): JSX.Element => { - const { customerId, shouldCreateAccount, setShouldCreateAccount } = - useCheckoutContext(); + const { customerId, shouldCreateAccount } = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).getCheckoutState() + ); + + const { setShouldCreateAccount } = useDispatch( CHECKOUT_STORE_KEY ); const { billingAddress, setEmail } = useCheckoutAddress(); const { dispatchCheckoutEvent } = useStoreEvents(); - const onChangeEmail = ( value ) => { + const onChangeEmail = ( value: string ) => { setEmail( value ); dispatchCheckoutEvent( 'set-email-address' ); }; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/frontend.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/frontend.tsx index 3d772448fc6..3594226c4d7 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/frontend.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/frontend.tsx @@ -4,7 +4,8 @@ import classnames from 'classnames'; import { withFilteredAttributes } from '@woocommerce/shared-hocs'; import { FormStep } from '@woocommerce/base-components/cart-checkout'; -import { useCheckoutContext } from '@woocommerce/base-context'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -28,7 +29,9 @@ const FrontendBlock = ( { children: JSX.Element; className?: string; } ) => { - const { isProcessing: checkoutIsProcessing } = useCheckoutContext(); + const checkoutIsProcessing = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isProcessing() + ); const { allowCreateAccount } = useCheckoutBlockContext(); return ( diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js index 747444f37f5..6a1df4a19b5 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js +++ b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/login-prompt.js @@ -3,15 +3,18 @@ */ import { __ } from '@wordpress/i18n'; import { getSetting } from '@woocommerce/settings'; -import { useCheckoutContext } from '@woocommerce/base-context'; import { LOGIN_URL } from '@woocommerce/block-settings'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; const LOGIN_TO_CHECKOUT_URL = `${ LOGIN_URL }?redirect_to=${ encodeURIComponent( window.location.href ) }`; const LoginPrompt = () => { - const { customerId } = useCheckoutContext(); + const customerId = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).getCustomerId() + ); if ( ! getSetting( 'checkoutShowLoginReminder', true ) || customerId ) { return null; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/block.tsx index 9b31a5ac91f..7da20c09fc0 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-order-note-block/block.tsx @@ -4,8 +4,9 @@ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { FormStep } from '@woocommerce/base-components/cart-checkout'; -import { useCheckoutContext } from '@woocommerce/base-context'; import { useShippingData } from '@woocommerce/base-context/hooks'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -14,12 +15,16 @@ import CheckoutOrderNotes from '../../order-notes'; const Block = ( { className }: { className?: string } ): JSX.Element => { const { needsShipping } = useShippingData(); - const { - isProcessing: checkoutIsProcessing, - orderNotes, - dispatchActions, - } = useCheckoutContext(); - const { setOrderNotes } = dispatchActions; + const { isProcessing: checkoutIsProcessing, orderNotes } = useSelect( + ( select ) => { + const store = select( CHECKOUT_STORE_KEY ); + return { + isProcessing: store.isProcessing(), + orderNotes: store.getOrderNotes(), + }; + } + ); + const { setOrderNotes } = useDispatch( CHECKOUT_STORE_KEY ); return ( { - const { isProcessing: checkoutIsProcessing } = useCheckoutContext(); + const checkoutIsProcessing = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isProcessing() + ); const { cartNeedsPayment } = useStoreCart(); const { noticeContexts } = useEmitResponse(); diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/frontend.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/frontend.tsx index 9756834dd9a..08a4fd383f2 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/frontend.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/frontend.tsx @@ -4,9 +4,9 @@ import classnames from 'classnames'; import { withFilteredAttributes } from '@woocommerce/shared-hocs'; import { FormStep } from '@woocommerce/base-components/cart-checkout'; -import { useCheckoutContext } from '@woocommerce/base-context'; import { useCheckoutAddress } from '@woocommerce/base-context/hooks'; - +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ @@ -27,7 +27,9 @@ const FrontendBlock = ( { children: JSX.Element; className?: string; } ) => { - const { isProcessing: checkoutIsProcessing } = useCheckoutContext(); + const checkoutIsProcessing = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isProcessing() + ); const { showShippingFields } = useCheckoutAddress(); const { requireCompanyField, diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx index a9c8d90f7ac..c4041c30046 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/frontend.tsx @@ -4,8 +4,9 @@ import classnames from 'classnames'; import { withFilteredAttributes } from '@woocommerce/shared-hocs'; import { FormStep } from '@woocommerce/base-components/cart-checkout'; -import { useCheckoutContext } from '@woocommerce/base-context'; import { useCheckoutAddress } from '@woocommerce/base-context/hooks'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -31,7 +32,9 @@ const FrontendBlock = ( { children: JSX.Element; className?: string; } ) => { - const { isProcessing: checkoutIsProcessing } = useCheckoutContext(); + const checkoutIsProcessing = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isProcessing() + ); const { showShippingFields } = useCheckoutAddress(); if ( ! showShippingFields ) { diff --git a/assets/js/data/checkout/action-types.ts b/assets/js/data/checkout/action-types.ts new file mode 100644 index 00000000000..2fb0a662614 --- /dev/null +++ b/assets/js/data/checkout/action-types.ts @@ -0,0 +1,21 @@ +export const ACTION_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_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', + SET_IS_CART: 'SET_IS_CART', +} as const; diff --git a/assets/js/data/checkout/actions.ts b/assets/js/data/checkout/actions.ts new file mode 100644 index 00000000000..c611309a2c0 --- /dev/null +++ b/assets/js/data/checkout/actions.ts @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import { CheckoutResponse, 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'; + +export const setPristine = () => ( { + type: types.SET_PRISTINE, +} ); + +export const setIdle = () => ( { + type: types.SET_IDLE, +} ); + +export const setProcessing = () => ( { + type: types.SET_PROCESSING, +} ); + +export const setRedirectUrl = ( redirectUrl: string ) => ( { + type: types.SET_REDIRECT_URL, + redirectUrl, +} ); + +export const setProcessingResponse = ( data: PaymentResult ) => ( { + type: types.SET_PROCESSING_RESPONSE, + data, +} ); + +export const setComplete = ( data: Record< string, unknown > = {} ) => ( { + type: types.SET_COMPLETE, + data, +} ); + +export const setBeforeProcessing = () => ( { + type: types.SET_BEFORE_PROCESSING, +} ); + +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, +} ); + +export const incrementCalculating = () => ( { + type: types.INCREMENT_CALCULATING, +} ); + +export const decrementCalculating = () => ( { + type: types.DECREMENT_CALCULATING, +} ); + +export const setCustomerId = ( customerId: number ) => ( { + type: types.SET_CUSTOMER_ID, + customerId, +} ); + +export const setOrderId = ( orderId: number ) => ( { + type: types.SET_ORDER_ID, + orderId, +} ); + +export const setUseShippingAsBilling = ( useShippingAsBilling: boolean ) => ( { + type: types.SET_SHIPPING_ADDRESS_AS_BILLING_ADDRESS, + useShippingAsBilling, +} ); + +export const setShouldCreateAccount = ( shouldCreateAccount: boolean ) => ( { + type: types.SET_SHOULD_CREATE_ACCOUNT, + shouldCreateAccount, +} ); + +export const setOrderNotes = ( orderNotes: string ) => ( { + type: types.SET_ORDER_NOTES, + orderNotes, +} ); + +export const setExtensionData = ( + extensionData: Record< string, Record< string, unknown > > +) => ( { + type: types.SET_EXTENSION_DATA, + extensionData, +} ); + +type Action = ReturnType< + | typeof setPristine + | typeof setIdle + | typeof setComplete + | typeof setProcessing + | typeof setProcessingResponse + | typeof setBeforeProcessing + | typeof setAfterProcessing + | typeof setRedirectUrl + | typeof setHasError + | typeof incrementCalculating + | typeof decrementCalculating + | typeof setCustomerId + | typeof setOrderId + | typeof setUseShippingAsBilling + | typeof setShouldCreateAccount + | typeof setOrderNotes + | typeof setExtensionData +>; diff --git a/assets/js/data/checkout/constants.ts b/assets/js/data/checkout/constants.ts new file mode 100644 index 00000000000..ee1950d96a6 --- /dev/null +++ b/assets/js/data/checkout/constants.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { getSetting, EnteredAddress } from '@woocommerce/settings'; + +import { CheckoutResponseSuccess } from '@woocommerce/types'; + +export const STORE_KEY = 'wc/store/checkout'; + +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 Partial< + CheckoutResponseSuccess +>; + +export const checkoutData = { + order_id: 0, + customer_id: 0, + billing_address: {} as EnteredAddress, + shipping_address: {} as EnteredAddress, + ...( preloadedCheckoutData || {} ), +}; diff --git a/assets/js/data/checkout/default-state.ts b/assets/js/data/checkout/default-state.ts new file mode 100644 index 00000000000..3b3351ae4d1 --- /dev/null +++ b/assets/js/data/checkout/default-state.ts @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { isSameAddress } from '@woocommerce/base-utils'; +import { PaymentResult } from '@woocommerce/types'; + +/** + * Internal dependencies + */ +import { STATUS, checkoutData } from './constants'; + +export type CheckoutState = { + // Status of the checkout + status: STATUS; + // If any of the totals, taxes, shipping, etc need to be calculated, the count will be increased here + calculatingCount: number; + // The result of the payment processing + processingResponse: PaymentResult | null; + // 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; + // Should the billing form be hidden and inherit the shipping address? + useShippingAsBilling: boolean; + // Should a user account be created? + shouldCreateAccount: boolean; + // Custom checkout data passed to the store API on processing. + extensionData: Record< string, Record< string, unknown > >; +}; + +export const defaultState: CheckoutState = { + redirectUrl: '', + status: STATUS.PRISTINE, + hasError: false, + orderId: checkoutData.order_id, + customerId: checkoutData.customer_id, + calculatingCount: 0, + orderNotes: '', + useShippingAsBilling: isSameAddress( + checkoutData.billing_address, + checkoutData.shipping_address + ), + shouldCreateAccount: false, + processingResponse: null, + extensionData: {}, +}; diff --git a/assets/js/data/checkout/index.ts b/assets/js/data/checkout/index.ts new file mode 100644 index 00000000000..9237358082a --- /dev/null +++ b/assets/js/data/checkout/index.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from './constants'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import reducer from './reducers'; + +export const config = { + reducer, + selectors, + actions, + __experimentalUseThunks: true, +}; + +registerStore( STORE_KEY, config ); + +export const CHECKOUT_STORE_KEY = STORE_KEY; diff --git a/assets/js/data/checkout/reducers.ts b/assets/js/data/checkout/reducers.ts new file mode 100644 index 00000000000..c1e1108c13d --- /dev/null +++ b/assets/js/data/checkout/reducers.ts @@ -0,0 +1,185 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; + +import { PaymentResult } from '@woocommerce/types'; +/** + * Internal dependencies + */ +import { ACTION_TYPES as types } from './action-types'; +import { STATUS } from './constants'; +import { defaultState } from './default-state'; + +const reducer: Reducer = ( state = defaultState, action ) => { + let newState = state; + switch ( action.type ) { + case types.SET_PRISTINE: + newState = defaultState; + break; + + case types.SET_IDLE: + newState = + state.status !== STATUS.IDLE + ? { + ...state, + status: STATUS.IDLE, + } + : state; + break; + + case types.SET_REDIRECT_URL: + newState = + action.redirectUrl !== undefined && + action.redirectUrl !== state.redirectUrl + ? { + ...state, + redirectUrl: action.redirectUrl, + } + : state; + break; + + case types.SET_PROCESSING_RESPONSE: + newState = { + ...state, + processingResponse: action.data as PaymentResult, + }; + break; + + case types.SET_COMPLETE: + newState = { + ...state, + status: STATUS.COMPLETE, + redirectUrl: + typeof action.data?.redirectUrl === 'string' + ? action.data.redirectUrl + : state.redirectUrl, + }; + break; + case types.SET_PROCESSING: + newState = { + ...state, + status: STATUS.PROCESSING, + hasError: false, + }; + break; + + case types.SET_BEFORE_PROCESSING: + newState = { + ...state, + status: STATUS.BEFORE_PROCESSING, + hasError: false, + }; + break; + + case types.SET_AFTER_PROCESSING: + newState = { + ...state, + status: STATUS.AFTER_PROCESSING, + }; + break; + + case types.SET_HAS_ERROR: + newState = { + ...state, + hasError: action.hasError, + status: + state.status === STATUS.PROCESSING || + state.status === STATUS.BEFORE_PROCESSING + ? STATUS.IDLE + : state.status, + }; + break; + + case types.INCREMENT_CALCULATING: + newState = { + ...state, + calculatingCount: state.calculatingCount + 1, + }; + break; + + case types.DECREMENT_CALCULATING: + newState = { + ...state, + calculatingCount: Math.max( 0, state.calculatingCount - 1 ), + }; + break; + + case types.SET_CUSTOMER_ID: + if ( action.customerId !== undefined ) { + newState = { + ...state, + customerId: action.customerId, + }; + } + break; + + case types.SET_ORDER_ID: + if ( action.orderId !== undefined ) { + newState = { + ...state, + orderId: action.orderId, + }; + } + break; + + case types.SET_SHIPPING_ADDRESS_AS_BILLING_ADDRESS: + if ( + action.useShippingAsBilling !== undefined && + action.useShippingAsBilling !== state.useShippingAsBilling + ) { + newState = { + ...state, + useShippingAsBilling: action.useShippingAsBilling, + }; + } + break; + + case types.SET_SHOULD_CREATE_ACCOUNT: + if ( + action.shouldCreateAccount !== undefined && + action.shouldCreateAccount !== state.shouldCreateAccount + ) { + newState = { + ...state, + shouldCreateAccount: action.shouldCreateAccount, + }; + } + break; + + case types.SET_ORDER_NOTES: + if ( + action.orderNotes !== undefined && + state.orderNotes !== action.orderNotes + ) { + newState = { + ...state, + orderNotes: action.orderNotes, + }; + } + break; + + case types.SET_EXTENSION_DATA: + if ( + action.extensionData !== undefined && + state.extensionData !== action.extensionData + ) { + newState = { + ...state, + extensionData: action.extensionData, + }; + } + break; + } + + if ( + newState !== state && + action.type !== types.SET_PRISTINE && + newState?.status === STATUS.PRISTINE + ) { + newState.status = STATUS.IDLE; + } + return newState; +}; + +export default reducer; diff --git a/assets/js/data/checkout/selectors.ts b/assets/js/data/checkout/selectors.ts new file mode 100644 index 00000000000..d1809288470 --- /dev/null +++ b/assets/js/data/checkout/selectors.ts @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import { STATUS } from './constants'; +import { CheckoutState } from './default-state'; + +export const getCustomerId = ( state: CheckoutState ) => { + return state.customerId; +}; + +export const getOrderNotes = ( state: CheckoutState ) => { + return state.orderNotes; +}; + +export const hasError = ( state: CheckoutState ) => { + return state.hasError; +}; + +export const hasOrder = ( state: CheckoutState ) => { + return !! state.orderId; +}; + +export const isComplete = ( state: CheckoutState ) => { + return state.status === STATUS.COMPLETE; +}; + +export const isIdle = ( state: CheckoutState ) => { + return state.status === STATUS.IDLE; +}; + +export const isBeforeProcessing = ( state: CheckoutState ) => { + return state.status === STATUS.BEFORE_PROCESSING; +}; + +export const isAfterProcessing = ( state: CheckoutState ) => { + return state.status === STATUS.AFTER_PROCESSING; +}; + +export const isProcessing = ( state: CheckoutState ) => { + return state.status === STATUS.PROCESSING; +}; + +export const isCalculating = ( state: CheckoutState ) => { + return state.calculatingCount > 0; +}; + +export const getCheckoutState = ( state: CheckoutState ) => state; diff --git a/assets/js/data/index.ts b/assets/js/data/index.ts index 9615636ac5f..767f979cf01 100644 --- a/assets/js/data/index.ts +++ b/assets/js/data/index.ts @@ -9,5 +9,7 @@ import '@wordpress/notices'; export { SCHEMA_STORE_KEY } from './schema'; export { COLLECTIONS_STORE_KEY } from './collections'; export { CART_STORE_KEY } from './cart'; +export { CHECKOUT_STORE_KEY } from './checkout'; export { QUERY_STATE_STORE_KEY } from './query-state'; export * from './constants'; +export * from './types'; diff --git a/assets/js/data/tsconfig.json b/assets/js/data/tsconfig.json index 167c48669f0..64a7b9178d0 100644 --- a/assets/js/data/tsconfig.json +++ b/assets/js/data/tsconfig.json @@ -6,7 +6,8 @@ "../types/type-defs/**.ts", "../mapped-types.ts", "../settings/shared/index.js", - "../settings/blocks/index.js" + "../settings/blocks/index.js", + "../base/context/providers/cart-checkout/checkout-state/utils.ts" ], "exclude": [ "**/test/**" ] } diff --git a/assets/js/types/type-defs/checkout.ts b/assets/js/types/type-defs/checkout.ts new file mode 100644 index 00000000000..ef53a1862b3 --- /dev/null +++ b/assets/js/types/type-defs/checkout.ts @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import { EnteredAddress } from '../../settings/shared/default-address-fields'; + +export interface CheckoutResponseSuccess { + billing_address: EnteredAddress; + customer_id: number; + customer_note: string; + extensions: Record< string, unknown >; + order_id: number; + order_key: string; + payment_method: string; + payment_result: { + payment_details: Record< string, string > | Record< string, never >; + payment_status: 'success' | 'failure' | 'pending' | 'error'; + redirect_url: string; + }; + shipping_address: EnteredAddress; + status: string; +} + +export interface CheckoutResponseError { + code: string; + message: string; + data: { + status: number; + }; +} + +export type CheckoutResponse = CheckoutResponseSuccess | CheckoutResponseError; diff --git a/assets/js/types/type-defs/index.ts b/assets/js/types/type-defs/index.ts index a24730b4f51..4b2fe4bc849 100644 --- a/assets/js/types/type-defs/index.ts +++ b/assets/js/types/type-defs/index.ts @@ -1,12 +1,13 @@ -export * from './cart-response'; -export * from './product-response'; +export * from './blocks'; export * from './cart'; -export * from './hooks'; +export * from './cart-response'; +export * from './checkout'; export * from './currency'; -export * from './payments'; +export * from './hooks'; export * from './objects'; +export * from './payments'; export * from './payment-method-interface'; -export * from './blocks'; +export * from './product-response'; export * from './utils'; export * from './taxes'; export * from './attributes'; diff --git a/assets/js/types/type-defs/payments.ts b/assets/js/types/type-defs/payments.ts index 87f5742058a..b0edfe2534f 100644 --- a/assets/js/types/type-defs/payments.ts +++ b/assets/js/types/type-defs/payments.ts @@ -118,3 +118,10 @@ export interface ExpressPaymentMethodConfigInstance { canMakePaymentFromConfig: CanMakePaymentCallback; canMakePayment: CanMakePaymentCallback; } + +export interface PaymentResult { + message: string; + paymentStatus: string; + paymentDetails: Record< string, string > | Record< string, never >; + redirectUrl: string; +} diff --git a/docs/internal-developers/block-client-apis/checkout/checkout-api.md b/docs/internal-developers/block-client-apis/checkout/checkout-api.md index ff4e9996b49..8773581dc8c 100644 --- a/docs/internal-developers/block-client-apis/checkout/checkout-api.md +++ b/docs/internal-developers/block-client-apis/checkout/checkout-api.md @@ -2,18 +2,86 @@ ## Table of contents -- [Contexts](#contexts) - - [Notices Context](#notices-context) - - [Customer Data Context](#customer-data-context) - - [Shipping Method Data context](#shipping-method-data-context) - - [Payment Method Data Context](#payment-method-data-context) - - [Checkout Context](#checkout-context) -- [Hooks](#hooks) - - [`usePaymentMethodInterface`](#usepaymentmethodinterface) +**Note on migration:** We are in the process of moving much of the data from contexts into data stores, so this portion of the docs may change often as we do this. We will endavour to keep it up to date while the work is carried out -This document gives an overview of some of the major architectural components/APIs for the checkout block. If you haven't already, you may also want to read about the [Checkout Flow and Events](../../../internal-developers/block-client-apis/checkout/checkout-flow-and-events.md). +- [Checkout Block API overview](#checkout-block-api-overview) + - [Data Stores](#data-stores) + - [Checkout Data Store](#checkout-data-store) + - [Contexts](#contexts) + - [Notices Context](#notices-context) + - [Customer Data Context](#customer-data-context) + - [Billing Data Context](#billing-data-context) + - [Shipping Method Data context](#shipping-method-data-context) + - [Payment Method Data Context](#payment-method-data-context) + - [Checkout Context](#checkout-context) + - [Hooks](#hooks) + - [`usePaymentMethodInterface`](#usepaymentmethodinterface) -## Contexts +This document gives an overview of some of the major architectural components/APIs for the checkout block. If you haven't already, you may also want to read about the [Checkout Flow and Events](../../extensibility/checkout-flow-and-events.md). + +### Data Stores + +We are transitioning much of what is now available in Contexts, to `@wordpress/data` stores. + +#### Checkout Data Store + +This is responsible for holding all the data required for the checkout process. + +The following data is available: + +- `status`: The status of the current checkout. Possible values are `pristine`, `idle`, `processing`, `complete`, `before_processing` or `after_processing` +- `hasError`: This is true when anything in the checkout has created an error condition state. This might be validation errors, request errors, coupon application errors, payment processing errors etc. +- `redirectUrl`: The current set url that the checkout will redirect to when it is complete. +- `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 +- `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 +- `orderNotes`: Order notes introduced by the user in the checkout form. + +##### Selectors + +Data can be accessed through the following selectors: + +- `getCheckoutState()`: Returns all the data above. +- `isComplete()`: True when checkout has finished processing and the subscribed checkout processing callbacks have all been invoked along with a successful processing of the checkout by the server. +- `isIdle()`: When the checkout status is `IDLE` this flag is true. Checkout will be this status after any change to checkout state after the block is loaded. It will also be this status when retrying a purchase is possible after processing happens with an error. +- `isBeforeProcessing()`: When the checkout status is `BEFORE_PROCESSING` this flag is true. Checkout will be this status when the user submits checkout for processing. +- `isProcessing()`: When the checkout status is `PROCESSING` this flag is true. Checkout will be this status when all the observers on the event emitted with the `BEFORE_PROCESSING` status are completed without error. It is during this status that the block will be sending a request to the server on the checkout endpoint for processing the order. +- `isAfterProcessing()`: When the checkout status is `AFTER_PROCESSING` this flag is true. Checkout will have this status after the the block receives the response from the server side processing request. +- `isComplete()`: When the checkout status is `COMPLETE` this flag is true. Checkout will have this status after all observers on the events emitted during the `AFTER_PROCESSING` status are completed successfully. When checkout is at this status, the shopper's browser will be redirected to the value of `redirectUrl` at that point (usually the `order-received` route). +- `isCalculating()`: This is true when the total is being re-calculated for the order. There are numerous things that might trigger a recalculation of the total: coupons being added or removed, shipping rates updated, shipping rate selected etc. This flag consolidates all activity that might be occurring (including requests to the server that potentially affect calculation of totals). So instead of having to check each of those individual states you can reliably just check if this boolean is true (calculating) or false (not calculating). +- `hasOrder()`: This is true when orderId is truthy. +- `hasError()`: This is true when the checkout has an error. +- `getOrderNotes()`: Returns the order notes. +- `getCustomerId()`: Returns the customer ID. + +##### Actions + +The following actions can be dispatched from the Checkout data store: + +- `setPristine()`: Set `state.status` to `pristine` +- `setIdle()`: Set `state.status` to `idle` +- `setComplete()`: Set `state.status` to `complete` +- `setProcessing()`: Set `state.status` to `processing` +- `setProcessingResponse( response: PaymentResult )`: Set `state.processingResponse` to `response` +- `setBeforeProcessing()`: Set `state.status` to `before_processing` +- `setAfterProcessing()`: Set `state.status` to `after_processing` +- `processCheckoutResponse( response: CheckoutResponse )`: This is a thunk that will extract the paymentResult from the CheckoutResponse, and dispatch 3 actions: `setRedirectUrl`, `setProcessingResponse` and `setAfterProcessing`. +- `setRedirectUrl( url: string )`: Set `state.redirectUrl` to `url` +- `setHasError( trueOrFalse: bool )`: Set `state.hasError` to `trueOrFalse` +- `incrementCalculating()`: Increment `state.calculatingCount` +- `decrementCalculating()`: Decrement `state.calculatingCount` +- `setCustomerId( id: number )`: Set `state.customerId` to `id` +- `setOrderId( id: number )`: Set `state.orderId` to `id` +- `setUseShippingAsBilling( useShippingAsBilling: boolean )`: Set `state.useShippingAsBilling` to `useShippingAsBilling` +- `setShouldCreateAccount( shouldCreateAccount: boolean )`: Set `state.shouldCreateAccount` to `shouldCreateAccount` +- `setOrderNotes( orderNotes: string )`: Set `state.orderNotes` to `orderNotes` +- `setExtensionData( extensionData: Record< string, Record< string, unknown > > )`: Set `state.extensionData` to `extensionData` + +### Contexts Much of the data and api interface for components in the Checkout Block are constructed and exposed via [usage of `React.Context`](https://reactjs.org/docs/context.html). In some cases the context maintains the "tree" state within the context itself (via `useReducer`) and in others it interacts with a global `wp.data` store (for data that communicates with the server). @@ -123,4 +191,3 @@ The contract is established through props fed to the payment method components v 🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce-blocks/issues/new?assignees=&labels=type%3A+documentation&template=--doc-feedback.md&title=Feedback%20on%20./docs/internal-developers/block-client-apis/checkout/checkout-api.md) - diff --git a/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md b/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md index 1b66f393b2b..9bbf8a3aaf2 100644 --- a/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md +++ b/docs/internal-developers/block-client-apis/checkout/checkout-flow-and-events.md @@ -50,17 +50,20 @@ To surface the flow state, the block uses statuses that are tracked in the vario The following statuses exist in the Checkout. -### `CheckoutProvider` Exposed Statuses +#### Checkout Data Store Exposed Statuses -You can find all the checkout provider statuses defined [in this typedef](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/34e17c3622637dbe8b02fac47b5c9b9ebf9e3596/assets/js/type-defs/checkout.js#L21-L38). +There are various statuses that are exposed on the Checkout data store via selectors. All the selectors are detailed below and in the [Checkout API docs](https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/block-client-apis/checkout/checkout-api.md). -They are exposed to children components via the `useCheckoutContext` via various boolean flags. For instance you can access the `isComplete` flag by doing something like this in your component: +You can use them in your component like so ```jsx -import { useCheckoutContext } from '@woocommerce/base-contexts'; +import { useSelect } from '@wordpress/data'; +import { CHECKOUT_STORE_KEY } from '@woocommerce/blocks-data'; const MyComponent = ( props ) => { - const { isComplete } = useCheckoutContext(); + const isComplete = useSelect( ( select ) => + select( CHECKOUT_STORE_KEY ).isComplete() + ); // do something with isComplete }; ```