@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.
- package/CHANGELOG.md +8 -4
- package/app/components/_app/index.jsx +9 -7
- package/app/components/_app/index.test.js +2 -2
- package/app/components/_app-config/index.jsx +9 -3
- package/app/components/drawer-menu/drawer-menu.jsx +3 -1
- package/app/components/footer/index.jsx +3 -1
- package/app/components/header/index.jsx +3 -1
- package/app/components/header/index.test.js +2 -2
- package/app/components/island/README.md +1 -1
- package/app/components/island/index.jsx +3 -1
- package/app/components/island/index.test.js +94 -5
- package/app/components/item-variant/item-attributes.jsx +12 -3
- package/app/components/multiship/multiship-order-summary.jsx +137 -0
- package/app/components/multiship/multiship-order-summary.test.js +121 -0
- package/app/components/order-summary/index.jsx +2 -4
- package/app/components/pickup-or-delivery/index.jsx +80 -0
- package/app/components/pickup-or-delivery/index.test.jsx +182 -0
- package/app/components/product-item/index.jsx +26 -16
- package/app/components/product-item/index.test.js +29 -2
- package/app/components/product-item-list/index.jsx +10 -0
- package/app/components/product-item-list/index.test.jsx +14 -0
- package/app/components/product-view/index.jsx +9 -6
- package/app/components/product-view/index.test.js +25 -21
- package/app/components/quantity-picker/index.test.jsx +12 -12
- package/app/components/reset-password/index.test.js +1 -1
- package/app/components/shared/ui/AlertDescription/index.jsx +8 -0
- package/app/components/shared/ui/index.jsx +1 -0
- package/app/components/store-display/index.jsx +28 -4
- package/app/components/store-display/index.test.js +71 -0
- package/app/components/store-locator/form.test.jsx +16 -4
- package/app/components/store-locator/list.jsx +9 -4
- package/app/components/toggle-card/index.jsx +14 -0
- package/app/components/unavailable-product-confirmation-modal/index.jsx +19 -5
- package/app/components/unavailable-product-confirmation-modal/index.test.js +122 -1
- package/app/constants.js +20 -6
- package/app/contexts/store-locator-provider.jsx +7 -1
- package/app/contexts/store-locator-provider.test.jsx +36 -1
- package/app/hooks/use-address-form.js +155 -0
- package/app/hooks/use-address-form.test.js +501 -0
- package/app/hooks/use-auth-modal.js +2 -6
- package/app/hooks/use-current-basket.js +71 -2
- package/app/hooks/use-current-basket.test.js +37 -1
- package/app/hooks/use-dnt-notification.js +4 -4
- package/app/hooks/use-dnt-notification.test.js +5 -5
- package/app/hooks/use-item-shipment-management.js +233 -0
- package/app/hooks/use-item-shipment-management.test.js +696 -0
- package/app/hooks/use-multiship.js +589 -0
- package/app/hooks/use-multiship.test.js +776 -0
- package/app/hooks/use-pickup-shipment.js +70 -106
- package/app/hooks/use-pickup-shipment.test.js +345 -209
- package/app/hooks/use-product-address-assignment.js +280 -0
- package/app/hooks/use-product-address-assignment.test.js +414 -0
- package/app/hooks/use-product-inventory.js +100 -0
- package/app/hooks/use-product-inventory.test.js +254 -0
- package/app/hooks/use-shipment-operations.js +168 -0
- package/app/hooks/use-shipment-operations.test.js +385 -0
- package/app/hooks/use-store-locator.js +24 -2
- package/app/hooks/use-store-locator.test.jsx +109 -1
- package/app/pages/account/index.test.js +1 -1
- package/app/pages/account/profile.test.js +0 -2
- package/app/pages/cart/index.jsx +397 -157
- package/app/pages/cart/index.test.js +353 -2
- package/app/pages/cart/partials/bonus-products-title.jsx +10 -8
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +1 -1
- package/app/pages/cart/partials/order-type-display.jsx +68 -0
- package/app/pages/cart/partials/order-type-display.test.js +241 -0
- package/app/pages/checkout/confirmation.jsx +79 -158
- package/app/pages/checkout/index.jsx +34 -9
- package/app/pages/checkout/index.test.js +245 -118
- package/app/pages/checkout/partials/contact-info.jsx +2 -6
- package/app/pages/checkout/partials/contact-info.test.js +93 -7
- package/app/pages/checkout/partials/payment.jsx +19 -5
- package/app/pages/checkout/partials/pickup-address.jsx +340 -70
- package/app/pages/checkout/partials/pickup-address.test.js +1075 -82
- package/app/pages/checkout/partials/product-shipping-address-card.jsx +382 -0
- package/app/pages/checkout/partials/shipment-details.jsx +209 -0
- package/app/pages/checkout/partials/shipment-details.test.js +246 -0
- package/app/pages/checkout/partials/shipping-address.jsx +156 -68
- package/app/pages/checkout/partials/shipping-address.test.js +673 -0
- package/app/pages/checkout/partials/shipping-method-options.jsx +180 -0
- package/app/pages/checkout/partials/shipping-methods.jsx +403 -0
- package/app/pages/checkout/partials/shipping-methods.test.js +472 -0
- package/app/pages/checkout/partials/shipping-multi-address.jsx +259 -0
- package/app/pages/checkout/partials/shipping-multi-address.test.js +2088 -0
- package/app/pages/checkout/partials/shipping-product-cards.jsx +101 -0
- package/app/pages/checkout/util/checkout-context.js +25 -18
- package/app/pages/login/index.jsx +2 -6
- package/app/pages/product-detail/index.jsx +96 -81
- package/app/pages/product-detail/index.test.js +103 -19
- package/app/pages/product-list/index.jsx +3 -1
- package/app/pages/product-list/partials/inventory-filter.jsx +18 -21
- package/app/pages/product-list/partials/inventory-filter.test.js +15 -17
- package/app/pages/product-list/partials/selected-refinements.jsx +3 -1
- package/app/ssr.js +1 -5
- package/app/static/translations/compiled/en-GB.json +316 -30
- package/app/static/translations/compiled/en-US.json +316 -30
- package/app/static/translations/compiled/en-XA.json +673 -75
- package/app/utils/address-utils.js +112 -0
- package/app/utils/address-utils.test.js +484 -0
- package/app/utils/product-utils.js +17 -5
- package/app/utils/product-utils.test.js +17 -8
- package/app/utils/sfdc-user-agent-utils.js +32 -0
- package/app/utils/sfdc-user-agent-utils.test.js +82 -0
- package/app/utils/shipment-utils.js +196 -0
- package/app/utils/shipment-utils.test.js +458 -0
- package/app/utils/test-utils.js +4 -4
- package/app/utils/utils.js +6 -1
- package/config/default.js +4 -1
- package/config/mocks/default.js +3 -1
- package/package.json +9 -9
- package/translations/en-GB.json +127 -10
- package/translations/en-US.json +127 -10
- 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
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
addressId
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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={
|
|
168
|
+
editing={isEditingShippingAddress}
|
|
119
169
|
isLoading={isLoading}
|
|
120
170
|
disabled={step === STEPS.CONTACT_INFO && !selectedShippingAddress}
|
|
121
171
|
onEdit={() => goToStep(STEPS.SHIPPING_ADDRESS)}
|
|
122
|
-
editLabel={
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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>
|