@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,246 @@
1
+ /*
2
+ * Copyright (c) 2025, salesforce.com, 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
+
8
+ import React from 'react'
9
+ import {screen} from '@testing-library/react'
10
+ import ShipmentDetails from '@salesforce/retail-react-app/app/pages/checkout/partials/shipment-details'
11
+ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
12
+
13
+ // Mock the useStores hook
14
+ jest.mock('@salesforce/commerce-sdk-react', () => ({
15
+ ...jest.requireActual('@salesforce/commerce-sdk-react'),
16
+ useStores: jest.fn()
17
+ }))
18
+
19
+ import {useStores} from '@salesforce/commerce-sdk-react'
20
+
21
+ describe('ShipmentDetails', () => {
22
+ const mockStoresData = {
23
+ data: [
24
+ {
25
+ id: 'store-001',
26
+ name: 'Downtown Store',
27
+ address1: '123 Main Street',
28
+ city: 'San Francisco',
29
+ stateCode: 'CA',
30
+ postalCode: '94105',
31
+ countryCode: 'US',
32
+ phone: '(555) 123-4567',
33
+ c_customerServiceEmail: 'downtown@example.com',
34
+ storeHours:
35
+ 'Monday - Friday: 9:00 AM - 8:00 PM\nSaturday: 10:00 AM - 6:00 PM\nSunday: 12:00 PM - 5:00 PM',
36
+ storeType: 'retail'
37
+ },
38
+ {
39
+ id: 'store-002',
40
+ name: 'Uptown Store',
41
+ address1: '456 Oak Avenue',
42
+ city: 'San Francisco',
43
+ stateCode: 'CA',
44
+ postalCode: '94102',
45
+ countryCode: 'US',
46
+ phone: '(555) 987-6543',
47
+ c_customerServiceEmail: 'uptown@example.com',
48
+ storeHours:
49
+ 'Monday - Friday: 10:00 AM - 9:00 PM\nSaturday: 11:00 AM - 7:00 PM\nSunday: 1:00 PM - 6:00 PM',
50
+ storeType: 'retail'
51
+ }
52
+ ]
53
+ }
54
+
55
+ const mockPickupShipment = {
56
+ shipmentId: 'pickup-1',
57
+ c_fromStoreId: 'store-001',
58
+ shippingMethod: {
59
+ c_storePickupEnabled: true,
60
+ name: 'Store Pickup',
61
+ description: 'Pick up at store location'
62
+ }
63
+ }
64
+
65
+ const mockDeliveryShipment = {
66
+ shipmentId: 'delivery-1',
67
+ shippingMethod: {
68
+ c_storePickupEnabled: false,
69
+ name: 'Standard Shipping',
70
+ description: '3-5 business days'
71
+ },
72
+ shippingAddress: {
73
+ address1: '123 Delivery Street',
74
+ city: 'San Francisco',
75
+ stateCode: 'CA',
76
+ postalCode: '94105',
77
+ countryCode: 'US'
78
+ }
79
+ }
80
+
81
+ const defaultProps = {
82
+ shipments: [mockPickupShipment, mockDeliveryShipment]
83
+ }
84
+
85
+ beforeEach(() => {
86
+ jest.clearAllMocks()
87
+ // Default mock implementation for useStores
88
+ useStores.mockReturnValue({
89
+ data: mockStoresData,
90
+ isLoading: false,
91
+ error: null
92
+ })
93
+ })
94
+
95
+ test('renders component with pickup and delivery sections', () => {
96
+ renderWithProviders(<ShipmentDetails {...defaultProps} />)
97
+
98
+ expect(screen.getByText('Pickup Details')).toBeInTheDocument()
99
+ expect(screen.getByText('Delivery Details')).toBeInTheDocument()
100
+ })
101
+
102
+ test('renders pickup location information when store data is available', () => {
103
+ renderWithProviders(<ShipmentDetails {...defaultProps} />)
104
+
105
+ expect(screen.getByText('Pickup Address')).toBeInTheDocument()
106
+ expect(screen.getByText('Downtown Store')).toBeInTheDocument()
107
+ expect(screen.getByText('123 Main Street')).toBeInTheDocument()
108
+ })
109
+
110
+ test('renders delivery address and shipping method information', () => {
111
+ renderWithProviders(<ShipmentDetails {...defaultProps} />)
112
+
113
+ expect(screen.getByText('Shipping Address')).toBeInTheDocument()
114
+ expect(screen.getByText('Shipping Method')).toBeInTheDocument()
115
+ expect(screen.getByText('123 Delivery Street')).toBeInTheDocument()
116
+ expect(screen.getByText('Standard Shipping')).toBeInTheDocument()
117
+ expect(screen.getByText('3-5 business days')).toBeInTheDocument()
118
+ })
119
+
120
+ test('renders pickup location numbers when multiple pickup shipments exist', () => {
121
+ const multiplePickupShipments = [
122
+ {
123
+ ...mockPickupShipment,
124
+ c_fromStoreId: 'store-001'
125
+ },
126
+ {
127
+ ...mockPickupShipment,
128
+ shipmentId: 'pickup-2',
129
+ c_fromStoreId: 'store-002'
130
+ }
131
+ ]
132
+
133
+ renderWithProviders(<ShipmentDetails shipments={multiplePickupShipments} />)
134
+
135
+ expect(screen.getByText('Pickup Location 1')).toBeInTheDocument()
136
+ expect(screen.getByText('Pickup Location 2')).toBeInTheDocument()
137
+ })
138
+
139
+ test('renders delivery numbers when multiple delivery shipments exist', () => {
140
+ const multipleDeliveryShipments = [
141
+ mockDeliveryShipment,
142
+ {
143
+ ...mockDeliveryShipment,
144
+ shipmentId: 'delivery-2',
145
+ shippingAddress: {
146
+ address1: '456 Second Street',
147
+ city: 'Los Angeles',
148
+ stateCode: 'CA',
149
+ postalCode: '90210',
150
+ countryCode: 'US'
151
+ }
152
+ }
153
+ ]
154
+
155
+ renderWithProviders(<ShipmentDetails shipments={multipleDeliveryShipments} />)
156
+
157
+ expect(screen.getByText('Delivery 1')).toBeInTheDocument()
158
+ expect(screen.getByText('Delivery 2')).toBeInTheDocument()
159
+ })
160
+
161
+ test('shows store information unavailable message when store data is not available', () => {
162
+ useStores.mockReturnValue({
163
+ data: null,
164
+ isLoading: false,
165
+ error: null
166
+ })
167
+
168
+ renderWithProviders(<ShipmentDetails shipments={[mockPickupShipment]} />)
169
+
170
+ expect(screen.getByText("Store information isn't available")).toBeInTheDocument()
171
+ })
172
+
173
+ test('does not render pickup section when no pickup shipments exist', () => {
174
+ const deliveryOnlyShipments = [mockDeliveryShipment]
175
+
176
+ renderWithProviders(<ShipmentDetails shipments={deliveryOnlyShipments} />)
177
+
178
+ expect(screen.queryByText('Pickup Details')).not.toBeInTheDocument()
179
+ expect(screen.getByText('Delivery Details')).toBeInTheDocument()
180
+ })
181
+
182
+ test('does not render delivery section when no delivery shipments exist', () => {
183
+ const pickupOnlyShipments = [mockPickupShipment]
184
+
185
+ renderWithProviders(<ShipmentDetails shipments={pickupOnlyShipments} />)
186
+
187
+ expect(screen.getByText('Pickup Details')).toBeInTheDocument()
188
+ expect(screen.queryByText('Delivery Details')).not.toBeInTheDocument()
189
+ })
190
+
191
+ test('handles shipments without store IDs gracefully', () => {
192
+ const shipmentWithoutStoreId = {
193
+ ...mockPickupShipment,
194
+ c_fromStoreId: null
195
+ }
196
+
197
+ renderWithProviders(<ShipmentDetails shipments={[shipmentWithoutStoreId]} />)
198
+
199
+ expect(screen.getByText('Pickup Details')).toBeInTheDocument()
200
+ expect(screen.getByText("Store information isn't available")).toBeInTheDocument()
201
+ })
202
+
203
+ test('calls useStores with correct parameters for pickup shipments', () => {
204
+ const pickupShipments = [
205
+ {
206
+ ...mockPickupShipment,
207
+ c_fromStoreId: 'store-001'
208
+ },
209
+ {
210
+ ...mockPickupShipment,
211
+ shipmentId: 'pickup-2',
212
+ c_fromStoreId: 'store-002'
213
+ }
214
+ ]
215
+
216
+ renderWithProviders(<ShipmentDetails shipments={pickupShipments} />)
217
+
218
+ expect(useStores).toHaveBeenCalledWith(
219
+ {
220
+ parameters: {
221
+ ids: 'store-001,store-002'
222
+ }
223
+ },
224
+ {
225
+ enabled: true
226
+ }
227
+ )
228
+ })
229
+
230
+ test('does not call useStores when no pickup shipments exist', () => {
231
+ const deliveryOnlyShipments = [mockDeliveryShipment]
232
+
233
+ renderWithProviders(<ShipmentDetails shipments={deliveryOnlyShipments} />)
234
+
235
+ expect(useStores).toHaveBeenCalledWith(
236
+ {
237
+ parameters: {
238
+ ids: ''
239
+ }
240
+ },
241
+ {
242
+ enabled: false
243
+ }
244
+ )
245
+ })
246
+ })
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: BSD-3-Clause
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
- import React, {useState} from 'react'
7
+ import React, {useState, useEffect} from 'react'
8
8
  import {nanoid} from 'nanoid'
9
9
  import {defineMessage, useIntl} from 'react-intl'
10
10
  import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout/util/checkout-context'
@@ -13,6 +13,7 @@ import {
13
13
  ToggleCardEdit,
14
14
  ToggleCardSummary
15
15
  } from '@salesforce/retail-react-app/app/components/toggle-card'
16
+ import {Text} from '@salesforce/retail-react-app/app/components/shared/ui'
16
17
  import ShippingAddressSelection from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-address-selection'
17
18
  import AddressDisplay from '@salesforce/retail-react-app/app/components/address-display'
18
19
  import {
@@ -21,6 +22,19 @@ import {
21
22
  } from '@salesforce/commerce-sdk-react'
22
23
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
23
24
  import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
25
+ import ShippingMultiAddress from '@salesforce/retail-react-app/app/pages/checkout/partials/shipping-multi-address'
26
+ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
27
+ import {useMultiship} from '@salesforce/retail-react-app/app/hooks/use-multiship'
28
+ import {DEFAULT_SHIPMENT_ID} from '@salesforce/retail-react-app/app/constants'
29
+ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
30
+ import {
31
+ sanitizedCustomerAddress,
32
+ cleanAddressForOrder
33
+ } from '@salesforce/retail-react-app/app/utils/address-utils'
34
+ import {
35
+ findExistingDeliveryShipment,
36
+ isPickupShipment
37
+ } from '@salesforce/retail-react-app/app/utils/shipment-utils'
24
38
 
25
39
  const submitButtonMessage = defineMessage({
26
40
  defaultMessage: 'Continue to Shipping Method',
@@ -30,84 +44,120 @@ const shippingAddressAriaLabel = defineMessage({
30
44
  defaultMessage: 'Shipping Address Form',
31
45
  id: 'shipping_address.label.shipping_address_form'
32
46
  })
47
+ const noItemsInBasketMessage = defineMessage({
48
+ defaultMessage: 'No items in basket.',
49
+ id: 'shipping_address.message.no_items_in_basket'
50
+ })
51
+ const shipToOneAddressLabel = defineMessage({
52
+ defaultMessage: 'Ship to Single Address',
53
+ id: 'shipping_address.action.ship_to_single_address'
54
+ })
55
+ const deliverToMultipleAddressesLabel = defineMessage({
56
+ defaultMessage: 'Ship to Multiple Addresses',
57
+ id: 'shipping_address.action.ship_to_multiple_addresses'
58
+ })
33
59
 
34
60
  export default function ShippingAddress() {
35
61
  const {formatMessage} = useIntl()
36
62
  const [isLoading, setIsLoading] = useState()
37
63
  const {data: customer} = useCurrentCustomer()
38
64
  const {data: basket} = useCurrentBasket()
39
- const selectedShippingAddress = basket?.shipments && basket?.shipments[0]?.shippingAddress
65
+ const multishipEnabled = getConfig()?.app?.multishipEnabled ?? true
66
+ const {moveItemsToDeliveryShipment, removeEmptyShipments} = useMultiship(basket)
67
+ const selectedShipment = findExistingDeliveryShipment(basket)
68
+ const selectedShippingAddress = selectedShipment?.shippingAddress
40
69
  const isAddressFilled = selectedShippingAddress?.address1 && selectedShippingAddress?.city
41
- const {step, STEPS, goToStep, goToNextStep} = useCheckout()
70
+
71
+ // Check if there are multiple delivery shipments (multi-shipping was used)
72
+ const deliveryShipments =
73
+ basket?.shipments?.filter((shipment) => !isPickupShipment(shipment)) || []
74
+ const hasMultipleDeliveryShipments = deliveryShipments.length > 1
75
+
76
+ // Initialize multi-shipping state based on existing basket shipments
77
+ const [isMultiShipping, setIsMultiShipping] = useState(hasMultipleDeliveryShipments)
78
+ const {step, STEPS, goToStep} = useCheckout()
42
79
  const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
43
80
  const updateCustomerAddress = useShopperCustomersMutation('updateCustomerAddress')
44
81
  const updateShippingAddressForShipment = useShopperBasketsMutation(
45
82
  'updateShippingAddressForShipment'
46
83
  )
84
+ const showToast = useToast()
85
+
86
+ // Keep multi-shipping state in sync with basket shipments
87
+ useEffect(() => {
88
+ setIsMultiShipping(hasMultipleDeliveryShipments)
89
+ }, [hasMultipleDeliveryShipments])
47
90
 
48
91
  const submitAndContinue = async (address) => {
49
92
  setIsLoading(true)
50
- const {
51
- addressId,
52
- address1,
53
- city,
54
- countryCode,
55
- firstName,
56
- lastName,
57
- phone,
58
- postalCode,
59
- stateCode
60
- } = address
61
- await updateShippingAddressForShipment.mutateAsync({
62
- parameters: {
63
- basketId: basket.basketId,
64
- shipmentId: 'me',
65
- useAsBilling: false
66
- },
67
- body: {
68
- address1,
69
- city,
70
- countryCode,
71
- firstName,
72
- lastName,
73
- phone,
74
- postalCode,
75
- stateCode
76
- }
77
- })
93
+ try {
94
+ const {addressId} = address
95
+ const targetShipment = findExistingDeliveryShipment(basket)
96
+ const targetShipmentId = targetShipment?.shipmentId || DEFAULT_SHIPMENT_ID
97
+ let basketAfterItemMoves = null
78
98
 
79
- if (customer.isRegistered && !addressId) {
80
- const body = {
81
- address1,
82
- city,
83
- countryCode,
84
- firstName,
85
- lastName,
86
- phone,
87
- postalCode,
88
- stateCode,
89
- addressId: nanoid()
90
- }
91
- await createCustomerAddress.mutateAsync({
92
- body,
93
- parameters: {customerId: customer.customerId}
99
+ await updateShippingAddressForShipment.mutateAsync({
100
+ parameters: {
101
+ basketId: basket.basketId,
102
+ shipmentId: targetShipmentId,
103
+ useAsBilling: false
104
+ },
105
+ body: cleanAddressForOrder(address)
94
106
  })
95
- }
96
107
 
97
- if (customer.isRegistered && addressId) {
98
- await updateCustomerAddress.mutateAsync({
99
- body: address,
100
- parameters: {
101
- customerId: customer.customerId,
102
- addressName: addressId
108
+ if (customer.isRegistered && !addressId) {
109
+ const body = {
110
+ ...sanitizedCustomerAddress(address),
111
+ addressId: nanoid()
103
112
  }
113
+ await createCustomerAddress.mutateAsync({
114
+ body,
115
+ parameters: {customerId: customer.customerId}
116
+ })
117
+ }
118
+
119
+ if (customer.isRegistered && addressId) {
120
+ await updateCustomerAddress.mutateAsync({
121
+ body: address,
122
+ parameters: {
123
+ customerId: customer.customerId,
124
+ addressName: addressId
125
+ }
126
+ })
127
+ }
128
+ // Move all items to the single target delivery shipment.
129
+ const deliveryItems =
130
+ basket?.productItems?.filter((item) =>
131
+ deliveryShipments.some((shipment) => shipment.shipmentId === item.shipmentId)
132
+ ) || []
133
+ const itemsToMove = deliveryItems.filter((item) => item.shipmentId !== targetShipmentId)
134
+ if (itemsToMove.length > 0) {
135
+ basketAfterItemMoves = await moveItemsToDeliveryShipment(
136
+ itemsToMove,
137
+ targetShipmentId
138
+ )
139
+ }
140
+ // Remove any empty shipments.
141
+ await removeEmptyShipments(basketAfterItemMoves || basket)
142
+
143
+ goToStep(STEPS.SHIPPING_OPTIONS)
144
+ } catch (e) {
145
+ showToast({
146
+ title: formatMessage({
147
+ defaultMessage:
148
+ 'Something went wrong while updating the shipping address. Try again.',
149
+ id: 'shipping_address.error.update_failed'
150
+ }),
151
+ status: 'error'
104
152
  })
153
+ } finally {
154
+ setIsLoading(false)
105
155
  }
106
-
107
- goToNextStep()
108
- setIsLoading(false)
109
156
  }
110
157
 
158
+ // Determine if multi-shipping should be available
159
+ const isEditingShippingAddress = step === STEPS.SHIPPING_ADDRESS
160
+
111
161
  return (
112
162
  <ToggleCard
113
163
  id="step-1"
@@ -115,26 +165,64 @@ export default function ShippingAddress() {
115
165
  defaultMessage: 'Shipping Address',
116
166
  id: 'shipping_address.title.shipping_address'
117
167
  })}
118
- editing={step === STEPS.SHIPPING_ADDRESS}
168
+ editing={isEditingShippingAddress}
119
169
  isLoading={isLoading}
120
170
  disabled={step === STEPS.CONTACT_INFO && !selectedShippingAddress}
121
171
  onEdit={() => goToStep(STEPS.SHIPPING_ADDRESS)}
122
- editLabel={formatMessage({
123
- defaultMessage: 'Edit Shipping Address',
124
- id: 'toggle_card.action.editShippingAddress'
125
- })}
172
+ editLabel={
173
+ isMultiShipping
174
+ ? formatMessage({
175
+ defaultMessage: 'Edit Shipping Addresses',
176
+ id: 'toggle_card.action.editShippingAddresses'
177
+ })
178
+ : formatMessage({
179
+ defaultMessage: 'Edit Shipping Address',
180
+ id: 'toggle_card.action.editShippingAddress'
181
+ })
182
+ }
183
+ editAction={
184
+ multishipEnabled
185
+ ? isMultiShipping
186
+ ? formatMessage(shipToOneAddressLabel)
187
+ : formatMessage(deliverToMultipleAddressesLabel)
188
+ : null
189
+ }
190
+ onEditActionClick={
191
+ multishipEnabled
192
+ ? async () => {
193
+ setIsMultiShipping(!isMultiShipping)
194
+ }
195
+ : null
196
+ }
126
197
  >
127
198
  <ToggleCardEdit>
128
- <ShippingAddressSelection
129
- selectedAddress={selectedShippingAddress}
130
- submitButtonLabel={submitButtonMessage}
131
- onSubmit={submitAndContinue}
132
- formTitleAriaLabel={shippingAddressAriaLabel}
133
- />
199
+ {!isMultiShipping ? (
200
+ <ShippingAddressSelection
201
+ selectedAddress={selectedShippingAddress}
202
+ submitButtonLabel={submitButtonMessage}
203
+ onSubmit={submitAndContinue}
204
+ formTitleAriaLabel={shippingAddressAriaLabel}
205
+ />
206
+ ) : (
207
+ <ShippingMultiAddress
208
+ basket={basket}
209
+ submitButtonLabel={submitButtonMessage}
210
+ noItemsInBasketMessage={noItemsInBasketMessage}
211
+ />
212
+ )}
134
213
  </ToggleCardEdit>
135
214
  {isAddressFilled && (
136
215
  <ToggleCardSummary>
137
- <AddressDisplay address={selectedShippingAddress} />
216
+ {hasMultipleDeliveryShipments ? (
217
+ <Text>
218
+ {formatMessage({
219
+ defaultMessage: 'Your items will be shipped to multiple addresses.',
220
+ id: 'shipping_address.summary.multiple_addresses'
221
+ })}
222
+ </Text>
223
+ ) : (
224
+ <AddressDisplay address={selectedShippingAddress} />
225
+ )}
138
226
  </ToggleCardSummary>
139
227
  )}
140
228
  </ToggleCard>