From b0d9a837c998759a1d8da46a9972c66e8282e23b Mon Sep 17 00:00:00 2001 From: David Chin Date: Wed, 24 Sep 2025 10:31:51 +1000 Subject: [PATCH 1/2] feat(core): CHECKOUT-9513 Add option to pass initial state during initialisation --- .../src/billing/billing-address-reducer.ts | 12 ++++++++-- packages/core/src/cart/cart-reducer.ts | 13 ++++++++-- .../src/checkout/checkout-hydrate-actions.ts | 11 +++++++++ .../src/checkout/checkout-initial-state.ts | 12 ++++++++++ .../core/src/checkout/checkout-reducer.ts | 17 ++++++++++++- .../checkout/create-checkout-service.spec.ts | 24 +++++++++++++++++++ .../src/checkout/create-checkout-service.ts | 4 +++- .../src/checkout/create-checkout-store.ts | 24 ++++++++++++++----- packages/core/src/checkout/index.ts | 1 + packages/core/src/config/config-reducer.ts | 9 ++++++- packages/core/src/config/config-selector.ts | 15 ++++++------ packages/core/src/config/config.ts | 2 ++ packages/core/src/config/configs.mock.ts | 2 ++ packages/core/src/coupon/coupon-reducer.ts | 12 ++++++++-- .../src/coupon/gift-certificate-reducer.ts | 17 +++++++++++-- .../core/src/customer/customer-reducer.ts | 13 ++++++++-- .../core/src/extension/extension-reducer.ts | 10 +++++++- packages/core/src/form/form-fields-reducer.ts | 9 ++++++- .../core/src/shipping/consignment-reducer.ts | 17 +++++++++++-- .../src/config/config.ts | 8 +++++++ .../src/mocks/config.mock.ts | 6 +++++ .../src/test-utils/config.mock.ts | 6 +++++ 22 files changed, 214 insertions(+), 30 deletions(-) create mode 100644 packages/core/src/checkout/checkout-hydrate-actions.ts create mode 100644 packages/core/src/checkout/checkout-initial-state.ts diff --git a/packages/core/src/billing/billing-address-reducer.ts b/packages/core/src/billing/billing-address-reducer.ts index ef458972cb..d30743c461 100644 --- a/packages/core/src/billing/billing-address-reducer.ts +++ b/packages/core/src/billing/billing-address-reducer.ts @@ -1,6 +1,11 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; -import { CheckoutAction, CheckoutActionType } from '../checkout'; +import { + CheckoutAction, + CheckoutActionType, + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout'; import { clearErrorReducer } from '../common/error'; import { objectSet, replace } from '../common/utility'; import { OrderAction } from '../order'; @@ -29,7 +34,7 @@ export default function billingAddressReducer( function dataReducer( data: BillingAddress | undefined, - action: CheckoutAction | BillingAddressAction | OrderAction, + action: CheckoutAction | BillingAddressAction | OrderAction | CheckoutHydrateAction, ): BillingAddress | undefined { switch (action.type) { case BillingAddressActionType.UpdateBillingAddressSucceeded: @@ -37,6 +42,9 @@ function dataReducer( case CheckoutActionType.LoadCheckoutSucceeded: return replace(data, action.payload && action.payload.billingAddress); + case CheckoutHydrateActionType.HydrateInitialState: + return replace(data, action.payload?.checkout.billingAddress); + default: return data; } diff --git a/packages/core/src/cart/cart-reducer.ts b/packages/core/src/cart/cart-reducer.ts index e86545ebab..eabcc04e4e 100644 --- a/packages/core/src/cart/cart-reducer.ts +++ b/packages/core/src/cart/cart-reducer.ts @@ -1,7 +1,12 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; import { BillingAddressAction, BillingAddressActionType } from '../billing'; -import { CheckoutAction, CheckoutActionType } from '../checkout'; +import { + CheckoutAction, + CheckoutActionType, + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout'; import { clearErrorReducer } from '../common/error'; import { objectMerge, objectSet } from '../common/utility'; import { @@ -32,7 +37,8 @@ function dataReducer( | CheckoutAction | ConsignmentAction | CouponAction - | GiftCertificateAction, + | GiftCertificateAction + | CheckoutHydrateAction, ): Cart | undefined { switch (action.type) { case BillingAddressActionType.UpdateBillingAddressSucceeded: @@ -48,6 +54,9 @@ function dataReducer( case GiftCertificateActionType.RemoveGiftCertificateSucceeded: return objectMerge(data, action.payload && action.payload.cart); + case CheckoutHydrateActionType.HydrateInitialState: + return objectMerge(data, action.payload?.checkout.cart); + default: return data; } diff --git a/packages/core/src/checkout/checkout-hydrate-actions.ts b/packages/core/src/checkout/checkout-hydrate-actions.ts new file mode 100644 index 0000000000..3812b26d52 --- /dev/null +++ b/packages/core/src/checkout/checkout-hydrate-actions.ts @@ -0,0 +1,11 @@ +import { Action } from '@bigcommerce/data-store'; + +import CheckoutInitialState from './checkout-initial-state'; + +export enum CheckoutHydrateActionType { + HydrateInitialState = 'HYDRATE_INITIAL_STATE', +} + +export interface CheckoutHydrateAction extends Action { + type: CheckoutHydrateActionType.HydrateInitialState; +} diff --git a/packages/core/src/checkout/checkout-initial-state.ts b/packages/core/src/checkout/checkout-initial-state.ts new file mode 100644 index 0000000000..2526f3d5e7 --- /dev/null +++ b/packages/core/src/checkout/checkout-initial-state.ts @@ -0,0 +1,12 @@ +import { Config } from '../config'; +import { Extension } from '../extension'; +import { FormFields } from '../form'; + +import Checkout from './checkout'; + +export default interface CheckoutInitialState { + config: Config; + formFields: FormFields; + checkout: Checkout; + extensions: Extension[]; +} diff --git a/packages/core/src/checkout/checkout-reducer.ts b/packages/core/src/checkout/checkout-reducer.ts index 9df23f4bd1..b75617da53 100644 --- a/packages/core/src/checkout/checkout-reducer.ts +++ b/packages/core/src/checkout/checkout-reducer.ts @@ -22,6 +22,7 @@ import CheckoutState, { CheckoutStatusesState, DEFAULT_STATE, } from './checkout-state'; +import { CheckoutHydrateAction, CheckoutHydrateActionType } from './checkout-hydrate-actions'; export default function checkoutReducer( state: CheckoutState = DEFAULT_STATE, @@ -46,7 +47,8 @@ function dataReducer( | GiftCertificateAction | OrderAction | SpamProtectionAction - | StoreCreditAction, + | StoreCreditAction + | CheckoutHydrateAction, ): CheckoutDataState | undefined { switch (action.type) { case CheckoutActionType.LoadCheckoutSucceeded: @@ -78,6 +80,19 @@ function dataReducer( case OrderActionType.SubmitOrderSucceeded: return objectSet(data, 'orderId', action.payload && action.payload.order.orderId); + case CheckoutHydrateActionType.HydrateInitialState: + return objectMerge( + data, + omit(action.payload?.checkout, [ + 'billingAddress', + 'cart', + 'consignments', + 'customer', + 'coupons', + 'giftCertificates', + ]), + ) as CheckoutDataState; + default: return data; } diff --git a/packages/core/src/checkout/create-checkout-service.spec.ts b/packages/core/src/checkout/create-checkout-service.spec.ts index 51622825ff..c5d9c7eb5f 100644 --- a/packages/core/src/checkout/create-checkout-service.spec.ts +++ b/packages/core/src/checkout/create-checkout-service.spec.ts @@ -1,8 +1,13 @@ import { createRequestSender } from '@bigcommerce/request-sender'; import { getDefaultLogger, Logger } from '../common/log'; +import { getConfig } from '../config/configs.mock'; +import { getExtensions } from '../extension/extension.mock'; +import { getFormFields } from '../form/form.mock'; +import CheckoutInitialState from './checkout-initial-state'; import CheckoutService from './checkout-service'; +import { getCheckout } from './checkouts.mock'; import createCheckoutService from './create-checkout-service'; jest.mock('@bigcommerce/request-sender'); @@ -43,4 +48,23 @@ describe('createCheckoutService()', () => { expect(logger.warn).toHaveBeenCalled(); }); + + it('creates instance with initial data', () => { + const initialState: CheckoutInitialState = { + config: getConfig(), + formFields: getFormFields(), + checkout: getCheckout(), + extensions: getExtensions(), + }; + const checkoutService = createCheckoutService({ initialState }); + const state = checkoutService.getState(); + + expect(checkoutService).toBeInstanceOf(CheckoutService); + expect(state.data.getCheckout()).toEqual(initialState.checkout); + expect(state.data.getConfig()).toEqual(initialState.config.storeConfig); + expect(state.data.getCustomerAccountFields()).toEqual( + initialState.formFields.customerAccount, + ); + expect(state.data.getExtensions()).toEqual(initialState.extensions); + }); }); diff --git a/packages/core/src/checkout/create-checkout-service.ts b/packages/core/src/checkout/create-checkout-service.ts index faed1b49bb..9cced94aa4 100644 --- a/packages/core/src/checkout/create-checkout-service.ts +++ b/packages/core/src/checkout/create-checkout-service.ts @@ -62,6 +62,7 @@ import { StoreCreditActionCreator, StoreCreditRequestSender } from '../store-cre import { SubscriptionsActionCreator, SubscriptionsRequestSender } from '../subscription'; import CheckoutActionCreator from './checkout-action-creator'; +import CheckoutInitialState from './checkout-initial-state'; import CheckoutRequestSender from './checkout-request-sender'; import CheckoutService from './checkout-service'; import CheckoutValidator from './checkout-validator'; @@ -114,7 +115,7 @@ export default function createCheckoutService(options?: CheckoutServiceOptions): errorLogger = new DefaultErrorLogger(), } = options || {}; const requestSender = createRequestSender({ host: options && options.host }); - const store = createCheckoutStore({ config }, { shouldWarnMutation }); + const store = createCheckoutStore({ config }, options?.initialState, { shouldWarnMutation }); const paymentClient = createPaymentClient(store); const orderRequestSender = new OrderRequestSender(requestSender); const checkoutRequestSender = new CheckoutRequestSender(requestSender); @@ -232,4 +233,5 @@ export interface CheckoutServiceOptions { shouldWarnMutation?: boolean; externalSource?: string; errorLogger?: ErrorLogger; + initialState?: CheckoutInitialState; } diff --git a/packages/core/src/checkout/create-checkout-store.ts b/packages/core/src/checkout/create-checkout-store.ts index 101a60b9bf..ee9c1684ec 100644 --- a/packages/core/src/checkout/create-checkout-store.ts +++ b/packages/core/src/checkout/create-checkout-store.ts @@ -2,6 +2,8 @@ import { createDataStore } from '@bigcommerce/data-store'; import { createRequestErrorFactory } from '../common/error'; +import { CheckoutHydrateActionType } from './checkout-hydrate-actions'; +import CheckoutInitialState from './checkout-initial-state'; import CheckoutStore, { CheckoutStoreOptions } from './checkout-store'; import CheckoutStoreState from './checkout-store-state'; import createActionTransformer from './create-action-transformer'; @@ -9,16 +11,26 @@ import createCheckoutStoreReducer from './create-checkout-store-reducer'; import { createInternalCheckoutSelectorsFactory } from './create-internal-checkout-selectors'; export default function createCheckoutStore( - initialState: Partial = {}, + initialStoreState: Partial = {}, + initialServerState?: CheckoutInitialState, options?: CheckoutStoreOptions, ): CheckoutStore { const actionTransformer = createActionTransformer(createRequestErrorFactory()); const createInternalCheckoutSelectors = createInternalCheckoutSelectorsFactory(); const stateTransformer = (state: CheckoutStoreState) => createInternalCheckoutSelectors(state); + const reducer = createCheckoutStoreReducer(); + const hydrateAction = { + type: CheckoutHydrateActionType.HydrateInitialState, + payload: initialServerState, + }; - return createDataStore(createCheckoutStoreReducer(), initialState, { - actionTransformer, - stateTransformer, - ...options, - }); + return createDataStore( + reducer, + reducer(initialStoreState as CheckoutStoreState, hydrateAction), + { + actionTransformer, + stateTransformer, + ...options, + }, + ); } diff --git a/packages/core/src/checkout/index.ts b/packages/core/src/checkout/index.ts index b75992c2c1..16e852696d 100644 --- a/packages/core/src/checkout/index.ts +++ b/packages/core/src/checkout/index.ts @@ -1,4 +1,5 @@ export * from './checkout-actions'; +export * from './checkout-hydrate-actions'; export { default as Checkout, CheckoutPayment } from './checkout'; export { default as CHECKOUT_DEFAULT_INCLUDES } from './checkout-default-includes'; diff --git a/packages/core/src/config/config-reducer.ts b/packages/core/src/config/config-reducer.ts index 4e74098393..01ff5f4b0d 100644 --- a/packages/core/src/config/config-reducer.ts +++ b/packages/core/src/config/config-reducer.ts @@ -1,5 +1,6 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; +import { CheckoutHydrateAction, CheckoutHydrateActionType } from '../checkout'; import { clearErrorReducer } from '../common/error'; import { objectMerge, objectSet } from '../common/utility'; @@ -20,11 +21,17 @@ export default function configReducer( return reducer(state, action); } -function dataReducer(data: Config | undefined, action: LoadConfigAction): Config | undefined { +function dataReducer( + data: Config | undefined, + action: LoadConfigAction | CheckoutHydrateAction, +): Config | undefined { switch (action.type) { case ConfigActionType.LoadConfigSucceeded: return objectMerge(data, action.payload); + case CheckoutHydrateActionType.HydrateInitialState: + return objectMerge(data, action.payload?.config); + default: return data; } diff --git a/packages/core/src/config/config-selector.ts b/packages/core/src/config/config-selector.ts index b1f94f4244..44f8ce851d 100644 --- a/packages/core/src/config/config-selector.ts +++ b/packages/core/src/config/config-selector.ts @@ -59,13 +59,14 @@ export function createConfigSelectorFactory(): ConfigSelectorFactory { const getStoreConfig = createSelector( (state: ConfigState) => state.data, (_: ConfigState, { formState }: ConfigSelectorDependencies) => formState && formState.data, - (data, formFields) => () => - data && formFields - ? { - ...data.storeConfig, - formFields, - } - : undefined, + (data, formFields = { customerAccount: [], shippingAddress: [], billingAddress: [] }) => + () => + data + ? { + ...data.storeConfig, + formFields, + } + : undefined, ); const getStoreConfigOrThrow = createSelector(getStoreConfig, (getStoreConfig) => () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index af9a54b4e3..64604fc71c 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -85,6 +85,7 @@ export interface StoreCurrency { code: string; decimalPlaces: string; decimalSeparator: string; + isTransactional: boolean; symbolLocation: string; symbol: string; thousandsSeparator: string; @@ -117,6 +118,7 @@ export interface CheckoutSettings { isSpamProtectionEnabled: boolean; isTrustedShippingAddressEnabled: boolean; orderTermsAndConditions: string; + orderTermsAndConditionsLocation: string; orderTermsAndConditionsLink: string; orderTermsAndConditionsType: string; privacyPolicyUrl: string; diff --git a/packages/core/src/config/configs.mock.ts b/packages/core/src/config/configs.mock.ts index cf14262f65..bb2f962163 100644 --- a/packages/core/src/config/configs.mock.ts +++ b/packages/core/src/config/configs.mock.ts @@ -43,6 +43,7 @@ export function getConfig(): Config { isSpamProtectionEnabled: true, isTrustedShippingAddressEnabled: false, orderTermsAndConditions: '', + orderTermsAndConditionsLocation: '', orderTermsAndConditionsLink: '', orderTermsAndConditionsType: '', privacyPolicyUrl: '', @@ -58,6 +59,7 @@ export function getConfig(): Config { code: 'USD', decimalPlaces: '2', decimalSeparator: '.', + isTransactional: true, symbolLocation: 'left', symbol: '$', thousandsSeparator: ',', diff --git a/packages/core/src/coupon/coupon-reducer.ts b/packages/core/src/coupon/coupon-reducer.ts index fb9962d101..21a4964202 100644 --- a/packages/core/src/coupon/coupon-reducer.ts +++ b/packages/core/src/coupon/coupon-reducer.ts @@ -1,6 +1,11 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; -import { CheckoutAction, CheckoutActionType } from '../checkout'; +import { + CheckoutAction, + CheckoutActionType, + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout'; import { clearErrorReducer } from '../common/error'; import { arrayReplace, objectSet } from '../common/utility'; import { OrderAction, OrderActionType } from '../order'; @@ -25,7 +30,7 @@ export default function couponReducer( function dataReducer( data: Coupon[] | undefined, - action: CouponAction | CheckoutAction | OrderAction | ConsignmentAction, + action: CouponAction | CheckoutAction | OrderAction | ConsignmentAction | CheckoutHydrateAction, ): Coupon[] | undefined { switch (action.type) { case CheckoutActionType.LoadCheckoutSucceeded: @@ -35,6 +40,9 @@ function dataReducer( case OrderActionType.LoadOrderSucceeded: return arrayReplace(data, action.payload && action.payload.coupons); + case CheckoutHydrateActionType.HydrateInitialState: + return arrayReplace(data, action.payload?.checkout.coupons); + default: return data; } diff --git a/packages/core/src/coupon/gift-certificate-reducer.ts b/packages/core/src/coupon/gift-certificate-reducer.ts index d42639fc00..f387f30dee 100644 --- a/packages/core/src/coupon/gift-certificate-reducer.ts +++ b/packages/core/src/coupon/gift-certificate-reducer.ts @@ -1,6 +1,11 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; -import { CheckoutAction, CheckoutActionType } from '../checkout'; +import { + CheckoutAction, + CheckoutActionType, + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout'; import { clearErrorReducer } from '../common/error'; import { arrayReplace, objectSet } from '../common/utility'; import { ConsignmentAction, ConsignmentActionType } from '../shipping'; @@ -29,7 +34,12 @@ export default function giftCertificateReducer( function dataReducer( data: GiftCertificate[] | undefined, - action: CheckoutAction | GiftCertificateAction | ConsignmentAction | CouponAction, + action: + | CheckoutAction + | GiftCertificateAction + | ConsignmentAction + | CouponAction + | CheckoutHydrateAction, ): GiftCertificate[] | undefined { switch (action.type) { case CheckoutActionType.LoadCheckoutSucceeded: @@ -43,6 +53,9 @@ function dataReducer( case GiftCertificateActionType.RemoveGiftCertificateSucceeded: return arrayReplace(data, action.payload && action.payload.giftCertificates); + case CheckoutHydrateActionType.HydrateInitialState: + return arrayReplace(data, action.payload?.checkout.giftCertificates); + default: return data; } diff --git a/packages/core/src/customer/customer-reducer.ts b/packages/core/src/customer/customer-reducer.ts index cc513d61b8..bea4d5c2ce 100644 --- a/packages/core/src/customer/customer-reducer.ts +++ b/packages/core/src/customer/customer-reducer.ts @@ -1,7 +1,12 @@ import { combineReducers, composeReducers } from '@bigcommerce/data-store'; import { BillingAddressActionType, ContinueAsGuestAction } from '../billing'; -import { CheckoutAction, CheckoutActionType } from '../checkout'; +import { + CheckoutAction, + CheckoutActionType, + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout'; import { clearErrorReducer } from '../common/error'; import { objectMerge, objectSet } from '../common/utility'; @@ -21,7 +26,8 @@ type ReducerActionType = | CheckoutAction | ContinueAsGuestAction | CustomerAction - | StripeLinkAuthenticatedAction; + | StripeLinkAuthenticatedAction + | CheckoutHydrateAction; export default function customerReducer( state: CustomerState = DEFAULT_STATE, @@ -45,6 +51,9 @@ function dataReducer(data: Customer | undefined, action: ReducerActionType): Cus case CustomerActionType.CreateCustomerAddressSucceeded: return objectMerge(data, action.payload); + case CheckoutHydrateActionType.HydrateInitialState: + return objectMerge(data, action.payload?.checkout.customer); + default: return data; } diff --git a/packages/core/src/extension/extension-reducer.ts b/packages/core/src/extension/extension-reducer.ts index b862f364ff..ea303b974b 100644 --- a/packages/core/src/extension/extension-reducer.ts +++ b/packages/core/src/extension/extension-reducer.ts @@ -1,5 +1,9 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; +import { + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout/checkout-hydrate-actions'; import { clearErrorReducer } from '../common/error'; import { arrayReplace, objectSet } from '../common/utility'; @@ -27,12 +31,16 @@ export function extensionReducer( function dataReducer( data: Extension[] | undefined, - action: ExtensionAction, + action: ExtensionAction | CheckoutHydrateAction, ): Extension[] | undefined { if (action.type === ExtensionActionType.LoadExtensionsSucceeded) { return arrayReplace(data, action.payload); } + if (action.type === CheckoutHydrateActionType.HydrateInitialState) { + return arrayReplace(data, action.payload?.extensions); + } + return data; } diff --git a/packages/core/src/form/form-fields-reducer.ts b/packages/core/src/form/form-fields-reducer.ts index 864b0f9891..36c1736965 100644 --- a/packages/core/src/form/form-fields-reducer.ts +++ b/packages/core/src/form/form-fields-reducer.ts @@ -1,5 +1,9 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; +import { + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout/checkout-hydrate-actions'; import { clearErrorReducer } from '../common/error'; import { objectMerge, objectSet } from '../common/utility'; @@ -26,12 +30,15 @@ export default function formFieldsReducer( function dataReducer( data: FormFields | undefined, - action: LoadFormFieldsAction, + action: LoadFormFieldsAction | CheckoutHydrateAction, ): FormFields | undefined { switch (action.type) { case FormFieldsActionType.LoadFormFieldsSucceeded: return objectMerge(data, action.payload); + case CheckoutHydrateActionType.HydrateInitialState: + return objectMerge(data, action.payload?.formFields); + default: return data; } diff --git a/packages/core/src/shipping/consignment-reducer.ts b/packages/core/src/shipping/consignment-reducer.ts index ea20dfd1a9..3c151f7d3f 100644 --- a/packages/core/src/shipping/consignment-reducer.ts +++ b/packages/core/src/shipping/consignment-reducer.ts @@ -1,6 +1,11 @@ import { Action, combineReducers, composeReducers } from '@bigcommerce/data-store'; -import { CheckoutAction, CheckoutActionType } from '../checkout'; +import { + CheckoutAction, + CheckoutActionType, + CheckoutHydrateAction, + CheckoutHydrateActionType, +} from '../checkout'; import { clearErrorReducer } from '../common/error'; import { arrayReplace, objectMerge, objectSet } from '../common/utility'; import { CouponAction, CouponActionType } from '../coupon'; @@ -29,7 +34,12 @@ export default function consignmentReducer( function dataReducer( data: Consignment[] | undefined, - action: ConsignmentAction | CheckoutAction | CouponAction | CustomerAction, + action: + | ConsignmentAction + | CheckoutAction + | CouponAction + | CustomerAction + | CheckoutHydrateAction, ): Consignment[] | undefined { switch (action.type) { case CheckoutActionType.LoadCheckoutSucceeded: @@ -45,6 +55,9 @@ function dataReducer( case CustomerActionType.SignOutCustomerSucceeded: return arrayReplace(data, []); + case CheckoutHydrateActionType.HydrateInitialState: + return arrayReplace(data, action.payload?.checkout.consignments); + default: return data; } diff --git a/packages/payment-integration-api/src/config/config.ts b/packages/payment-integration-api/src/config/config.ts index 4b72480ba5..64604fc71c 100644 --- a/packages/payment-integration-api/src/config/config.ts +++ b/packages/payment-integration-api/src/config/config.ts @@ -11,6 +11,7 @@ export interface StoreConfig { checkoutSettings: CheckoutSettings; currency: StoreCurrency; displayDateFormat: string; + displaySettings: DisplaySettings; inputDateFormat: string; /** @@ -75,6 +76,7 @@ export interface StoreLinks { createAccountLink: string; forgotPasswordLink: string; loginLink: string; + logoutLink: string; siteLink: string; orderConfirmationLink: string; } @@ -83,6 +85,7 @@ export interface StoreCurrency { code: string; decimalPlaces: string; decimalSeparator: string; + isTransactional: boolean; symbolLocation: string; symbol: string; thousandsSeparator: string; @@ -115,6 +118,7 @@ export interface CheckoutSettings { isSpamProtectionEnabled: boolean; isTrustedShippingAddressEnabled: boolean; orderTermsAndConditions: string; + orderTermsAndConditionsLocation: string; orderTermsAndConditionsLink: string; orderTermsAndConditionsType: string; privacyPolicyUrl: string; @@ -147,3 +151,7 @@ export interface ContextConfig { token?: string; }; } + +export interface DisplaySettings { + hidePriceFromGuests: boolean; +} diff --git a/packages/payment-integration-api/src/mocks/config.mock.ts b/packages/payment-integration-api/src/mocks/config.mock.ts index c2cdb954d9..bb9050f282 100644 --- a/packages/payment-integration-api/src/mocks/config.mock.ts +++ b/packages/payment-integration-api/src/mocks/config.mock.ts @@ -40,6 +40,7 @@ export default function getConfig(): Config { isSpamProtectionEnabled: true, isTrustedShippingAddressEnabled: false, orderTermsAndConditions: '', + orderTermsAndConditionsLocation: '', orderTermsAndConditionsLink: '', orderTermsAndConditionsType: '', privacyPolicyUrl: '', @@ -55,11 +56,15 @@ export default function getConfig(): Config { code: 'USD', decimalPlaces: '2', decimalSeparator: '.', + isTransactional: true, symbolLocation: 'left', symbol: '$', thousandsSeparator: ',', }, displayDateFormat: 'dd/MM/yyyy', + displaySettings: { + hidePriceFromGuests: false, + }, inputDateFormat: 'dd/MM/yyyy', formFields: { customerAccount: [], @@ -74,6 +79,7 @@ export default function getConfig(): Config { forgotPasswordLink: 'https://store-k1drp8k8.bcapp.dev/login.php?action=reset_password', loginLink: 'https://store-k1drp8k8.bcapp.dev/login.php', + logoutLink: 'https://store-k1drp8k8.bcapp.dev/login.php?action=logout', siteLink: 'https://store-k1drp8k8.bcapp.dev/', orderConfirmationLink: 'https://store-k1drp8k8.bcapp.dev/checkout/order-confirmation', diff --git a/packages/payment-integrations-test-utils/src/test-utils/config.mock.ts b/packages/payment-integrations-test-utils/src/test-utils/config.mock.ts index a4becc1c25..babd30a570 100644 --- a/packages/payment-integrations-test-utils/src/test-utils/config.mock.ts +++ b/packages/payment-integrations-test-utils/src/test-utils/config.mock.ts @@ -40,6 +40,7 @@ export default function getConfig(): Config { isSpamProtectionEnabled: true, isTrustedShippingAddressEnabled: false, orderTermsAndConditions: '', + orderTermsAndConditionsLocation: '', orderTermsAndConditionsLink: '', orderTermsAndConditionsType: '', privacyPolicyUrl: '', @@ -55,11 +56,15 @@ export default function getConfig(): Config { code: 'USD', decimalPlaces: '2', decimalSeparator: '.', + isTransactional: true, symbolLocation: 'left', symbol: '$', thousandsSeparator: ',', }, displayDateFormat: 'dd/MM/yyyy', + displaySettings: { + hidePriceFromGuests: false, + }, inputDateFormat: 'dd/MM/yyyy', formFields: { customerAccount: [], @@ -74,6 +79,7 @@ export default function getConfig(): Config { forgotPasswordLink: 'https://store-k1drp8k8.bcapp.dev/login.php?action=reset_password', loginLink: 'https://store-k1drp8k8.bcapp.dev/login.php', + logoutLink: 'https://store-k1drp8k8.bcapp.dev/login.php?action=logout', siteLink: 'https://store-k1drp8k8.bcapp.dev/', orderConfirmationLink: 'https://store-k1drp8k8.bcapp.dev/checkout/order-confirmation', From ce90ae349f6056aee792a568dcd571d7927f1899 Mon Sep 17 00:00:00 2001 From: David Chin Date: Tue, 30 Sep 2025 15:45:48 +1000 Subject: [PATCH 2/2] feat(core): CHECKOUT-9513 Pass initial state through separate method to reduce work in single microtask --- .../src/billing/billing-address-reducer.ts | 2 +- packages/core/src/cart/cart-reducer.ts | 2 +- .../src/checkout/checkout-action-creator.ts | 11 +++++++- .../src/checkout/checkout-initial-state.ts | 8 +++--- .../core/src/checkout/checkout-reducer.ts | 2 +- .../src/checkout/checkout-service.spec.ts | 21 ++++++++++++++++ .../core/src/checkout/checkout-service.ts | 25 +++++++++++++++++++ .../checkout/create-checkout-service.spec.ts | 24 ------------------ .../src/checkout/create-checkout-service.ts | 4 +-- .../src/checkout/create-checkout-store.ts | 21 ++++------------ packages/core/src/coupon/coupon-reducer.ts | 2 +- .../src/coupon/gift-certificate-reducer.ts | 2 +- .../core/src/customer/customer-reducer.ts | 2 +- .../core/src/shipping/consignment-reducer.ts | 2 +- 14 files changed, 73 insertions(+), 55 deletions(-) diff --git a/packages/core/src/billing/billing-address-reducer.ts b/packages/core/src/billing/billing-address-reducer.ts index d30743c461..8d04ce30aa 100644 --- a/packages/core/src/billing/billing-address-reducer.ts +++ b/packages/core/src/billing/billing-address-reducer.ts @@ -43,7 +43,7 @@ function dataReducer( return replace(data, action.payload && action.payload.billingAddress); case CheckoutHydrateActionType.HydrateInitialState: - return replace(data, action.payload?.checkout.billingAddress); + return replace(data, action.payload?.checkout?.billingAddress); default: return data; diff --git a/packages/core/src/cart/cart-reducer.ts b/packages/core/src/cart/cart-reducer.ts index eabcc04e4e..92cd005656 100644 --- a/packages/core/src/cart/cart-reducer.ts +++ b/packages/core/src/cart/cart-reducer.ts @@ -55,7 +55,7 @@ function dataReducer( return objectMerge(data, action.payload && action.payload.cart); case CheckoutHydrateActionType.HydrateInitialState: - return objectMerge(data, action.payload?.checkout.cart); + return objectMerge(data, action.payload?.checkout?.cart); default: return data; diff --git a/packages/core/src/checkout/checkout-action-creator.ts b/packages/core/src/checkout/checkout-action-creator.ts index 5a644daadb..ee1ed6123b 100644 --- a/packages/core/src/checkout/checkout-action-creator.ts +++ b/packages/core/src/checkout/checkout-action-creator.ts @@ -1,4 +1,4 @@ -import { createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store'; +import { Action, createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store'; import { concat, defer, merge, Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -10,6 +10,8 @@ import { FormFieldsActionCreator } from '../form'; import Checkout, { CheckoutRequestBody } from './checkout'; import { CheckoutActionType, LoadCheckoutAction, UpdateCheckoutAction } from './checkout-actions'; +import { CheckoutHydrateActionType } from './checkout-hydrate-actions'; +import CheckoutInitialState from './checkout-initial-state'; import CheckoutRequestSender from './checkout-request-sender'; import InternalCheckoutSelectors from './internal-checkout-selectors'; @@ -143,6 +145,13 @@ export default class CheckoutActionCreator { }; } + hydrateInitialState(state: CheckoutInitialState): Action { + return { + type: CheckoutHydrateActionType.HydrateInitialState, + payload: state, + }; + } + private _transformCustomerAddresses(body: Checkout): Checkout { return { ...body, diff --git a/packages/core/src/checkout/checkout-initial-state.ts b/packages/core/src/checkout/checkout-initial-state.ts index 2526f3d5e7..b7b9cace81 100644 --- a/packages/core/src/checkout/checkout-initial-state.ts +++ b/packages/core/src/checkout/checkout-initial-state.ts @@ -5,8 +5,8 @@ import { FormFields } from '../form'; import Checkout from './checkout'; export default interface CheckoutInitialState { - config: Config; - formFields: FormFields; - checkout: Checkout; - extensions: Extension[]; + config?: Config; + formFields?: FormFields; + checkout?: Checkout; + extensions?: Extension[]; } diff --git a/packages/core/src/checkout/checkout-reducer.ts b/packages/core/src/checkout/checkout-reducer.ts index b75617da53..7cb20dee34 100644 --- a/packages/core/src/checkout/checkout-reducer.ts +++ b/packages/core/src/checkout/checkout-reducer.ts @@ -16,13 +16,13 @@ import { SpamProtectionAction, SpamProtectionActionType } from '../spam-protecti import { StoreCreditAction, StoreCreditActionType } from '../store-credit'; import { CheckoutAction, CheckoutActionType } from './checkout-actions'; +import { CheckoutHydrateAction, CheckoutHydrateActionType } from './checkout-hydrate-actions'; import CheckoutState, { CheckoutDataState, CheckoutErrorsState, CheckoutStatusesState, DEFAULT_STATE, } from './checkout-state'; -import { CheckoutHydrateAction, CheckoutHydrateActionType } from './checkout-hydrate-actions'; export default function checkoutReducer( state: CheckoutState = DEFAULT_STATE, diff --git a/packages/core/src/checkout/checkout-service.spec.ts b/packages/core/src/checkout/checkout-service.spec.ts index 45f9643093..2c07dae433 100644 --- a/packages/core/src/checkout/checkout-service.spec.ts +++ b/packages/core/src/checkout/checkout-service.spec.ts @@ -108,6 +108,7 @@ import { StoreCreditActionCreator, StoreCreditRequestSender } from '../store-cre import { SubscriptionsActionCreator, SubscriptionsRequestSender } from '../subscription'; import CheckoutActionCreator from './checkout-action-creator'; +import CheckoutInitialState from './checkout-initial-state'; import CheckoutRequestSender from './checkout-request-sender'; import CheckoutSelectors from './checkout-selectors'; import CheckoutService from './checkout-service'; @@ -1636,4 +1637,24 @@ describe('CheckoutService', () => { ); }); }); + + describe('#hydrateInitialState', () => { + it('creates instance with initial data', async () => { + const initialState: CheckoutInitialState = { + config: getConfig(), + formFields: getFormFields(), + checkout: getCheckout(), + extensions: getExtensions(), + }; + + const state = await checkoutService.hydrateInitialState(initialState); + + expect(state.data.getCheckout()).toEqual(initialState.checkout); + expect(state.data.getConfig()).toEqual(initialState.config?.storeConfig); + expect(state.data.getCustomerAccountFields()).toEqual( + initialState.formFields?.customerAccount, + ); + expect(state.data.getExtensions()).toEqual(initialState.extensions); + }); + }); }); diff --git a/packages/core/src/checkout/checkout-service.ts b/packages/core/src/checkout/checkout-service.ts index 42ebba04ec..30d3f4b02a 100644 --- a/packages/core/src/checkout/checkout-service.ts +++ b/packages/core/src/checkout/checkout-service.ts @@ -60,6 +60,7 @@ import { Subscriptions, SubscriptionsActionCreator } from '../subscription'; import { CheckoutRequestBody } from './checkout'; import CheckoutActionCreator from './checkout-action-creator'; +import CheckoutInitialState from './checkout-initial-state'; import CheckoutParams from './checkout-params'; import CheckoutSelectors from './checkout-selectors'; import CheckoutStore from './checkout-store'; @@ -180,6 +181,30 @@ export default class CheckoutService { return this._storeProjection.subscribe(subscriber, ...filters); } + /** + * Hydrates the checkout service with an initial state. + * + * The initial state can contain various checkout data such as cart items, + * customer information, and other relevant state. + * + * ```js + * const initialState = { + * // ... initial checkout state data + * }; + * + * const state = await service.hydrateInitialState(initialState); + * + * console.log(state.data.getCheckout()); + * ``` + * + * @alpha + * @param state - The initial state data to hydrate the checkout service with. + * @returns A promise that resolves to the current state after hydration. + */ + hydrateInitialState(state: CheckoutInitialState): Promise { + return this._dispatch(this._checkoutActionCreator.hydrateInitialState(state)); + } + /** * Loads the current checkout. * diff --git a/packages/core/src/checkout/create-checkout-service.spec.ts b/packages/core/src/checkout/create-checkout-service.spec.ts index c5d9c7eb5f..51622825ff 100644 --- a/packages/core/src/checkout/create-checkout-service.spec.ts +++ b/packages/core/src/checkout/create-checkout-service.spec.ts @@ -1,13 +1,8 @@ import { createRequestSender } from '@bigcommerce/request-sender'; import { getDefaultLogger, Logger } from '../common/log'; -import { getConfig } from '../config/configs.mock'; -import { getExtensions } from '../extension/extension.mock'; -import { getFormFields } from '../form/form.mock'; -import CheckoutInitialState from './checkout-initial-state'; import CheckoutService from './checkout-service'; -import { getCheckout } from './checkouts.mock'; import createCheckoutService from './create-checkout-service'; jest.mock('@bigcommerce/request-sender'); @@ -48,23 +43,4 @@ describe('createCheckoutService()', () => { expect(logger.warn).toHaveBeenCalled(); }); - - it('creates instance with initial data', () => { - const initialState: CheckoutInitialState = { - config: getConfig(), - formFields: getFormFields(), - checkout: getCheckout(), - extensions: getExtensions(), - }; - const checkoutService = createCheckoutService({ initialState }); - const state = checkoutService.getState(); - - expect(checkoutService).toBeInstanceOf(CheckoutService); - expect(state.data.getCheckout()).toEqual(initialState.checkout); - expect(state.data.getConfig()).toEqual(initialState.config.storeConfig); - expect(state.data.getCustomerAccountFields()).toEqual( - initialState.formFields.customerAccount, - ); - expect(state.data.getExtensions()).toEqual(initialState.extensions); - }); }); diff --git a/packages/core/src/checkout/create-checkout-service.ts b/packages/core/src/checkout/create-checkout-service.ts index 9cced94aa4..faed1b49bb 100644 --- a/packages/core/src/checkout/create-checkout-service.ts +++ b/packages/core/src/checkout/create-checkout-service.ts @@ -62,7 +62,6 @@ import { StoreCreditActionCreator, StoreCreditRequestSender } from '../store-cre import { SubscriptionsActionCreator, SubscriptionsRequestSender } from '../subscription'; import CheckoutActionCreator from './checkout-action-creator'; -import CheckoutInitialState from './checkout-initial-state'; import CheckoutRequestSender from './checkout-request-sender'; import CheckoutService from './checkout-service'; import CheckoutValidator from './checkout-validator'; @@ -115,7 +114,7 @@ export default function createCheckoutService(options?: CheckoutServiceOptions): errorLogger = new DefaultErrorLogger(), } = options || {}; const requestSender = createRequestSender({ host: options && options.host }); - const store = createCheckoutStore({ config }, options?.initialState, { shouldWarnMutation }); + const store = createCheckoutStore({ config }, { shouldWarnMutation }); const paymentClient = createPaymentClient(store); const orderRequestSender = new OrderRequestSender(requestSender); const checkoutRequestSender = new CheckoutRequestSender(requestSender); @@ -233,5 +232,4 @@ export interface CheckoutServiceOptions { shouldWarnMutation?: boolean; externalSource?: string; errorLogger?: ErrorLogger; - initialState?: CheckoutInitialState; } diff --git a/packages/core/src/checkout/create-checkout-store.ts b/packages/core/src/checkout/create-checkout-store.ts index ee9c1684ec..64f86f46d4 100644 --- a/packages/core/src/checkout/create-checkout-store.ts +++ b/packages/core/src/checkout/create-checkout-store.ts @@ -2,8 +2,6 @@ import { createDataStore } from '@bigcommerce/data-store'; import { createRequestErrorFactory } from '../common/error'; -import { CheckoutHydrateActionType } from './checkout-hydrate-actions'; -import CheckoutInitialState from './checkout-initial-state'; import CheckoutStore, { CheckoutStoreOptions } from './checkout-store'; import CheckoutStoreState from './checkout-store-state'; import createActionTransformer from './create-action-transformer'; @@ -12,25 +10,16 @@ import { createInternalCheckoutSelectorsFactory } from './create-internal-checko export default function createCheckoutStore( initialStoreState: Partial = {}, - initialServerState?: CheckoutInitialState, options?: CheckoutStoreOptions, ): CheckoutStore { const actionTransformer = createActionTransformer(createRequestErrorFactory()); const createInternalCheckoutSelectors = createInternalCheckoutSelectorsFactory(); const stateTransformer = (state: CheckoutStoreState) => createInternalCheckoutSelectors(state); const reducer = createCheckoutStoreReducer(); - const hydrateAction = { - type: CheckoutHydrateActionType.HydrateInitialState, - payload: initialServerState, - }; - return createDataStore( - reducer, - reducer(initialStoreState as CheckoutStoreState, hydrateAction), - { - actionTransformer, - stateTransformer, - ...options, - }, - ); + return createDataStore(reducer, initialStoreState, { + actionTransformer, + stateTransformer, + ...options, + }); } diff --git a/packages/core/src/coupon/coupon-reducer.ts b/packages/core/src/coupon/coupon-reducer.ts index 21a4964202..6c0db455a8 100644 --- a/packages/core/src/coupon/coupon-reducer.ts +++ b/packages/core/src/coupon/coupon-reducer.ts @@ -41,7 +41,7 @@ function dataReducer( return arrayReplace(data, action.payload && action.payload.coupons); case CheckoutHydrateActionType.HydrateInitialState: - return arrayReplace(data, action.payload?.checkout.coupons); + return arrayReplace(data, action.payload?.checkout?.coupons); default: return data; diff --git a/packages/core/src/coupon/gift-certificate-reducer.ts b/packages/core/src/coupon/gift-certificate-reducer.ts index f387f30dee..8e2c0947c6 100644 --- a/packages/core/src/coupon/gift-certificate-reducer.ts +++ b/packages/core/src/coupon/gift-certificate-reducer.ts @@ -54,7 +54,7 @@ function dataReducer( return arrayReplace(data, action.payload && action.payload.giftCertificates); case CheckoutHydrateActionType.HydrateInitialState: - return arrayReplace(data, action.payload?.checkout.giftCertificates); + return arrayReplace(data, action.payload?.checkout?.giftCertificates); default: return data; diff --git a/packages/core/src/customer/customer-reducer.ts b/packages/core/src/customer/customer-reducer.ts index bea4d5c2ce..6b0e4e539a 100644 --- a/packages/core/src/customer/customer-reducer.ts +++ b/packages/core/src/customer/customer-reducer.ts @@ -52,7 +52,7 @@ function dataReducer(data: Customer | undefined, action: ReducerActionType): Cus return objectMerge(data, action.payload); case CheckoutHydrateActionType.HydrateInitialState: - return objectMerge(data, action.payload?.checkout.customer); + return objectMerge(data, action.payload?.checkout?.customer); default: return data; diff --git a/packages/core/src/shipping/consignment-reducer.ts b/packages/core/src/shipping/consignment-reducer.ts index 3c151f7d3f..94adff1bfb 100644 --- a/packages/core/src/shipping/consignment-reducer.ts +++ b/packages/core/src/shipping/consignment-reducer.ts @@ -56,7 +56,7 @@ function dataReducer( return arrayReplace(data, []); case CheckoutHydrateActionType.HydrateInitialState: - return arrayReplace(data, action.payload?.checkout.consignments); + return arrayReplace(data, action.payload?.checkout?.consignments); default: return data;