@salesforce/retail-react-app 7.1.0-preview.1 → 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 -2
- 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 -1
- 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
|
@@ -43,6 +43,53 @@ const mockProductsWithUnavailableProducts = {
|
|
|
43
43
|
]
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const mockProductsWithBopisInventories = {
|
|
47
|
+
limit: 0,
|
|
48
|
+
total: 1,
|
|
49
|
+
data: [
|
|
50
|
+
{
|
|
51
|
+
currency: 'GBP',
|
|
52
|
+
id: '701642889830M',
|
|
53
|
+
imageGroups: [],
|
|
54
|
+
inventories: [
|
|
55
|
+
{
|
|
56
|
+
id: 'bopis-store-1',
|
|
57
|
+
orderable: true,
|
|
58
|
+
stockLevel: 10
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mockProductWithBopisUnavailableInOneStore = {
|
|
66
|
+
limit: 0,
|
|
67
|
+
total: 1,
|
|
68
|
+
data: [
|
|
69
|
+
{
|
|
70
|
+
currency: 'GBP',
|
|
71
|
+
id: 'product-a',
|
|
72
|
+
imageGroups: [],
|
|
73
|
+
inventories: [
|
|
74
|
+
{
|
|
75
|
+
id: 'store-1',
|
|
76
|
+
orderable: true,
|
|
77
|
+
stockLevel: 10
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'store-2',
|
|
81
|
+
orderable: false, // Not orderable from store-2
|
|
82
|
+
stockLevel: 0
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
inventory: {
|
|
86
|
+
orderable: true,
|
|
87
|
+
stockLevel: 100
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
describe('UnavailableProductConfirmationModal', () => {
|
|
47
94
|
test('renders without crashing', () => {
|
|
48
95
|
prependHandlersToServer([
|
|
@@ -103,7 +150,6 @@ describe('UnavailableProductConfirmationModal', () => {
|
|
|
103
150
|
})
|
|
104
151
|
|
|
105
152
|
test('opens confirmation modal when unavailable products are found with defined productIds prop', async () => {
|
|
106
|
-
const mockProductIds = ['701642889899M', '701642889830M']
|
|
107
153
|
prependHandlersToServer([
|
|
108
154
|
{
|
|
109
155
|
path: '*/products',
|
|
@@ -147,3 +193,78 @@ describe('UnavailableProductConfirmationModal', () => {
|
|
|
147
193
|
expect(removeBtn).not.toBeInTheDocument()
|
|
148
194
|
})
|
|
149
195
|
})
|
|
196
|
+
|
|
197
|
+
test('does not open confirmation modal with sufficient bopis inventory', async () => {
|
|
198
|
+
prependHandlersToServer([
|
|
199
|
+
{
|
|
200
|
+
path: '*/products',
|
|
201
|
+
res: () => {
|
|
202
|
+
return mockProductsWithBopisInventories
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
])
|
|
206
|
+
const mockFunc = jest.fn()
|
|
207
|
+
const basket = {
|
|
208
|
+
productItems: [
|
|
209
|
+
{
|
|
210
|
+
productId: '701642889830M',
|
|
211
|
+
quantity: 2,
|
|
212
|
+
inventoryId: 'bopis-store-1' // available
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
const {queryByText} = renderWithProviders(
|
|
217
|
+
<UnavailableProductConfirmationModal
|
|
218
|
+
productItems={basket.productItems}
|
|
219
|
+
handleUnavailableProducts={mockFunc}
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(queryByText(/Items Unavailable/i)).not.toBeInTheDocument()
|
|
225
|
+
})
|
|
226
|
+
expect(mockFunc).not.toHaveBeenCalled()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('opens confirmation modal for bopis item unavailable in selected store', async () => {
|
|
230
|
+
prependHandlersToServer([
|
|
231
|
+
{
|
|
232
|
+
path: '*/products',
|
|
233
|
+
res: () => {
|
|
234
|
+
return mockProductWithBopisUnavailableInOneStore
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
])
|
|
238
|
+
const mockFunc = jest.fn()
|
|
239
|
+
const basket = {
|
|
240
|
+
productItems: [
|
|
241
|
+
{
|
|
242
|
+
productId: 'product-a',
|
|
243
|
+
quantity: 1,
|
|
244
|
+
inventoryId: 'store-2' // unavailable in this store
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
const {getByText, queryByText, queryByRole, user} = renderWithProviders(
|
|
249
|
+
<UnavailableProductConfirmationModal
|
|
250
|
+
productItems={basket.productItems}
|
|
251
|
+
handleUnavailableProducts={mockFunc}
|
|
252
|
+
/>
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
await waitFor(async () => {
|
|
256
|
+
expect(getByText(/^Items Unavailable$/i)).toBeInTheDocument()
|
|
257
|
+
})
|
|
258
|
+
const removeBtn = queryByRole('button')
|
|
259
|
+
|
|
260
|
+
expect(removeBtn).toBeInTheDocument()
|
|
261
|
+
await user.click(removeBtn)
|
|
262
|
+
await waitFor(async () => {
|
|
263
|
+
expect(mockFunc).toHaveBeenCalledWith(['product-a'])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await waitFor(async () => {
|
|
267
|
+
expect(queryByText(/Items Unavailable/i)).not.toBeInTheDocument()
|
|
268
|
+
})
|
|
269
|
+
expect(removeBtn).not.toBeInTheDocument()
|
|
270
|
+
})
|
package/app/constants.js
CHANGED
|
@@ -42,6 +42,9 @@ export const HOME_SHOP_PRODUCTS_LIMIT = 10
|
|
|
42
42
|
export const CAT_MENU_DEFAULT_NAV_SSR_DEPTH = 1
|
|
43
43
|
export const CAT_MENU_DEFAULT_ROOT_CATEGORY = 'root'
|
|
44
44
|
|
|
45
|
+
// Constants for shipments
|
|
46
|
+
export const DEFAULT_SHIPMENT_ID = 'me'
|
|
47
|
+
|
|
45
48
|
// Default details of badge labels and the corresponding product custom properties that enable badges.
|
|
46
49
|
export const PRODUCT_BADGE_DETAILS = [
|
|
47
50
|
{
|
|
@@ -99,11 +102,6 @@ export const FEATURE_UNAVAILABLE_ERROR_MESSAGE = defineMessage({
|
|
|
99
102
|
defaultMessage: 'This feature is not currently available.',
|
|
100
103
|
id: 'global.error.feature_unavailable'
|
|
101
104
|
})
|
|
102
|
-
export const CREATE_ACCOUNT_FIRST_ERROR_MESSAGE = defineMessage({
|
|
103
|
-
defaultMessage:
|
|
104
|
-
'This feature is not currently available. You must create an account to access this feature.',
|
|
105
|
-
id: 'global.error.create_account'
|
|
106
|
-
})
|
|
107
105
|
|
|
108
106
|
export const HOME_HREF = '/'
|
|
109
107
|
|
|
@@ -144,6 +142,11 @@ export const TOAST_MESSAGE_REMOVED_FROM_WISHLIST = defineMessage({
|
|
|
144
142
|
defaultMessage: 'Item removed from wishlist'
|
|
145
143
|
})
|
|
146
144
|
|
|
145
|
+
export const TOAST_MESSAGE_STORE_INSUFFICIENT_INVENTORY = defineMessage({
|
|
146
|
+
id: 'global.info.store_insufficient_inventory',
|
|
147
|
+
defaultMessage: "Some items aren't available for pickup at this store."
|
|
148
|
+
})
|
|
149
|
+
|
|
147
150
|
// Einstein recommender constants used in <RecommendedProducts/>
|
|
148
151
|
export const EINSTEIN_RECOMMENDERS = {
|
|
149
152
|
ADD_TO_CART_MODAL: 'pdp-similar-items',
|
|
@@ -184,6 +187,10 @@ export const REMOVE_UNAVAILABLE_CART_ITEM_DIALOG_CONFIG = {
|
|
|
184
187
|
}),
|
|
185
188
|
onPrimaryAction: noop
|
|
186
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Constant to enable the store locator and shop the store feature.
|
|
192
|
+
* @deprecated Use `storeLocatorEnabled` in the config file instead
|
|
193
|
+
*/
|
|
187
194
|
export const STORE_LOCATOR_IS_ENABLED = true
|
|
188
195
|
export const STORE_LOCATOR_SUPPORTED_COUNTRIES = [
|
|
189
196
|
{
|
|
@@ -255,7 +262,14 @@ export const PASSWORDLESS_ERROR_MESSAGES = [
|
|
|
255
262
|
|
|
256
263
|
export const INVALID_TOKEN_ERROR = /invalid token/i
|
|
257
264
|
|
|
265
|
+
/**
|
|
266
|
+
* @deprecated The SLAS private client proxy will mask user not found errors
|
|
267
|
+
* so this variable should not be used as the app will not see these errors.
|
|
268
|
+
*/
|
|
258
269
|
export const USER_NOT_FOUND_ERROR = /user not found/i
|
|
259
270
|
|
|
260
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Constant to enable partial hydration capabilities, i.e. `<Island/>` components
|
|
273
|
+
* @deprecated Use `partialHydrationEnabled` in the config file instead
|
|
274
|
+
*/
|
|
261
275
|
export const PARTIAL_HYDRATION_ENABLED = false
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import React, {useState, useEffect, createContext} from 'react'
|
|
9
9
|
import PropTypes from 'prop-types'
|
|
10
10
|
import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
|
|
11
|
+
import {useDisclosure} from '@salesforce/retail-react-app/app/components/shared/ui'
|
|
11
12
|
|
|
12
13
|
const onClient = typeof window !== 'undefined'
|
|
13
14
|
|
|
@@ -26,6 +27,7 @@ export const StoreLocatorProvider = ({config, children}) => {
|
|
|
26
27
|
const {site} = useMultiSite()
|
|
27
28
|
const selectedStoreBySiteId = `selectedStore_${site?.id}`
|
|
28
29
|
const selectedStoreId = readValue(selectedStoreBySiteId)
|
|
30
|
+
const {isOpen, onOpen, onClose} = useDisclosure()
|
|
29
31
|
|
|
30
32
|
const [state, setState] = useState({
|
|
31
33
|
mode: 'input',
|
|
@@ -49,7 +51,11 @@ export const StoreLocatorProvider = ({config, children}) => {
|
|
|
49
51
|
|
|
50
52
|
const value = {
|
|
51
53
|
state,
|
|
52
|
-
setState
|
|
54
|
+
setState,
|
|
55
|
+
// Modal actions
|
|
56
|
+
isOpen,
|
|
57
|
+
onOpen,
|
|
58
|
+
onClose
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
return <StoreLocatorContext.Provider value={value}>{children}</StoreLocatorContext.Provider>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import PropTypes from 'prop-types'
|
|
9
9
|
import {render, act} from '@testing-library/react'
|
|
10
|
+
import {MemoryRouter} from 'react-router-dom'
|
|
10
11
|
import {
|
|
11
12
|
StoreLocatorProvider,
|
|
12
13
|
StoreLocatorContext
|
|
@@ -47,7 +48,9 @@ describe('StoreLocatorProvider', () => {
|
|
|
47
48
|
|
|
48
49
|
const TestWrapper = ({children}) => (
|
|
49
50
|
<MultiSiteProvider site={mockSite}>
|
|
50
|
-
<
|
|
51
|
+
<MemoryRouter>
|
|
52
|
+
<StoreLocatorProvider config={mockConfig}>{children}</StoreLocatorProvider>
|
|
53
|
+
</MemoryRouter>
|
|
51
54
|
</MultiSiteProvider>
|
|
52
55
|
)
|
|
53
56
|
|
|
@@ -83,6 +86,9 @@ describe('StoreLocatorProvider', () => {
|
|
|
83
86
|
config: mockConfig
|
|
84
87
|
})
|
|
85
88
|
expect(typeof contextValue?.setState).toBe('function')
|
|
89
|
+
expect(typeof contextValue?.isOpen).toBe('boolean')
|
|
90
|
+
expect(typeof contextValue?.onOpen).toBe('function')
|
|
91
|
+
expect(typeof contextValue?.onClose).toBe('function')
|
|
86
92
|
})
|
|
87
93
|
|
|
88
94
|
it('initializes with stored selectedStoreId from localStorage', () => {
|
|
@@ -169,4 +175,33 @@ describe('StoreLocatorProvider', () => {
|
|
|
169
175
|
|
|
170
176
|
expect(getByText('Test Child')).toBeTruthy()
|
|
171
177
|
})
|
|
178
|
+
|
|
179
|
+
it('handles modal state correctly', () => {
|
|
180
|
+
let contextValue
|
|
181
|
+
const TestComponent = () => {
|
|
182
|
+
contextValue = React.useContext(StoreLocatorContext)
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
render(
|
|
187
|
+
<TestWrapper>
|
|
188
|
+
<TestComponent />
|
|
189
|
+
</TestWrapper>
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// Initially modal should be closed
|
|
193
|
+
expect(contextValue?.isOpen).toBe(false)
|
|
194
|
+
|
|
195
|
+
// Open modal
|
|
196
|
+
act(() => {
|
|
197
|
+
contextValue?.onOpen()
|
|
198
|
+
})
|
|
199
|
+
expect(contextValue?.isOpen).toBe(true)
|
|
200
|
+
|
|
201
|
+
// Close modal
|
|
202
|
+
act(() => {
|
|
203
|
+
contextValue?.onClose()
|
|
204
|
+
})
|
|
205
|
+
expect(contextValue?.isOpen).toBe(false)
|
|
206
|
+
})
|
|
172
207
|
})
|
|
@@ -0,0 +1,155 @@
|
|
|
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 {useState, useCallback, useMemo} from 'react'
|
|
8
|
+
import {useForm} from 'react-hook-form'
|
|
9
|
+
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
|
|
10
|
+
import {useIntl} from 'react-intl'
|
|
11
|
+
import {useShopperCustomersMutation} from '@salesforce/commerce-sdk-react'
|
|
12
|
+
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
|
|
13
|
+
import {
|
|
14
|
+
areAddressesEqual,
|
|
15
|
+
sanitizedCustomerAddress
|
|
16
|
+
} from '@salesforce/retail-react-app/app/utils/address-utils'
|
|
17
|
+
import {nanoid} from 'nanoid'
|
|
18
|
+
|
|
19
|
+
export const useAddressForm = (
|
|
20
|
+
addGuestAddress,
|
|
21
|
+
isGuest,
|
|
22
|
+
setAddressesForItems,
|
|
23
|
+
availableAddresses,
|
|
24
|
+
deliveryItems
|
|
25
|
+
) => {
|
|
26
|
+
const {formatMessage} = useIntl()
|
|
27
|
+
const showToast = useToast()
|
|
28
|
+
const {data: customer, refetch: refetchCustomer} = useCurrentCustomer()
|
|
29
|
+
const [formStateByItemId, setFormStateByItemId] = useState({})
|
|
30
|
+
|
|
31
|
+
const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
|
|
32
|
+
|
|
33
|
+
const form = useForm({
|
|
34
|
+
mode: 'onSubmit',
|
|
35
|
+
defaultValues: {
|
|
36
|
+
firstName: '',
|
|
37
|
+
lastName: '',
|
|
38
|
+
phone: '',
|
|
39
|
+
countryCode: 'US',
|
|
40
|
+
address1: '',
|
|
41
|
+
city: '',
|
|
42
|
+
stateCode: '',
|
|
43
|
+
postalCode: '',
|
|
44
|
+
preferred: false
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const isAddressFormOpen = useMemo(() => {
|
|
49
|
+
return Object.keys(formStateByItemId).filter((key) => formStateByItemId[key])?.length > 0
|
|
50
|
+
}, [formStateByItemId])
|
|
51
|
+
|
|
52
|
+
const handleCreateAddress = useCallback(
|
|
53
|
+
async (addressData, itemId) => {
|
|
54
|
+
try {
|
|
55
|
+
const isDuplicate = availableAddresses.some((existingAddr) =>
|
|
56
|
+
areAddressesEqual(addressData, existingAddr)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (isDuplicate) {
|
|
60
|
+
showToast({
|
|
61
|
+
title: formatMessage({
|
|
62
|
+
id: 'shipping_multi_address.error.duplicate_address',
|
|
63
|
+
defaultMessage: 'The address you entered already exists.'
|
|
64
|
+
}),
|
|
65
|
+
status: 'info'
|
|
66
|
+
})
|
|
67
|
+
setFormStateByItemId((prev) => ({...prev, [itemId]: false}))
|
|
68
|
+
form.reset()
|
|
69
|
+
form.clearErrors()
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let newAddress
|
|
74
|
+
|
|
75
|
+
if (isGuest) {
|
|
76
|
+
newAddress = addGuestAddress(addressData)
|
|
77
|
+
} else {
|
|
78
|
+
const apiAddressData = {
|
|
79
|
+
...sanitizedCustomerAddress(addressData),
|
|
80
|
+
addressId: `addr_${nanoid()}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const createdAddress = await createCustomerAddress.mutateAsync({
|
|
84
|
+
body: apiAddressData,
|
|
85
|
+
parameters: {customerId: customer.customerId}
|
|
86
|
+
})
|
|
87
|
+
await refetchCustomer()
|
|
88
|
+
newAddress = createdAddress
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
showToast({
|
|
92
|
+
title: formatMessage({
|
|
93
|
+
id: 'shipping_multi_address.success.address_saved',
|
|
94
|
+
defaultMessage: 'Address saved successfully'
|
|
95
|
+
}),
|
|
96
|
+
status: 'success'
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Assign the address to items
|
|
100
|
+
if (availableAddresses.length === 0) {
|
|
101
|
+
// If first address, apply it to all items
|
|
102
|
+
const itemIds = deliveryItems.map((item) => item.itemId)
|
|
103
|
+
setAddressesForItems(itemIds, newAddress.addressId)
|
|
104
|
+
} else {
|
|
105
|
+
setAddressesForItems(itemId, newAddress.addressId)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setFormStateByItemId((prev) => ({...prev, [itemId]: false}))
|
|
109
|
+
form.reset()
|
|
110
|
+
form.clearErrors()
|
|
111
|
+
|
|
112
|
+
return newAddress
|
|
113
|
+
} catch (error) {
|
|
114
|
+
showToast({
|
|
115
|
+
title: formatMessage({
|
|
116
|
+
id: 'shipping_multi_address.error.save_failed',
|
|
117
|
+
defaultMessage: "Couldn't save the address."
|
|
118
|
+
}),
|
|
119
|
+
status: 'error'
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[
|
|
124
|
+
isGuest,
|
|
125
|
+
addGuestAddress,
|
|
126
|
+
customer?.customerId,
|
|
127
|
+
setAddressesForItems,
|
|
128
|
+
availableAddresses,
|
|
129
|
+
deliveryItems
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const openForm = useCallback((itemId) => {
|
|
134
|
+
setFormStateByItemId((prev) => ({...prev, [itemId]: true}))
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
const closeForm = useCallback(
|
|
138
|
+
(itemId) => {
|
|
139
|
+
setFormStateByItemId((prev) => ({...prev, [itemId]: false}))
|
|
140
|
+
form.clearErrors()
|
|
141
|
+
},
|
|
142
|
+
[form]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
form,
|
|
147
|
+
formStateByItemId,
|
|
148
|
+
isSubmitting: form.formState.isSubmitting,
|
|
149
|
+
openForm,
|
|
150
|
+
closeForm,
|
|
151
|
+
handleCreateAddress,
|
|
152
|
+
isAddressFormOpen,
|
|
153
|
+
formErrors: form.formState.errors
|
|
154
|
+
}
|
|
155
|
+
}
|