@salesforce/retail-react-app 7.1.0-preview.0 → 8.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +8 -4
  2. package/app/components/_app/index.jsx +9 -7
  3. package/app/components/_app/index.test.js +2 -2
  4. package/app/components/_app-config/index.jsx +9 -3
  5. package/app/components/drawer-menu/drawer-menu.jsx +3 -1
  6. package/app/components/footer/index.jsx +3 -1
  7. package/app/components/header/index.jsx +3 -1
  8. package/app/components/header/index.test.js +2 -2
  9. package/app/components/island/README.md +1 -1
  10. package/app/components/island/index.jsx +3 -1
  11. package/app/components/island/index.test.js +94 -5
  12. package/app/components/item-variant/item-attributes.jsx +12 -3
  13. package/app/components/multiship/multiship-order-summary.jsx +137 -0
  14. package/app/components/multiship/multiship-order-summary.test.js +121 -0
  15. package/app/components/order-summary/index.jsx +2 -4
  16. package/app/components/pickup-or-delivery/index.jsx +80 -0
  17. package/app/components/pickup-or-delivery/index.test.jsx +182 -0
  18. package/app/components/product-item/index.jsx +26 -16
  19. package/app/components/product-item/index.test.js +29 -2
  20. package/app/components/product-item-list/index.jsx +10 -0
  21. package/app/components/product-item-list/index.test.jsx +14 -0
  22. package/app/components/product-view/index.jsx +9 -6
  23. package/app/components/product-view/index.test.js +25 -21
  24. package/app/components/quantity-picker/index.test.jsx +12 -12
  25. package/app/components/reset-password/index.test.js +1 -1
  26. package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
  27. package/app/components/shared/ui/index.jsx +1 -0
  28. package/app/components/store-display/index.jsx +28 -4
  29. package/app/components/store-display/index.test.js +71 -0
  30. package/app/components/store-locator/form.test.jsx +16 -4
  31. package/app/components/store-locator/list.jsx +9 -4
  32. package/app/components/toggle-card/index.jsx +14 -0
  33. package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
  34. package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
  35. package/app/constants.js +20 -6
  36. package/app/contexts/store-locator-provider.jsx +7 -1
  37. package/app/contexts/store-locator-provider.test.jsx +36 -1
  38. package/app/hooks/use-address-form.js +155 -0
  39. package/app/hooks/use-address-form.test.js +501 -0
  40. package/app/hooks/use-auth-modal.js +2 -6
  41. package/app/hooks/use-current-basket.js +71 -2
  42. package/app/hooks/use-current-basket.test.js +37 -1
  43. package/app/hooks/use-dnt-notification.js +4 -4
  44. package/app/hooks/use-dnt-notification.test.js +5 -5
  45. package/app/hooks/use-item-shipment-management.js +233 -0
  46. package/app/hooks/use-item-shipment-management.test.js +696 -0
  47. package/app/hooks/use-multiship.js +589 -0
  48. package/app/hooks/use-multiship.test.js +776 -0
  49. package/app/hooks/use-pickup-shipment.js +70 -106
  50. package/app/hooks/use-pickup-shipment.test.js +345 -209
  51. package/app/hooks/use-product-address-assignment.js +280 -0
  52. package/app/hooks/use-product-address-assignment.test.js +414 -0
  53. package/app/hooks/use-product-inventory.js +100 -0
  54. package/app/hooks/use-product-inventory.test.js +254 -0
  55. package/app/hooks/use-shipment-operations.js +168 -0
  56. package/app/hooks/use-shipment-operations.test.js +385 -0
  57. package/app/hooks/use-store-locator.js +24 -2
  58. package/app/hooks/use-store-locator.test.jsx +109 -1
  59. package/app/pages/account/index.test.js +1 -1
  60. package/app/pages/account/profile.test.js +0 -2
  61. package/app/pages/cart/index.jsx +397 -157
  62. package/app/pages/cart/index.test.js +353 -2
  63. package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
  64. package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
  65. package/app/pages/cart/partials/order-type-display.jsx +68 -0
  66. package/app/pages/cart/partials/order-type-display.test.js +241 -0
  67. package/app/pages/checkout/confirmation.jsx +79 -158
  68. package/app/pages/checkout/index.jsx +34 -9
  69. package/app/pages/checkout/index.test.js +245 -118
  70. package/app/pages/checkout/partials/contact-info.jsx +2 -6
  71. package/app/pages/checkout/partials/contact-info.test.js +93 -7
  72. package/app/pages/checkout/partials/payment.jsx +19 -5
  73. package/app/pages/checkout/partials/pickup-address.jsx +340 -70
  74. package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
  75. package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
  76. package/app/pages/checkout/partials/shipment-details.jsx +209 -0
  77. package/app/pages/checkout/partials/shipment-details.test.js +246 -0
  78. package/app/pages/checkout/partials/shipping-address.jsx +156 -68
  79. package/app/pages/checkout/partials/shipping-address.test.js +673 -0
  80. package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
  81. package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
  82. package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
  83. package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
  84. package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
  85. package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
  86. package/app/pages/checkout/util/checkout-context.js +25 -18
  87. package/app/pages/login/index.jsx +2 -6
  88. package/app/pages/product-detail/index.jsx +96 -81
  89. package/app/pages/product-detail/index.test.js +103 -19
  90. package/app/pages/product-list/index.jsx +3 -1
  91. package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
  92. package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
  93. package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
  94. package/app/ssr.js +1 -5
  95. package/app/static/translations/compiled/en-GB.json +316 -30
  96. package/app/static/translations/compiled/en-US.json +316 -30
  97. package/app/static/translations/compiled/en-XA.json +673 -75
  98. package/app/utils/address-utils.js +112 -0
  99. package/app/utils/address-utils.test.js +484 -0
  100. package/app/utils/product-utils.js +17 -5
  101. package/app/utils/product-utils.test.js +17 -8
  102. package/app/utils/sfdc-user-agent-utils.js +32 -0
  103. package/app/utils/sfdc-user-agent-utils.test.js +82 -0
  104. package/app/utils/shipment-utils.js +196 -0
  105. package/app/utils/shipment-utils.test.js +458 -0
  106. package/app/utils/test-utils.js +4 -4
  107. package/app/utils/utils.js +6 -1
  108. package/config/default.js +4 -1
  109. package/config/mocks/default.js +3 -1
  110. package/package.json +9 -9
  111. package/translations/en-GB.json +127 -10
  112. package/translations/en-US.json +127 -10
  113. package/app/pages/checkout/partials/shipping-options.jsx +0 -269
@@ -0,0 +1,241 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import React from 'react'
8
+ import {screen} from '@testing-library/react'
9
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
10
+ import OrderTypeDisplay from '@salesforce/retail-react-app/app/pages/cart/partials/order-type-display'
11
+
12
+ const mockStore = {
13
+ id: 'store-123',
14
+ name: 'Downtown Store',
15
+ address1: '123 Main Street',
16
+ city: 'San Francisco',
17
+ stateCode: 'CA',
18
+ postalCode: '94105',
19
+ phone: '(555) 123-4567',
20
+ c_customerServiceEmail: 'store@example.com',
21
+ storeHours:
22
+ 'Monday - Friday: 9:00 AM - 8:00 PM\nSaturday: 10:00 AM - 6:00 PM\nSunday: 12:00 PM - 5:00 PM',
23
+ distance: 2.5,
24
+ distanceUnit: 'miles'
25
+ }
26
+
27
+ describe('OrderTypeDisplay', () => {
28
+ beforeEach(() => {
29
+ jest.clearAllMocks()
30
+ })
31
+
32
+ describe('Pickup Order', () => {
33
+ test('renders pickup order display with store information', () => {
34
+ renderWithProviders(
35
+ <OrderTypeDisplay
36
+ isPickupOrder={true}
37
+ store={mockStore}
38
+ itemsInShipment={3}
39
+ totalItemsInCart={5}
40
+ />
41
+ )
42
+
43
+ // Check pickup message with item counts
44
+ expect(screen.getByText('Pick Up in Store - 3 out of 5 items')).toBeInTheDocument()
45
+
46
+ // Check store information is displayed
47
+ expect(screen.getByText('Downtown Store')).toBeInTheDocument()
48
+ expect(screen.getByText('123 Main Street')).toBeInTheDocument()
49
+ expect(screen.getByText('San Francisco, CA 94105')).toBeInTheDocument()
50
+ expect(screen.getByText('2.5 miles away')).toBeInTheDocument()
51
+ })
52
+
53
+ test('renders pickup order with single item', () => {
54
+ renderWithProviders(
55
+ <OrderTypeDisplay
56
+ isPickupOrder={true}
57
+ store={mockStore}
58
+ itemsInShipment={1}
59
+ totalItemsInCart={1}
60
+ />
61
+ )
62
+
63
+ expect(screen.getByText('Pick Up in Store - 1 out of 1 items')).toBeInTheDocument()
64
+ })
65
+
66
+ test('renders pickup order with zero items in shipment', () => {
67
+ renderWithProviders(
68
+ <OrderTypeDisplay
69
+ isPickupOrder={true}
70
+ store={mockStore}
71
+ itemsInShipment={0}
72
+ totalItemsInCart={3}
73
+ />
74
+ )
75
+
76
+ expect(screen.getByText('Pick Up in Store - 0 out of 3 items')).toBeInTheDocument()
77
+ })
78
+
79
+ test('renders pickup order without store (null store)', () => {
80
+ renderWithProviders(
81
+ <OrderTypeDisplay
82
+ isPickupOrder={true}
83
+ store={null}
84
+ itemsInShipment={2}
85
+ totalItemsInCart={4}
86
+ />
87
+ )
88
+
89
+ expect(screen.getByText('Pick Up in Store - 2 out of 4 items')).toBeInTheDocument()
90
+ // Store display should not crash with null store
91
+ expect(screen.queryByText('Downtown Store')).not.toBeInTheDocument()
92
+ })
93
+
94
+ test('renders pickup order without store (undefined store)', () => {
95
+ renderWithProviders(
96
+ <OrderTypeDisplay
97
+ isPickupOrder={true}
98
+ store={undefined}
99
+ itemsInShipment={2}
100
+ totalItemsInCart={4}
101
+ />
102
+ )
103
+
104
+ expect(screen.getByText('Pick Up in Store - 2 out of 4 items')).toBeInTheDocument()
105
+ // Store display should not crash with undefined store
106
+ expect(screen.queryByText('Downtown Store')).not.toBeInTheDocument()
107
+ })
108
+ })
109
+
110
+ describe('Delivery Order', () => {
111
+ test('renders delivery order display', () => {
112
+ renderWithProviders(
113
+ <OrderTypeDisplay
114
+ isPickupOrder={false}
115
+ store={mockStore}
116
+ itemsInShipment={4}
117
+ totalItemsInCart={7}
118
+ />
119
+ )
120
+
121
+ // Check delivery message with item counts
122
+ expect(screen.getByText('Delivery - 4 out of 7 items')).toBeInTheDocument()
123
+
124
+ // Store information should not be displayed for delivery
125
+ expect(screen.queryByText('Downtown Store')).not.toBeInTheDocument()
126
+ expect(screen.queryByText('123 Main Street')).not.toBeInTheDocument()
127
+ })
128
+
129
+ test('renders delivery order with single item', () => {
130
+ renderWithProviders(
131
+ <OrderTypeDisplay
132
+ isPickupOrder={false}
133
+ store={mockStore}
134
+ itemsInShipment={1}
135
+ totalItemsInCart={1}
136
+ />
137
+ )
138
+
139
+ expect(screen.getByText('Delivery - 1 out of 1 items')).toBeInTheDocument()
140
+ })
141
+
142
+ test('renders delivery order with zero items in shipment', () => {
143
+ renderWithProviders(
144
+ <OrderTypeDisplay
145
+ isPickupOrder={false}
146
+ store={mockStore}
147
+ itemsInShipment={0}
148
+ totalItemsInCart={2}
149
+ />
150
+ )
151
+
152
+ expect(screen.getByText('Delivery - 0 out of 2 items')).toBeInTheDocument()
153
+ })
154
+
155
+ test('renders delivery order without store prop', () => {
156
+ renderWithProviders(
157
+ <OrderTypeDisplay
158
+ isPickupOrder={false}
159
+ store={null}
160
+ itemsInShipment={3}
161
+ totalItemsInCart={5}
162
+ />
163
+ )
164
+
165
+ expect(screen.getByText('Delivery - 3 out of 5 items')).toBeInTheDocument()
166
+ // Store information should not be displayed for delivery regardless of store prop
167
+ expect(screen.queryByText('Downtown Store')).not.toBeInTheDocument()
168
+ })
169
+ })
170
+
171
+ describe('Component Structure', () => {
172
+ test('renders with proper card styling', () => {
173
+ const {container} = renderWithProviders(
174
+ <OrderTypeDisplay
175
+ isPickupOrder={false}
176
+ store={mockStore}
177
+ itemsInShipment={2}
178
+ totalItemsInCart={4}
179
+ />
180
+ )
181
+
182
+ // Check for Box component with layerStyle and padding
183
+ const cardBox = container.querySelector('[data-testid], div')
184
+ expect(cardBox).toBeInTheDocument()
185
+ })
186
+
187
+ test('renders pickup text message', () => {
188
+ renderWithProviders(
189
+ <OrderTypeDisplay
190
+ isPickupOrder={true}
191
+ store={mockStore}
192
+ itemsInShipment={2}
193
+ totalItemsInCart={4}
194
+ />
195
+ )
196
+
197
+ expect(screen.getByText('Pick Up in Store - 2 out of 4 items')).toBeInTheDocument()
198
+ })
199
+
200
+ test('renders delivery text message', () => {
201
+ renderWithProviders(
202
+ <OrderTypeDisplay
203
+ isPickupOrder={false}
204
+ store={mockStore}
205
+ itemsInShipment={2}
206
+ totalItemsInCart={4}
207
+ />
208
+ )
209
+
210
+ expect(screen.getByText('Delivery - 2 out of 4 items')).toBeInTheDocument()
211
+ })
212
+ })
213
+
214
+ describe('Large Numbers', () => {
215
+ test('handles large item counts correctly', () => {
216
+ renderWithProviders(
217
+ <OrderTypeDisplay
218
+ isPickupOrder={true}
219
+ store={mockStore}
220
+ itemsInShipment={999}
221
+ totalItemsInCart={1000}
222
+ />
223
+ )
224
+
225
+ expect(screen.getByText('Pick Up in Store - 999 out of 1000 items')).toBeInTheDocument()
226
+ })
227
+
228
+ test('handles edge case where items in shipment equals total items', () => {
229
+ renderWithProviders(
230
+ <OrderTypeDisplay
231
+ isPickupOrder={false}
232
+ store={mockStore}
233
+ itemsInShipment={5}
234
+ totalItemsInCart={5}
235
+ />
236
+ )
237
+
238
+ expect(screen.getByText('Delivery - 5 out of 5 items')).toBeInTheDocument()
239
+ })
240
+ })
241
+ })
@@ -22,19 +22,19 @@ import {
22
22
  } from '@salesforce/retail-react-app/app/components/shared/ui'
23
23
  import {useForm} from 'react-hook-form'
24
24
  import {useParams} from 'react-router-dom'
25
+ import {nanoid} from 'nanoid'
25
26
  import {
26
27
  useOrder,
27
28
  useProducts,
28
29
  useAuthHelper,
29
30
  AuthHelpers,
30
- useStores,
31
31
  useShopperCustomersMutation
32
32
  } from '@salesforce/commerce-sdk-react'
33
33
  import {getCreditCardIcon} from '@salesforce/retail-react-app/app/utils/cc-utils'
34
- import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
34
+
35
+ // Components
35
36
  import Link from '@salesforce/retail-react-app/app/components/link'
36
37
  import AddressDisplay from '@salesforce/retail-react-app/app/components/address-display'
37
- import StoreDisplay from '@salesforce/retail-react-app/app/components/store-display'
38
38
  import PostCheckoutRegistrationFields from '@salesforce/retail-react-app/app/components/forms/post-checkout-registration-fields'
39
39
  import PromoPopover from '@salesforce/retail-react-app/app/components/promo-popover'
40
40
  import ItemVariantProvider from '@salesforce/retail-react-app/app/components/item-variant'
@@ -42,13 +42,16 @@ import CartItemVariantImage from '@salesforce/retail-react-app/app/components/it
42
42
  import CartItemVariantName from '@salesforce/retail-react-app/app/components/item-variant/item-name'
43
43
  import CartItemVariantAttributes from '@salesforce/retail-react-app/app/components/item-variant/item-attributes'
44
44
  import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/item-variant/item-price'
45
+ import MultiShipOrderSummary from '@salesforce/retail-react-app/app/components/multiship/multiship-order-summary'
46
+ import ShipmentDetails from '@salesforce/retail-react-app/app/pages/checkout/partials/shipment-details'
47
+
48
+ // Hooks
49
+ import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
45
50
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
46
- import {
47
- API_ERROR_MESSAGE,
48
- STORE_LOCATOR_IS_ENABLED
49
- } from '@salesforce/retail-react-app/app/constants'
50
51
  import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
51
- import {nanoid} from 'nanoid'
52
+
53
+ // Constants
54
+ import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'
52
55
 
53
56
  const onClient = typeof window !== 'undefined'
54
57
 
@@ -69,25 +72,13 @@ const CheckoutConfirmation = () => {
69
72
  const {currency} = useCurrency()
70
73
  const itemIds = order?.productItems.map((item) => item.productId)
71
74
  const {data: products} = useProducts({parameters: {ids: itemIds?.join(',')}})
72
- const productItemsMap = products?.data.reduce((map, item) => ({...map, [item.id]: item}), {})
75
+ const productItemsMap = (products?.data || []).reduce(
76
+ (map, item) => ({...map, [item.id]: item}),
77
+ {}
78
+ )
73
79
  const form = useForm()
74
80
 
75
- // Check if this is a pickup order and get store details
76
- const isPickupOrder = STORE_LOCATOR_IS_ENABLED
77
- ? order?.shipments?.[0]?.shippingMethod?.c_storePickupEnabled === true
78
- : false
79
- const storeId = order?.shipments?.[0]?.c_fromStoreId
80
- const {data: storeData} = useStores(
81
- {
82
- parameters: {
83
- ids: storeId
84
- }
85
- },
86
- {
87
- enabled: !!storeId && isPickupOrder && onClient
88
- }
89
- )
90
- const store = storeData?.data?.[0]
81
+ const hasMultipleShipments = order?.shipments && order.shipments.length > 1
91
82
 
92
83
  useEffect(() => {
93
84
  form.reset({
@@ -108,7 +99,8 @@ const CheckoutConfirmation = () => {
108
99
  const saveShippingAddress = async (customerId) => {
109
100
  try {
110
101
  const shippingAddress = order.shipments[0].shippingAddress
111
- let {id, ...shippingAddressWithoutId} = shippingAddress
102
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
103
+ const {id, ...shippingAddressWithoutId} = shippingAddress
112
104
  const bodyShippingAddress = {
113
105
  addressId: nanoid(),
114
106
  ...shippingAddressWithoutId
@@ -269,90 +261,8 @@ const CheckoutConfirmation = () => {
269
261
  </Box>
270
262
  )}
271
263
 
272
- <Box layerStyle="card" rounded={[0, 0, 'base']} px={[4, 4, 6]} py={[6, 6, 8]}>
273
- <Container variant="form">
274
- <Stack spacing={6}>
275
- {isPickupOrder ? (
276
- <>
277
- <Heading fontSize="lg">
278
- <FormattedMessage
279
- defaultMessage="Pickup Details"
280
- id="checkout_confirmation.heading.pickup_details"
281
- />
282
- </Heading>
283
-
284
- <Stack spacing={2}>
285
- <Heading as="h3" fontSize="md">
286
- <FormattedMessage
287
- defaultMessage="Pickup Address"
288
- id="checkout_confirmation.heading.pickup_address"
289
- />
290
- </Heading>
291
- {store ? (
292
- <StoreDisplay
293
- store={store}
294
- showDistance={false}
295
- showEmail={true}
296
- showPhone={true}
297
- showStoreHours={true}
298
- />
299
- ) : (
300
- <Text>
301
- <FormattedMessage
302
- defaultMessage="Store information isn't available"
303
- id="checkout_confirmation.message.store_info_unavailable"
304
- />
305
- </Text>
306
- )}
307
- </Stack>
308
- </>
309
- ) : (
310
- <>
311
- <Heading fontSize="lg">
312
- <FormattedMessage
313
- defaultMessage="Delivery Details"
314
- id="checkout_confirmation.heading.delivery_details"
315
- />
316
- </Heading>
317
-
318
- <SimpleGrid columns={[1, 1, 2]} spacing={6}>
319
- <Stack spacing={1}>
320
- <Heading as="h3" fontSize="sm">
321
- <FormattedMessage
322
- defaultMessage="Shipping Address"
323
- id="checkout_confirmation.heading.shipping_address"
324
- />
325
- </Heading>
326
- <AddressDisplay
327
- address={order.shipments[0].shippingAddress}
328
- />
329
- </Stack>
330
-
331
- <Stack spacing={1}>
332
- <Heading as="h3" fontSize="sm">
333
- <FormattedMessage
334
- defaultMessage="Shipping Method"
335
- id="checkout_confirmation.heading.shipping_method"
336
- />
337
- </Heading>
338
- <Box>
339
- <Text>
340
- {order.shipments[0].shippingMethod.name}
341
- </Text>
342
- <Text>
343
- {
344
- order.shipments[0].shippingMethod
345
- .description
346
- }
347
- </Text>
348
- </Box>
349
- </Stack>
350
- </SimpleGrid>
351
- </>
352
- )}
353
- </Stack>
354
- </Container>
355
- </Box>
264
+ {/* Shipment Details */}
265
+ <ShipmentDetails shipments={order.shipments} />
356
266
 
357
267
  <Box layerStyle="card" rounded={[0, 0, 'base']} px={[4, 4, 6]} py={[6, 6, 8]}>
358
268
  <Container variant="form">
@@ -380,56 +290,67 @@ const CheckoutConfirmation = () => {
380
290
  </Text>
381
291
 
382
292
  <Stack spacing={5} align="flex-start">
383
- <Stack
384
- spacing={5}
385
- align="flex-start"
386
- width="full"
387
- divider={<Divider />}
388
- >
389
- {order.productItems?.map((product, idx) => {
390
- const productDetail =
391
- productItemsMap?.[product.productId] || {}
392
- const variant = {
393
- ...product,
394
- ...productDetail,
395
- price: product.price
396
- }
397
-
398
- return (
399
- <ItemVariantProvider
400
- key={product.productId}
401
- index={idx}
402
- variant={variant}
403
- >
404
- <Flex width="full" alignItems="flex-start">
405
- <CartItemVariantImage
406
- width="80px"
407
- mr={2}
408
- />
409
- <Stack
410
- spacing={1}
411
- marginTop="-3px"
412
- flex={1}
293
+ {hasMultipleShipments ? (
294
+ <MultiShipOrderSummary
295
+ order={order}
296
+ productItemsMap={productItemsMap}
297
+ currency={currency}
298
+ />
299
+ ) : (
300
+ <Stack
301
+ spacing={5}
302
+ align="flex-start"
303
+ width="full"
304
+ divider={<Divider />}
305
+ >
306
+ {order.productItems?.map((product, idx) => {
307
+ const productDetail =
308
+ productItemsMap?.[product.productId] || {}
309
+ const variant = {
310
+ ...product,
311
+ ...productDetail,
312
+ price: product.price
313
+ }
314
+
315
+ return (
316
+ <ItemVariantProvider
317
+ key={product.productId}
318
+ index={idx}
319
+ variant={variant}
320
+ >
321
+ <Flex
322
+ width="full"
323
+ alignItems="flex-start"
413
324
  >
414
- <CartItemVariantName />
415
- <Flex
416
- width="full"
417
- justifyContent="space-between"
418
- alignItems="flex-end"
325
+ <CartItemVariantImage
326
+ width="80px"
327
+ mr={2}
328
+ />
329
+ <Stack
330
+ spacing={1}
331
+ marginTop="-3px"
332
+ flex={1}
419
333
  >
420
- <CartItemVariantAttributes
421
- includeQuantity
422
- />
423
- <CartItemVariantPrice
424
- currency={currency}
425
- />
426
- </Flex>
427
- </Stack>
428
- </Flex>
429
- </ItemVariantProvider>
430
- )
431
- })}
432
- </Stack>
334
+ <CartItemVariantName />
335
+ <Flex
336
+ width="full"
337
+ justifyContent="space-between"
338
+ alignItems="flex-end"
339
+ >
340
+ <CartItemVariantAttributes
341
+ includeQuantity
342
+ />
343
+ <CartItemVariantPrice
344
+ currency={currency}
345
+ />
346
+ </Flex>
347
+ </Stack>
348
+ </Flex>
349
+ </ItemVariantProvider>
350
+ )
351
+ })}
352
+ </Stack>
353
+ )}
433
354
 
434
355
  <Stack w="full" py={4} borderY="1px" borderColor="gray.200">
435
356
  <Flex justify="space-between">
@@ -24,7 +24,7 @@ import {
24
24
  import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout/partials/contact-info'
25
25
  import PickupAddress from '@salesforce/retail-react-app/app/pages/checkout/partials/pickup-address'
26
26
  import ShippingAddress from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-address'
27
- import ShippingOptions from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-options'
27
+ import ShippingMethods from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-methods'
28
28
  import Payment from '@salesforce/retail-react-app/app/pages/checkout/partials/payment'
29
29
  import OrderSummary from '@salesforce/retail-react-app/app/components/order-summary'
30
30
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
@@ -34,30 +34,39 @@ import {useShopperOrdersMutation, useShopperBasketsMutation} from '@salesforce/c
34
34
  import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal'
35
35
  import {
36
36
  API_ERROR_MESSAGE,
37
- TOAST_MESSAGE_REMOVED_ITEM_FROM_CART,
38
- STORE_LOCATOR_IS_ENABLED
37
+ TOAST_MESSAGE_REMOVED_ITEM_FROM_CART
39
38
  } from '@salesforce/retail-react-app/app/constants'
40
39
  import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
41
40
  import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
42
41
  import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
42
+ import {useMultiship} from '@salesforce/retail-react-app/app/hooks/use-multiship'
43
43
 
44
44
  const Checkout = () => {
45
45
  const {formatMessage} = useIntl()
46
46
  const navigate = useNavigation()
47
47
  const {step} = useCheckout()
48
48
  const [error, setError] = useState()
49
- const {data: basket} = useCurrentBasket()
49
+ const {data: basket, derivedData} = useCurrentBasket()
50
50
  const [isLoading, setIsLoading] = useState(false)
51
51
  const {mutateAsync: createOrder} = useShopperOrdersMutation('createOrder')
52
52
  const {passwordless = {}, social = {}} = getConfig().app.login || {}
53
53
  const idps = social?.idps
54
54
  const isSocialEnabled = !!social?.enabled
55
55
  const isPasswordlessEnabled = !!passwordless?.enabled
56
+ const {removeEmptyShipments} = useMultiship(basket)
57
+ const multishipEnabled = getConfig()?.app?.multishipEnabled ?? true
58
+
59
+ // cart has both pickup and delivery orders
60
+ const isDeliveryAndPickupOrder =
61
+ multishipEnabled &&
62
+ derivedData?.totalPickupShipments > 0 &&
63
+ derivedData?.totalDeliveryShipments > 0
64
+
65
+ // Check if there are pickup shipments
66
+ const hasPickupShipments = derivedData?.totalPickupShipments > 0
56
67
 
57
68
  // Only enable BOPIS functionality if the feature toggle is on
58
- const isPickupOrder = STORE_LOCATOR_IS_ENABLED
59
- ? basket?.shipments[0]?.shippingMethod?.c_storePickupEnabled === true
60
- : false
69
+ const isPickupOrderOnly = !isDeliveryAndPickupOrder && hasPickupShipments
61
70
 
62
71
  useEffect(() => {
63
72
  if (error || step === 4) {
@@ -65,6 +74,14 @@ const Checkout = () => {
65
74
  }
66
75
  }, [error, step])
67
76
 
77
+ // Remove any empty shipments whenever navigating to the checkout page
78
+ // Using basketId ensures that the basket is in a valid state before removing empty shipments
79
+ useEffect(() => {
80
+ if (basket?.shipments?.length > 1) {
81
+ removeEmptyShipments(basket)
82
+ }
83
+ }, [basket?.basketId])
84
+
68
85
  const submitOrder = async () => {
69
86
  setIsLoading(true)
70
87
  try {
@@ -106,8 +123,16 @@ const Checkout = () => {
106
123
  isPasswordlessEnabled={isPasswordlessEnabled}
107
124
  idps={idps}
108
125
  />
109
- {isPickupOrder ? <PickupAddress /> : <ShippingAddress />}
110
- {!isPickupOrder && <ShippingOptions />}
126
+
127
+ {isPickupOrderOnly ? (
128
+ <PickupAddress />
129
+ ) : (
130
+ <>
131
+ {hasPickupShipments && <PickupAddress />}
132
+ <ShippingAddress />
133
+ <ShippingMethods />
134
+ </>
135
+ )}
111
136
  <Payment />
112
137
 
113
138
  {step === 5 && (