From a0f91456c3a185c12ba34b40fed2dd9bfeca229d Mon Sep 17 00:00:00 2001 From: Alessandro Casazza Date: Tue, 10 Oct 2023 19:01:03 +0200 Subject: [PATCH 1/8] feat: Add `invertAddresses` prop to `` --- .../specs/addresses/invert-addresses.spec.tsx | 117 +++++++++ .../addresses/AddressesContainer.tsx | 16 +- .../addresses/SaveAddressesButton.tsx | 22 +- .../src/context/BillingAddressFormContext.ts | 1 + .../src/reducers/AddressReducer.ts | 106 ++++++--- .../react-components/src/typings/index.ts | 2 + .../src/utils/addressesManager.ts | 224 ++++++++++++++++-- 7 files changed, 420 insertions(+), 68 deletions(-) create mode 100644 packages/react-components/specs/addresses/invert-addresses.spec.tsx diff --git a/packages/react-components/specs/addresses/invert-addresses.spec.tsx b/packages/react-components/specs/addresses/invert-addresses.spec.tsx new file mode 100644 index 00000000..6bc101b5 --- /dev/null +++ b/packages/react-components/specs/addresses/invert-addresses.spec.tsx @@ -0,0 +1,117 @@ +import CommerceLayer from '#components/auth/CommerceLayer' +import getToken from '../utils/getToken' +import { render, screen } from '@testing-library/react' +import { type OrderContext } from '../utils/context' +import AddressesContainer from '#components/addresses/AddressesContainer' +import AddressInput from '#components/addresses/AddressInput' +import ShippingAddressForm from '#components/addresses/ShippingAddressForm' +import AddressCountrySelector from '#components/addresses/AddressCountrySelector' +import AddressStateSelector from '#components/addresses/AddressStateSelector' +import OrderContainer from '#components/orders/OrderContainer' +import OrderNumber from '#components/orders/OrderNumber' + +describe('Billing info input', () => { + let token: string | undefined + let domain: string | undefined + beforeAll(async () => { + const { accessToken, endpoint } = await getToken() + if (accessToken !== undefined) { + token = accessToken + domain = endpoint + } + }) + beforeEach(async (ctx) => { + if (token != null && domain != null) { + ctx.accessToken = token + ctx.endpoint = domain + ctx.orderId = 'wxzYheVAAY' + } + }) + it('Use shipping address as billing address', async (ctx) => { + render( + + + + + + + + + + + + + + + + + + + + ) + await screen.findByText('2454728') + const firstName = screen.getByTestId('first-name') + const lastName = screen.getByTestId('last-name') + const line1 = screen.getByTestId('line-1') + const line2 = screen.getByTestId('line-2') + const city = screen.getByTestId('city') + const countryCode = screen.getByTestId('country-code') + const stateCode = screen.getByTestId('state-code') + const zipCode = screen.getByTestId('zip-code') + const phone = screen.getByTestId('phone') + const billingInfo = screen.getByTestId('billing-info') + expect(firstName).toBeDefined() + expect(lastName).toBeDefined() + expect(line1).toBeDefined() + expect(line2).toBeDefined() + expect(city).toBeDefined() + expect(countryCode).toBeDefined() + expect(stateCode).toBeDefined() + expect(zipCode).toBeDefined() + expect(phone).toBeDefined() + expect(billingInfo).toBeDefined() + }) + // it('Hide billing info if requires_billing_info is false and required is undefined', async (ctx) => { + // render( + // + // + // + // + // + // + // + // + // + // + // ) + // const billingInfo = screen.queryByTestId('billing-info') + // expect(billingInfo).toBeNull() + // }) +}) diff --git a/packages/react-components/src/components/addresses/AddressesContainer.tsx b/packages/react-components/src/components/addresses/AddressesContainer.tsx index 1af81470..db691cf1 100644 --- a/packages/react-components/src/components/addresses/AddressesContainer.tsx +++ b/packages/react-components/src/components/addresses/AddressesContainer.tsx @@ -25,6 +25,10 @@ interface Props { * If true, the address will be considered a business address. */ isBusiness?: boolean + /** + * If true, the shipping address will be considered as primary address. Default is false. + */ + invertAddresses?: boolean } /** @@ -50,7 +54,12 @@ interface Props { * */ export function AddressesContainer(props: Props): JSX.Element { - const { children, shipToDifferentAddress = false, isBusiness } = props + const { + children, + shipToDifferentAddress = false, + isBusiness, + invertAddresses = false + } = props const [state, dispatch] = useReducer(addressReducer, addressInitialState) const { order, orderId, updateOrder } = useContext(OrderContext) const config = useContext(CommerceLayerContext) @@ -59,7 +68,8 @@ export function AddressesContainer(props: Props): JSX.Element { type: 'setShipToDifferentAddress', payload: { shipToDifferentAddress, - isBusiness + isBusiness, + invertAddresses } }) return () => { @@ -68,7 +78,7 @@ export function AddressesContainer(props: Props): JSX.Element { payload: {} }) } - }, [shipToDifferentAddress, isBusiness]) + }, [shipToDifferentAddress, isBusiness, invertAddresses]) const contextValue = { ...state, setAddressErrors: (errors: BaseError[], resource: AddressResource) => { diff --git a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx index f11d793f..11e188c7 100644 --- a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx +++ b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx @@ -3,9 +3,8 @@ import Parent from '#components/utils/Parent' import { type ChildrenFunction } from '#typings/index' import AddressContext from '#context/AddressContext' import { - shippingAddressController, countryLockController, - billingAddressController + addressesController } from '#utils/addressesManager' import OrderContext from '#context/OrderContext' import CustomerContext from '#context/CustomerContext' @@ -46,8 +45,10 @@ export function SaveAddressesButton(props: Props): JSX.Element { shipping_address: shippingAddress, saveAddresses, billingAddressId, - shippingAddressId + shippingAddressId, + invertAddresses } = useContext(AddressContext) + console.log('invertAddresses', invertAddresses) const { order } = useContext(OrderContext) const { customerEmail: email, @@ -69,18 +70,13 @@ export function SaveAddressesButton(props: Props): JSX.Element { ) customerEmail = Object.keys(isValidEmail).length > 0 } - const billingDisable = billingAddressController({ + const { billingDisable, shippingDisable } = addressesController({ + invertAddresses, billing_address: billingAddress, - errors, - billingAddressId, - requiresBillingInfo: order?.requires_billing_info - }) - const shippingDisable = shippingAddressController({ - billingDisable, - errors, - shipToDifferentAddress, shipping_address: shippingAddress, - shippingAddressId + shippingAddressId, + billingAddressId, + errors }) const countryLockDisable = countryLockController({ countryCodeLock: order?.shipping_country_code_lock, diff --git a/packages/react-components/src/context/BillingAddressFormContext.ts b/packages/react-components/src/context/BillingAddressFormContext.ts index a87cb121..4cf33921 100644 --- a/packages/react-components/src/context/BillingAddressFormContext.ts +++ b/packages/react-components/src/context/BillingAddressFormContext.ts @@ -6,6 +6,7 @@ export type AddressValuesKeys = | `billing_address_${keyof Address}` | `shipping_address_${keyof Address}` | `billing_address_${`metadata_${string}`}` + | `shipping_address_${`metadata_${string}`}` export interface DefaultContextAddress { // eslint-disable-next-line @typescript-eslint/no-invalid-void-type diff --git a/packages/react-components/src/reducers/AddressReducer.ts b/packages/react-components/src/reducers/AddressReducer.ts index 2cae9e19..239e53f4 100644 --- a/packages/react-components/src/reducers/AddressReducer.ts +++ b/packages/react-components/src/reducers/AddressReducer.ts @@ -2,17 +2,18 @@ import baseReducer from '#utils/baseReducer' import { type Dispatch } from 'react' import { type BaseError } from '#typings/errors' import { type CommerceLayerConfig } from '#context/CommerceLayerContext' -import type { - Address, - AddressCreate, - Order, - OrderUpdate +import { + type OrderUpdate, + type Address, + type AddressCreate, + type Order } from '@commercelayer/sdk' import getSdk from '#utils/getSdk' import { type updateOrder } from './OrderReducer' import camelCase from 'lodash/camelCase' import { type TCustomerAddress } from './CustomerReducer' import { type TResourceError } from '#components/errors/Errors' +import { invertedAddressesHandler } from '#utils/addressesManager' export type AddressActionType = | 'setErrors' @@ -67,6 +68,7 @@ export interface AddressActionPayload { billingAddressId: string shippingAddressId: string isBusiness: boolean + invertAddresses: boolean } export type AddressState = Partial @@ -170,6 +172,7 @@ export async function saveAddresses({ }> { const { shipToDifferentAddress, + invertAddresses, billing_address: billingAddress, shipping_address: shippingAddress, billingAddressId, @@ -178,6 +181,17 @@ export async function saveAddresses({ try { const sdk = getSdk(config) if (order) { + let orderAttributes: OrderUpdate | null = null + if (invertAddresses) { + orderAttributes = await invertedAddressesHandler({ + billingAddress, + billingAddressId, + customerEmail, + order, + shipToDifferentAddress, + shippingAddress, + shippingAddressId, + sdk const doNotShipItems = order?.line_items?.every( // @ts-expect-error no type for do_not_ship on SDK (lineItem) => lineItem?.item?.do_not_ship === true @@ -205,30 +219,24 @@ export async function saveAddresses({ } return false }) - if (hasMetadata?.length > 0) { - hasMetadata.forEach((key) => { - const metadataKey = key.replace('metadata_', '') - billingAddress.metadata = { - ...(billingAddress.metadata || {}), - [metadataKey]: billingAddress[key] - } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete billingAddress[key] - }) + } else { + const currentBillingAddressRef = order?.billing_address?.reference + orderAttributes = { + id: order?.id, + _billing_address_clone_id: billingAddressId, + _shipping_address_clone_id: billingAddressId, + customer_email: customerEmail } - const address = await sdk.addresses.create(billingAddress) - orderAttributes.billing_address = sdk.addresses.relationship(address.id) - } - if (shipToDifferentAddress) { - delete orderAttributes._shipping_address_same_as_billing - if (shippingAddressId) - orderAttributes._shipping_address_clone_id = shippingAddressId - if ( - shippingAddress != null && - Object.keys(shippingAddress).length > 0 - ) { + if (currentBillingAddressRef === billingAddressId) { + orderAttributes._billing_address_clone_id = order?.billing_address?.id + orderAttributes._shipping_address_clone_id = + order?.shipping_address?.id + } + if (billingAddress != null && Object.keys(billingAddress).length > 0) { + delete orderAttributes._billing_address_clone_id delete orderAttributes._shipping_address_clone_id - const hasMetadata = Object.keys(shippingAddress).filter((key) => { + orderAttributes._shipping_address_same_as_billing = true + const hasMetadata = Object.keys(billingAddress).filter((key) => { if (key.startsWith('metadata_')) { return true } @@ -237,19 +245,51 @@ export async function saveAddresses({ if (hasMetadata?.length > 0) { hasMetadata.forEach((key) => { const metadataKey = key.replace('metadata_', '') - shippingAddress.metadata = { - ...(shippingAddress.metadata || {}), - [metadataKey]: shippingAddress[key] + billingAddress.metadata = { + ...(billingAddress.metadata || {}), + [metadataKey]: billingAddress[key] } // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete shippingAddress[key] + delete billingAddress[key] }) } - const address = await sdk.addresses.create(shippingAddress) - orderAttributes.shipping_address = sdk.addresses.relationship( + const address = await sdk.addresses.create(billingAddress) + orderAttributes.billing_address = sdk.addresses.relationship( address.id ) } + if (shipToDifferentAddress) { + delete orderAttributes._shipping_address_same_as_billing + if (shippingAddressId) + orderAttributes._shipping_address_clone_id = shippingAddressId + if ( + shippingAddress != null && + Object.keys(shippingAddress).length > 0 + ) { + delete orderAttributes._shipping_address_clone_id + const hasMetadata = Object.keys(shippingAddress).filter((key) => { + if (key.startsWith('metadata_')) { + return true + } + return false + }) + if (hasMetadata?.length > 0) { + hasMetadata.forEach((key) => { + const metadataKey = key.replace('metadata_', '') + shippingAddress.metadata = { + ...(shippingAddress.metadata || {}), + [metadataKey]: shippingAddress[key] + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete shippingAddress[key] + }) + } + const address = await sdk.addresses.create(shippingAddress) + orderAttributes.shipping_address = sdk.addresses.relationship( + address.id + ) + } + } } if (orderAttributes != null && updateOrder) { const orderUpdated = await updateOrder({ diff --git a/packages/react-components/src/typings/index.ts b/packages/react-components/src/typings/index.ts index d9a76256..01ec32ff 100644 --- a/packages/react-components/src/typings/index.ts +++ b/packages/react-components/src/typings/index.ts @@ -114,6 +114,8 @@ export type AddressInputName = | 'shipping_address_state_code' | 'shipping_address_zip_code' | 'shipping_address_save_to_customer_book' + | 'shipping_address_billing_info' + | `shipping_address_${`metadata_${string}`}` export type AddressCountrySelectName = | 'billing_address_country_code' diff --git a/packages/react-components/src/utils/addressesManager.ts b/packages/react-components/src/utils/addressesManager.ts index e9c5dcd5..4f6b21dd 100644 --- a/packages/react-components/src/utils/addressesManager.ts +++ b/packages/react-components/src/utils/addressesManager.ts @@ -1,35 +1,53 @@ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ /* eslint-disable @typescript-eslint/naming-convention */ import isEmpty from 'lodash/isEmpty' import { fieldsExist } from '#utils/validateFormFields' import { type BaseError } from '#typings/errors' import { addressFields } from '#reducers/AddressReducer' import { + type OrderUpdate, + type Address, + type AddressCreate, + type Order, + type CommerceLayerClient type LineItem, type Address, type AddressCreate } from '@commercelayer/sdk' import { type TCustomerAddress } from '#reducers/CustomerReducer' -type BillingAddressController = (params: { +interface BillingAddressControllerProps { billing_address?: AddressCreate billingAddressId?: string errors?: BaseError[] requiresBillingInfo?: boolean | null -}) => boolean + invertAddresses?: boolean + shippingDisable?: boolean + shipToDifferentAddress?: boolean +} -export const billingAddressController: BillingAddressController = ({ +export function billingAddressController({ billing_address, billingAddressId, errors, - requiresBillingInfo = false -}) => { - let billingDisable = !isEmpty(errors) || isEmpty(billing_address) + requiresBillingInfo = false, + invertAddresses = false, + shipToDifferentAddress, + shippingDisable +}: BillingAddressControllerProps): boolean { + let billingDisable = invertAddresses + ? !!(!shippingDisable && shipToDifferentAddress) + : !isEmpty(errors) || isEmpty(billing_address) if (isEmpty(errors) && !isEmpty(billing_address)) { - let billingInfo = [...addressFields] - if (requiresBillingInfo) billingInfo = [...billingInfo, 'billing_info'] - billingDisable = !!( - billing_address && fieldsExist(billing_address, billingInfo) - ) + if (invertAddresses) { + billingDisable = !!(billing_address && fieldsExist(billing_address)) + } else { + let billingInfo = [...addressFields] + if (requiresBillingInfo) billingInfo = [...billingInfo, 'billing_info'] + billingDisable = !!( + billing_address && fieldsExist(billing_address, billingInfo) + ) + } } if ( billingDisable && @@ -41,24 +59,39 @@ export const billingAddressController: BillingAddressController = ({ return billingDisable } -type ShippingAddressController = (params: { +interface ShippingAddressControllerProps { billingDisable?: boolean errors?: BaseError[] + requiresBillingInfo?: boolean | null shipToDifferentAddress?: boolean shipping_address?: AddressCreate shippingAddressId?: string -}) => boolean + invertAddresses?: boolean +} -export const shippingAddressController: ShippingAddressController = ({ +export function shippingAddressController({ billingDisable, errors, shipToDifferentAddress, shipping_address, - shippingAddressId -}) => { - let shippingDisable = !!(!billingDisable && shipToDifferentAddress) - if (shippingDisable && isEmpty(errors) && !isEmpty(shipping_address)) { - shippingDisable = !!(shipping_address && fieldsExist(shipping_address)) + shippingAddressId, + invertAddresses = false, + requiresBillingInfo = false +}: ShippingAddressControllerProps): boolean { + let shippingDisable = invertAddresses + ? !isEmpty(errors) || isEmpty(shipping_address) + : !!(!billingDisable && shipToDifferentAddress) + + if (isEmpty(errors) && !isEmpty(shipping_address)) { + if (invertAddresses) { + let billingInfo = [...addressFields] + if (requiresBillingInfo) billingInfo = [...billingInfo, 'billing_info'] + shippingDisable = !!( + shipping_address && fieldsExist(shipping_address, billingInfo) + ) + } else { + shippingDisable = !!(shipping_address && fieldsExist(shipping_address)) + } } if ( shippingDisable && @@ -137,3 +170,156 @@ export function countryLockController({ } return false } + +interface InvertedAddressesHandlerParams { + billingAddress?: AddressCreate + billingAddressId?: string + customerEmail?: string + order: Order + shipToDifferentAddress?: boolean + shippingAddress?: AddressCreate + shippingAddressId?: string + sdk: CommerceLayerClient +} + +export async function invertedAddressesHandler({ + order, + billingAddress, + billingAddressId, + customerEmail, + shipToDifferentAddress, + shippingAddress, + shippingAddressId, + sdk +}: InvertedAddressesHandlerParams): Promise { + const currentShippingAddressRef = order?.shipping_address?.reference + // const currentBillingAddressRef = order?.billing_address?.reference + const orderAttributes: OrderUpdate = { + id: order?.id, + _billing_address_clone_id: shippingAddressId, + _shipping_address_clone_id: shippingAddressId, + customer_email: customerEmail + } + if (currentShippingAddressRef === billingAddressId) { + orderAttributes._billing_address_clone_id = order?.billing_address?.id + orderAttributes._shipping_address_clone_id = order?.shipping_address?.id + } + if (shippingAddress != null && Object.keys(shippingAddress).length > 0) { + delete orderAttributes._billing_address_clone_id + delete orderAttributes._shipping_address_clone_id + orderAttributes._billing_address_same_as_shipping = true + const hasMetadata = Object.keys(shippingAddress).filter((key) => { + if (key.startsWith('metadata_')) { + return true + } + return false + }) + if (hasMetadata?.length > 0) { + hasMetadata.forEach((key) => { + const metadataKey = key.replace('metadata_', '') + shippingAddress.metadata = { + ...(shippingAddress.metadata || {}), + // @ts-expect-error type mismatch + [metadataKey]: shippingAddress[key] + } + // @ts-expect-error type mismatch + delete shippingAddress[key] + }) + } + const address = await sdk.addresses.create(shippingAddress) + orderAttributes.shipping_address = sdk.addresses.relationship(address.id) + } + if (shipToDifferentAddress) { + delete orderAttributes._billing_address_same_as_shipping + if (billingAddressId) + orderAttributes._billing_address_clone_id = billingAddressId + if (billingAddress != null && Object.keys(billingAddress).length > 0) { + delete orderAttributes._billing_address_clone_id + const hasMetadata = Object.keys(billingAddress).filter((key) => { + if (key.startsWith('metadata_')) { + return true + } + return false + }) + if (hasMetadata?.length > 0) { + hasMetadata.forEach((key) => { + const metadataKey = key.replace('metadata_', '') + billingAddress.metadata = { + ...(billingAddress.metadata || {}), + // @ts-expect-error type mismatch + [metadataKey]: billingAddress[key] + } + // @ts-expect-error type mismatch + delete billingAddress[key] + }) + } + const address = await sdk.addresses.create(billingAddress) + orderAttributes.billing_address = sdk.addresses.relationship(address.id) + } + } + return orderAttributes +} + +interface AddressControllerProps { + billing_address?: AddressCreate + billingAddressId?: string + shipToDifferentAddress?: boolean + shipping_address?: AddressCreate + shippingAddressId?: string + errors?: BaseError[] + requiresBillingInfo?: boolean | null + invertAddresses?: boolean +} + +export function addressesController({ + billing_address, + billingAddressId, + shipToDifferentAddress, + shipping_address, + shippingAddressId, + errors, + requiresBillingInfo, + invertAddresses +}: AddressControllerProps): { + billingDisable: boolean + shippingDisable: boolean +} { + if (invertAddresses) { + const shippingDisable = shippingAddressController({ + errors, + shipToDifferentAddress, + shipping_address, + shippingAddressId, + invertAddresses + }) + const billingDisable = billingAddressController({ + shippingDisable, + billing_address, + billingAddressId, + errors, + requiresBillingInfo, + invertAddresses + }) + return { + shippingDisable, + billingDisable + } + } + const billingDisable = billingAddressController({ + billing_address, + billingAddressId, + errors, + requiresBillingInfo + }) + const shippingDisable = shippingAddressController({ + billingDisable, + errors, + shipToDifferentAddress, + shipping_address, + shippingAddressId + }) + return { + billingDisable, + shippingDisable + } +} From 63aab9f647a6eacf7f915e18917c468975efb1af Mon Sep 17 00:00:00 2001 From: Alessandro Casazza Date: Tue, 10 Oct 2023 19:08:20 +0200 Subject: [PATCH 2/8] v4.8.0-beta.0 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 86cecf16..25174606 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": false, "npmClient": "pnpm", - "version": "4.7.11", + "version": "4.8.0-beta.0", "command": { "version": { "preid": "beta" From 3709fb32e43a23c29c8abed3cc578b040652df9c Mon Sep 17 00:00:00 2001 From: Matteo Alessani Date: Mon, 16 Oct 2023 19:15:13 +0200 Subject: [PATCH 3/8] fix: use `shipToDifferentAddress` and `requiresBillingInfo` on addresses check --- .../components/addresses/SaveAddressesButton.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx index 11e188c7..459484e0 100644 --- a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx +++ b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx @@ -70,10 +70,24 @@ export function SaveAddressesButton(props: Props): JSX.Element { ) customerEmail = Object.keys(isValidEmail).length > 0 } + + const shippingAddressCleaned: any = Object.keys(shippingAddress ?? {}).reduce( + (acc, key) => { + return { + ...acc, + // @ts-expect-error type mismatch + [key.replace(`shipping_address_`, '')]: shippingAddress[key].value + } + }, + {} + ) + const { billingDisable, shippingDisable } = addressesController({ invertAddresses, + requiresBillingInfo: order?.requires_billing_info, billing_address: billingAddress, - shipping_address: shippingAddress, + shipping_address: shippingAddressCleaned, + shipToDifferentAddress, shippingAddressId, billingAddressId, errors From 33bc92da03f6131addc06683d45d0cb87aad6a08 Mon Sep 17 00:00:00 2001 From: Matteo Alessani Date: Mon, 16 Oct 2023 19:19:28 +0200 Subject: [PATCH 4/8] v4.8.0-beta.2 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 25174606..07930e31 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": false, "npmClient": "pnpm", - "version": "4.8.0-beta.0", + "version": "4.8.0-beta.2", "command": { "version": { "preid": "beta" From f2d345c12217946966873106d1efe13594e89786 Mon Sep 17 00:00:00 2001 From: Alessandro Casazza Date: Mon, 20 Nov 2023 16:23:58 +0100 Subject: [PATCH 5/8] chore: Rebase from main --- .../src/reducers/AddressReducer.ts | 124 +++++++++--------- .../src/utils/addressesManager.ts | 4 +- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/packages/react-components/src/reducers/AddressReducer.ts b/packages/react-components/src/reducers/AddressReducer.ts index 239e53f4..15d7cc67 100644 --- a/packages/react-components/src/reducers/AddressReducer.ts +++ b/packages/react-components/src/reducers/AddressReducer.ts @@ -192,34 +192,12 @@ export async function saveAddresses({ shippingAddress, shippingAddressId, sdk - const doNotShipItems = order?.line_items?.every( - // @ts-expect-error no type for do_not_ship on SDK - (lineItem) => lineItem?.item?.do_not_ship === true - ) - const currentBillingAddressRef = order?.billing_address?.reference - const orderAttributes: OrderUpdate = { - id: order?.id, - _billing_address_clone_id: billingAddressId, - _shipping_address_clone_id: billingAddressId, - customer_email: customerEmail - } - if (currentBillingAddressRef === billingAddressId) { - orderAttributes._billing_address_clone_id = order?.billing_address?.id - orderAttributes._shipping_address_clone_id = order?.shipping_address?.id - } - if (billingAddress != null && Object.keys(billingAddress).length > 0) { - delete orderAttributes._billing_address_clone_id - delete orderAttributes._shipping_address_clone_id - if (!doNotShipItems) { - orderAttributes._shipping_address_same_as_billing = true - } - const hasMetadata = Object.keys(billingAddress).filter((key) => { - if (key.startsWith('metadata_')) { - return true - } - return false }) } else { + const doNotShipItems = order?.line_items?.every( + // @ts-expect-error no type for do_not_ship on SDK + (lineItem) => lineItem?.item?.do_not_ship === true + ) const currentBillingAddressRef = order?.billing_address?.reference orderAttributes = { id: order?.id, @@ -235,39 +213,31 @@ export async function saveAddresses({ if (billingAddress != null && Object.keys(billingAddress).length > 0) { delete orderAttributes._billing_address_clone_id delete orderAttributes._shipping_address_clone_id - orderAttributes._shipping_address_same_as_billing = true - const hasMetadata = Object.keys(billingAddress).filter((key) => { - if (key.startsWith('metadata_')) { - return true - } - return false - }) - if (hasMetadata?.length > 0) { - hasMetadata.forEach((key) => { - const metadataKey = key.replace('metadata_', '') - billingAddress.metadata = { - ...(billingAddress.metadata || {}), - [metadataKey]: billingAddress[key] - } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete billingAddress[key] - }) + if (!doNotShipItems) { + orderAttributes._shipping_address_same_as_billing = true + } + } else { + const currentBillingAddressRef = order?.billing_address?.reference + orderAttributes = { + id: order?.id, + _billing_address_clone_id: billingAddressId, + _shipping_address_clone_id: billingAddressId, + customer_email: customerEmail + } + if (currentBillingAddressRef === billingAddressId) { + orderAttributes._billing_address_clone_id = + order?.billing_address?.id + orderAttributes._shipping_address_clone_id = + order?.shipping_address?.id } - const address = await sdk.addresses.create(billingAddress) - orderAttributes.billing_address = sdk.addresses.relationship( - address.id - ) - } - if (shipToDifferentAddress) { - delete orderAttributes._shipping_address_same_as_billing - if (shippingAddressId) - orderAttributes._shipping_address_clone_id = shippingAddressId if ( - shippingAddress != null && - Object.keys(shippingAddress).length > 0 + billingAddress != null && + Object.keys(billingAddress).length > 0 ) { + delete orderAttributes._billing_address_clone_id delete orderAttributes._shipping_address_clone_id - const hasMetadata = Object.keys(shippingAddress).filter((key) => { + orderAttributes._shipping_address_same_as_billing = true + const hasMetadata = Object.keys(billingAddress).filter((key) => { if (key.startsWith('metadata_')) { return true } @@ -276,19 +246,51 @@ export async function saveAddresses({ if (hasMetadata?.length > 0) { hasMetadata.forEach((key) => { const metadataKey = key.replace('metadata_', '') - shippingAddress.metadata = { - ...(shippingAddress.metadata || {}), - [metadataKey]: shippingAddress[key] + billingAddress.metadata = { + ...(billingAddress.metadata || {}), + [metadataKey]: billingAddress[key] } // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete shippingAddress[key] + delete billingAddress[key] }) } - const address = await sdk.addresses.create(shippingAddress) - orderAttributes.shipping_address = sdk.addresses.relationship( + const address = await sdk.addresses.create(billingAddress) + orderAttributes.billing_address = sdk.addresses.relationship( address.id ) } + if (shipToDifferentAddress) { + delete orderAttributes._shipping_address_same_as_billing + if (shippingAddressId) + orderAttributes._shipping_address_clone_id = shippingAddressId + if ( + shippingAddress != null && + Object.keys(shippingAddress).length > 0 + ) { + delete orderAttributes._shipping_address_clone_id + const hasMetadata = Object.keys(shippingAddress).filter((key) => { + if (key.startsWith('metadata_')) { + return true + } + return false + }) + if (hasMetadata?.length > 0) { + hasMetadata.forEach((key) => { + const metadataKey = key.replace('metadata_', '') + shippingAddress.metadata = { + ...(shippingAddress.metadata || {}), + [metadataKey]: shippingAddress[key] + } + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete shippingAddress[key] + }) + } + const address = await sdk.addresses.create(shippingAddress) + orderAttributes.shipping_address = sdk.addresses.relationship( + address.id + ) + } + } } } if (orderAttributes != null && updateOrder) { diff --git a/packages/react-components/src/utils/addressesManager.ts b/packages/react-components/src/utils/addressesManager.ts index 4f6b21dd..be13906b 100644 --- a/packages/react-components/src/utils/addressesManager.ts +++ b/packages/react-components/src/utils/addressesManager.ts @@ -6,10 +6,8 @@ import { type BaseError } from '#typings/errors' import { addressFields } from '#reducers/AddressReducer' import { type OrderUpdate, - type Address, - type AddressCreate, type Order, - type CommerceLayerClient + type CommerceLayerClient, type LineItem, type Address, type AddressCreate From f4275d0a6924544928edc3f793e839df03a03e53 Mon Sep 17 00:00:00 2001 From: Alessandro Casazza Date: Mon, 20 Nov 2023 16:31:32 +0100 Subject: [PATCH 6/8] v4.8.0-beta.3 --- lerna.json | 2 +- packages/react-components/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lerna.json b/lerna.json index 07930e31..ceaf068c 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": false, "npmClient": "pnpm", - "version": "4.8.0-beta.2", + "version": "4.8.0-beta.3", "command": { "version": { "preid": "beta" diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 01869410..2dcc31f6 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@commercelayer/react-components", - "version": "4.7.11", + "version": "4.8.0-beta.3", "description": "The Official Commerce Layer React Components", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", From aa44f2e7225e95057de0f2b58dd972e7d930e803 Mon Sep 17 00:00:00 2001 From: Alessandro Casazza Date: Mon, 4 Dec 2023 18:54:08 +0100 Subject: [PATCH 7/8] feat: Add new components to show skus are inside the bundle. --- .../addresses/SaveAddressesButton.tsx | 1 - .../src/components/line_items/LineItem.tsx | 29 ++++-- .../components/line_items/LineItemAmount.tsx | 9 +- .../line_items/LineItemBundleSkuField.tsx | 95 +++++++++++++++++++ .../line_items/LineItemBundleSkus.tsx | 34 +++++++ .../components/line_items/LineItemCode.tsx | 4 +- .../components/line_items/LineItemImage.tsx | 13 ++- .../components/line_items/LineItemName.tsx | 9 +- .../line_items/LineItemQuantity.tsx | 15 +-- .../line_items/LineItemRemoveLink.tsx | 12 ++- .../line_items/LineItemsContainer.tsx | 15 +++ .../payment_source/KlarnaPayment.tsx | 2 - .../context/LineItemBundleChildrenContext.ts | 14 +++ .../LineItemBundleSkuChildrenContext.ts | 13 +++ .../src/reducers/OrderReducer.ts | 2 + 15 files changed, 234 insertions(+), 33 deletions(-) create mode 100644 packages/react-components/src/components/line_items/LineItemBundleSkuField.tsx create mode 100644 packages/react-components/src/components/line_items/LineItemBundleSkus.tsx create mode 100644 packages/react-components/src/context/LineItemBundleChildrenContext.ts create mode 100644 packages/react-components/src/context/LineItemBundleSkuChildrenContext.ts diff --git a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx index 459484e0..c62a0c44 100644 --- a/packages/react-components/src/components/addresses/SaveAddressesButton.tsx +++ b/packages/react-components/src/components/addresses/SaveAddressesButton.tsx @@ -48,7 +48,6 @@ export function SaveAddressesButton(props: Props): JSX.Element { shippingAddressId, invertAddresses } = useContext(AddressContext) - console.log('invertAddresses', invertAddresses) const { order } = useContext(OrderContext) const { customerEmail: email, diff --git a/packages/react-components/src/components/line_items/LineItem.tsx b/packages/react-components/src/components/line_items/LineItem.tsx index def6f571..e83c8b83 100644 --- a/packages/react-components/src/components/line_items/LineItem.tsx +++ b/packages/react-components/src/components/line_items/LineItem.tsx @@ -4,6 +4,9 @@ import LineItemChildrenContext, { type InitialLineItemChildrenContext } from '#context/LineItemChildrenContext' import ShipmentChildrenContext from '#context/ShipmentChildrenContext' +import LineItemBundleChildrenContext, { + type InitialLineItemBundleChildrenContext +} from '#context/LineItemBundleChildrenContext' export type TLineItem = | 'gift_cards' @@ -29,19 +32,29 @@ export function LineItem(props: Props): JSX.Element { : lineItems const components = items ?.filter((l) => l?.item_type === type) - .map((lineItem, k, check) => { - if ( - lineItem?.item_type === 'bundles' && - k > 0 && - check[k - 1]?.bundle_code === lineItem.bundle_code - ) - return null + .map((lineItem) => { + if (lineItem?.item_type === 'bundles') { + const skuListItems = lineItem?.bundle?.sku_list?.sku_list_items + const skuListItemsProps: InitialLineItemBundleChildrenContext = { + skuListItems, + lineItem + } + return ( + + {children} + + ) + } if ( lineItem?.item_type === 'gift_cards' && lineItem?.total_amount_cents && lineItem?.total_amount_cents <= 0 - ) + ) { return null + } const lineProps: InitialLineItemChildrenContext = { lineItem } diff --git a/packages/react-components/src/components/line_items/LineItemAmount.tsx b/packages/react-components/src/components/line_items/LineItemAmount.tsx index 6860b460..bc8e93c9 100644 --- a/packages/react-components/src/components/line_items/LineItemAmount.tsx +++ b/packages/react-components/src/components/line_items/LineItemAmount.tsx @@ -3,6 +3,7 @@ import getAmount from '#utils/getAmount' import LineItemChildrenContext from '#context/LineItemChildrenContext' import Parent from '#components/utils/Parent' import { type BaseAmountComponent, type BasePriceType } from '#typings/index' +import LineItemBundleChildrenContext from '#context/LineItemBundleChildrenContext' type Props = BaseAmountComponent & { type?: BasePriceType @@ -11,21 +12,23 @@ type Props = BaseAmountComponent & { export function LineItemAmount(props: Props): JSX.Element { const { format = 'formatted', type = 'total', ...p } = props const { lineItem } = useContext(LineItemChildrenContext) + const { lineItem: lineItemBundle } = useContext(LineItemBundleChildrenContext) const [price, setPrice] = useState('') + const item = lineItem ?? lineItemBundle useEffect(() => { - if (lineItem) { + if (item) { const p = getAmount({ base: 'amount', type, format, - obj: lineItem + obj: item }) setPrice(p) } return (): void => { setPrice('') } - }, [lineItem]) + }, [item]) const parentProps = { price, ...p diff --git a/packages/react-components/src/components/line_items/LineItemBundleSkuField.tsx b/packages/react-components/src/components/line_items/LineItemBundleSkuField.tsx new file mode 100644 index 00000000..a3abdfc5 --- /dev/null +++ b/packages/react-components/src/components/line_items/LineItemBundleSkuField.tsx @@ -0,0 +1,95 @@ +import { useContext } from 'react' +import Parent from '#components/utils/Parent' +import type { LineItem, Sku, SkuListItem } from '@commercelayer/sdk' +import { type ChildrenFunction } from '#typings/index' +import LineItemBundleSkuChildrenContext from '#context/LineItemBundleSkuChildrenContext' + +type SkuAttribute = Extract< + keyof Sku, + | 'code' + | 'image_url' + | 'description' + | 'name' + | 'pieces_per_pack' + | 'weight' + | 'unit_of_weight' +> +type SkuListItemAttribute = Extract +type Attribute = SkuListItemAttribute | SkuAttribute +type TagElementKey = Extract< + keyof JSX.IntrinsicElements, + 'p' | 'span' | 'div' | 'img' +> + +export interface TLineItemBundleSkuField extends Omit { + lineItem: LineItem + skuListItem: SkuListItem +} + +type ImageElement = Omit< + JSX.IntrinsicElements[Extract], + 'children' | 'ref' +> + +type OtherElements = Omit< + JSX.IntrinsicElements[Exclude], + 'children' | 'ref' +> + +type Props = { + children?: ChildrenFunction +} & ( + | ({ + attribute: 'image_url' + tagElement: 'img' + } & ImageElement) + | ({ + attribute: Exclude + tagElement?: Exclude + } & OtherElements) +) + +export function LineItemBundleSkuField({ + tagElement = 'span', + attribute, + children, + ...props +}: Props): JSX.Element { + const { skuListItem } = useContext(LineItemBundleSkuChildrenContext) + const item = skuListItem + let attr = null + if (attribute === 'quantity') { + attr = item?.quantity + } else { + attr = item?.sku?.[attribute] + } + const TagElement = tagElement + const parentProps = { + attribute: attr, + lineItem: item, + ...props + } + if (attribute === 'image_url' && children == null) { + return ( + + ) + } + + return children ? ( + {children} + ) : ( + + {attr} + + ) +} + +export default LineItemBundleSkuField diff --git a/packages/react-components/src/components/line_items/LineItemBundleSkus.tsx b/packages/react-components/src/components/line_items/LineItemBundleSkus.tsx new file mode 100644 index 00000000..2cc46b20 --- /dev/null +++ b/packages/react-components/src/components/line_items/LineItemBundleSkus.tsx @@ -0,0 +1,34 @@ +import { useContext } from 'react' +import LineItemBundleChildrenContext from '#context/LineItemBundleChildrenContext' +import LineItemBundleSkuChildrenContext from '#context/LineItemBundleSkuChildrenContext' + +interface Props { + children: JSX.Element | JSX.Element[] +} + +export function LineItemBundleSkus({ children }: Props): JSX.Element { + const { lineItem, skuListItems } = useContext(LineItemBundleChildrenContext) + const components = skuListItems?.map((skuListItem) => { + const quantity = + skuListItem?.quantity != null && lineItem?.quantity + ? skuListItem?.quantity * lineItem?.quantity + : 0 + const skuListProps = { + skuListItem: { + ...skuListItem, + quantity + } + } + return ( + + {children} + + ) + }) + return <>{components} +} + +export default LineItemBundleSkus diff --git a/packages/react-components/src/components/line_items/LineItemCode.tsx b/packages/react-components/src/components/line_items/LineItemCode.tsx index 270db674..f39b4a4b 100644 --- a/packages/react-components/src/components/line_items/LineItemCode.tsx +++ b/packages/react-components/src/components/line_items/LineItemCode.tsx @@ -3,6 +3,7 @@ import LineItemChildrenContext from '#context/LineItemChildrenContext' import Parent from '#components/utils/Parent' import { type LineItem } from '@commercelayer/sdk' import { type ChildrenFunction } from '#typings/index' +import LineItemBundleChildrenContext from '#context/LineItemBundleChildrenContext' export interface TLineItemCode extends Omit { lineItem: LineItem @@ -20,7 +21,8 @@ export function LineItemCode({ ...p }: Props): JSX.Element { const { lineItem } = useContext(LineItemChildrenContext) - const labelName = lineItem?.[type] + const { lineItem: lineItemBundle } = useContext(LineItemBundleChildrenContext) + const labelName = lineItem?.[type] ?? lineItemBundle?.[type] const parentProps = { lineItem, skuCode: labelName, diff --git a/packages/react-components/src/components/line_items/LineItemImage.tsx b/packages/react-components/src/components/line_items/LineItemImage.tsx index 57b096a7..45fcc61d 100644 --- a/packages/react-components/src/components/line_items/LineItemImage.tsx +++ b/packages/react-components/src/components/line_items/LineItemImage.tsx @@ -5,6 +5,7 @@ import type { ChildrenFunction } from '#typings' import { defaultGiftCardImgUrl, defaultImgUrl } from '#utils/placeholderImages' import Parent from '#components/utils/Parent' import { type TLineItem } from './LineItem' +import LineItemBundleChildrenContext from '#context/LineItemBundleChildrenContext' export interface TLineItemImage extends Omit { src: string @@ -22,8 +23,10 @@ type Props = { export function LineItemImage(props: Props): JSX.Element | null { const { placeholder, children, ...p } = props const { lineItem } = useContext(LineItemChildrenContext) - const itemType = lineItem?.item_type as TLineItem - let src = lineItem?.image_url + const { lineItem: lineItemBundle } = useContext(LineItemBundleChildrenContext) + const item = lineItem ?? lineItemBundle + const itemType = item?.item_type as TLineItem + let src = item?.image_url if (!src) { if (placeholder?.[itemType]) { src = placeholder?.[itemType] @@ -32,7 +35,7 @@ export function LineItemImage(props: Props): JSX.Element | null { } } const parenProps = { - lineItem, + lineItem: item, src, placeholder, ...p @@ -41,8 +44,8 @@ export function LineItemImage(props: Props): JSX.Element | null { {children} ) : !src ? null : ( {lineItem?.name diff --git a/packages/react-components/src/components/line_items/LineItemName.tsx b/packages/react-components/src/components/line_items/LineItemName.tsx index b42a947d..9eb729fb 100644 --- a/packages/react-components/src/components/line_items/LineItemName.tsx +++ b/packages/react-components/src/components/line_items/LineItemName.tsx @@ -3,6 +3,7 @@ import LineItemChildrenContext from '#context/LineItemChildrenContext' import Parent from '#components/utils/Parent' import type { LineItem } from '@commercelayer/sdk' import { type ChildrenFunction } from '#typings/index' +import LineItemBundleChildrenContext from '#context/LineItemBundleChildrenContext' export interface TLineItemName extends Omit { label: string @@ -15,16 +16,18 @@ interface Props extends Omit { export function LineItemName(props: Props): JSX.Element { const { lineItem } = useContext(LineItemChildrenContext) - const label = lineItem?.name + const { lineItem: listItemBundle } = useContext(LineItemBundleChildrenContext) + const item = lineItem ?? listItemBundle + const label = item?.name const parentProps = { label, - lineItem, + lineItem: item, ...props } return props.children ? ( {props.children} ) : ( -

+

{label}

) diff --git a/packages/react-components/src/components/line_items/LineItemQuantity.tsx b/packages/react-components/src/components/line_items/LineItemQuantity.tsx index 43811296..51458447 100644 --- a/packages/react-components/src/components/line_items/LineItemQuantity.tsx +++ b/packages/react-components/src/components/line_items/LineItemQuantity.tsx @@ -4,6 +4,7 @@ import LineItemContext from '#context/LineItemContext' import Parent from '#components/utils/Parent' import { type ChildrenFunction } from '#typings' import { type LineItem } from '@commercelayer/sdk' +import LineItemBundleChildrenContext from '#context/LineItemBundleChildrenContext' interface ChildrenProps extends Omit { quantity: number @@ -27,7 +28,9 @@ type Props = { export function LineItemQuantity(props: Props): JSX.Element { const { max = 50, readonly = false, hasExternalPrice, ...p } = props const { lineItem } = useContext(LineItemChildrenContext) + const { lineItem: lineItemBundle } = useContext(LineItemBundleChildrenContext) const { updateLineItem } = useContext(LineItemContext) + const item = lineItem ?? lineItemBundle const options: ReactNode[] = [] for (let i = 1; i <= max; i++) { options.push( @@ -38,15 +41,15 @@ export function LineItemQuantity(props: Props): JSX.Element { } const handleChange = (e: React.ChangeEvent): void => { const quantity = Number(e.target.value) - if (updateLineItem && lineItem) { - void updateLineItem(lineItem.id, quantity, hasExternalPrice) + if (updateLineItem && item) { + void updateLineItem(item.id, quantity, hasExternalPrice) } } - const quantity = lineItem?.quantity + const quantity = item?.quantity const parentProps = { handleChange, quantity, - lineItem, + lineItem: item, ...props } return props.children ? ( @@ -55,8 +58,8 @@ export function LineItemQuantity(props: Props): JSX.Element { {quantity} ) : (