diff --git a/lerna.json b/lerna.json index 86cecf16..b58f879a 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.4", "command": { "version": { "preid": "beta" diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 01869410..aa8b5e8d 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.4", "description": "The Official Commerce Layer React Components", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", 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..c62a0c44 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,7 +45,8 @@ export function SaveAddressesButton(props: Props): JSX.Element { shipping_address: shippingAddress, saveAddresses, billingAddressId, - shippingAddressId + shippingAddressId, + invertAddresses } = useContext(AddressContext) const { order } = useContext(OrderContext) const { @@ -69,18 +69,27 @@ export function SaveAddressesButton(props: Props): JSX.Element { ) customerEmail = Object.keys(isValidEmail).length > 0 } - const billingDisable = billingAddressController({ + + 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, - errors, - billingAddressId, - requiresBillingInfo: order?.requires_billing_info - }) - const shippingDisable = shippingAddressController({ - billingDisable, - errors, + shipping_address: shippingAddressCleaned, shipToDifferentAddress, - shipping_address: shippingAddress, - shippingAddressId + shippingAddressId, + billingAddressId, + errors }) const countryLockDisable = countryLockController({ countryCodeLock: order?.shipping_country_code_lock, 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} ) : (