@salesforce/retail-react-app 2.3.1 → 2.4.0-dev.1
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 +15 -0
- package/app/components/_app-config/index.jsx +12 -0
- package/app/components/confirmation-modal/index.jsx +11 -4
- package/app/components/item-variant/item-image.jsx +18 -0
- package/app/components/product-item/index.test.js +1 -1
- package/app/components/promo-code/index.jsx +25 -28
- package/app/components/unavailable-product-confirmation-modal/index.jsx +72 -0
- package/app/components/unavailable-product-confirmation-modal/index.test.js +362 -0
- package/app/constants.js +18 -0
- package/app/hooks/einstein-mock-data.js +157 -0
- package/app/hooks/use-einstein.js +3 -4
- package/app/hooks/use-einstein.test.js +19 -1
- package/app/pages/account/index.jsx +1 -1
- package/app/pages/account/order-detail.jsx +16 -14
- package/app/pages/account/profile.jsx +21 -30
- package/app/pages/account/wishlist/index.jsx +25 -1
- package/app/pages/cart/index.jsx +24 -6
- package/app/pages/checkout/index.jsx +55 -6
- package/app/pages/checkout/index.test.js +23 -7
- package/app/pages/checkout/partials/payment.jsx +2 -2
- package/app/pages/checkout/partials/shipping-options.jsx +1 -1
- package/app/pages/product-detail/index.jsx +1 -1
- package/app/pages/product-list/index.jsx +5 -2
- package/app/ssr.js +18 -1
- package/app/static/translations/compiled/en-GB.json +24 -0
- package/app/static/translations/compiled/en-US.json +24 -0
- package/app/static/translations/compiled/en-XA.json +56 -0
- package/app/utils/test-utils.js +1 -0
- package/app/utils/url.js +1 -4
- package/package.json +6 -6
- package/translations/en-GB.json +13 -0
- package/translations/en-US.json +13 -0
|
@@ -429,6 +429,163 @@ export const mockSearchResults = {
|
|
|
429
429
|
total: 4
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
export const mockNoSearchResults = {
|
|
433
|
+
limit: 0,
|
|
434
|
+
query: 'dsflksajfdklsafj',
|
|
435
|
+
refinements: [
|
|
436
|
+
{
|
|
437
|
+
attributeId: 'cgid',
|
|
438
|
+
label: 'Category'
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
attributeId: 'c_refinementColor',
|
|
442
|
+
label: 'Colour',
|
|
443
|
+
values: [
|
|
444
|
+
{
|
|
445
|
+
hitCount: 0,
|
|
446
|
+
label: 'Beige',
|
|
447
|
+
presentationId: 'beige',
|
|
448
|
+
value: 'Beige'
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
hitCount: 0,
|
|
452
|
+
label: 'Black',
|
|
453
|
+
presentationId: 'black',
|
|
454
|
+
value: 'Black'
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
hitCount: 0,
|
|
458
|
+
label: 'Blue',
|
|
459
|
+
presentationId: 'blue',
|
|
460
|
+
value: 'Blue'
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
hitCount: 0,
|
|
464
|
+
label: 'Navy',
|
|
465
|
+
presentationId: 'navy',
|
|
466
|
+
value: 'Navy'
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
hitCount: 0,
|
|
470
|
+
label: 'Brown',
|
|
471
|
+
presentationId: 'brown',
|
|
472
|
+
value: 'Brown'
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
hitCount: 0,
|
|
476
|
+
label: 'Green',
|
|
477
|
+
presentationId: 'green',
|
|
478
|
+
value: 'Green'
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
hitCount: 0,
|
|
482
|
+
label: 'Grey',
|
|
483
|
+
presentationId: 'grey',
|
|
484
|
+
value: 'Grey'
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
hitCount: 0,
|
|
488
|
+
label: 'Orange',
|
|
489
|
+
presentationId: 'orange',
|
|
490
|
+
value: 'Orange'
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
hitCount: 0,
|
|
494
|
+
label: 'Pink',
|
|
495
|
+
presentationId: 'pink',
|
|
496
|
+
value: 'Pink'
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
hitCount: 0,
|
|
500
|
+
label: 'Purple',
|
|
501
|
+
presentationId: 'purple',
|
|
502
|
+
value: 'Purple'
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
hitCount: 0,
|
|
506
|
+
label: 'Red',
|
|
507
|
+
presentationId: 'red',
|
|
508
|
+
value: 'Red'
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
hitCount: 0,
|
|
512
|
+
label: 'White',
|
|
513
|
+
presentationId: 'white',
|
|
514
|
+
value: 'White'
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
hitCount: 0,
|
|
518
|
+
label: 'Yellow',
|
|
519
|
+
presentationId: 'yellow',
|
|
520
|
+
value: 'Yellow'
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
hitCount: 0,
|
|
524
|
+
label: 'Miscellaneous',
|
|
525
|
+
presentationId: 'miscellaneous',
|
|
526
|
+
value: 'Miscellaneous'
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
attributeId: 'price',
|
|
532
|
+
label: 'Price'
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
attributeId: 'c_isNew',
|
|
536
|
+
label: 'New Arrival'
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
attributeId: 'brand',
|
|
540
|
+
label: 'By brand'
|
|
541
|
+
}
|
|
542
|
+
],
|
|
543
|
+
searchPhraseSuggestions: {
|
|
544
|
+
suggestedTerms: [
|
|
545
|
+
{
|
|
546
|
+
originalTerm: 'dsflksajfdklsafj'
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
selectedSortingOption: 'best-matches',
|
|
551
|
+
sortingOptions: [
|
|
552
|
+
{
|
|
553
|
+
id: 'best-matches',
|
|
554
|
+
label: 'Best Matches'
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: 'price-low-to-high',
|
|
558
|
+
label: 'Price Low To High'
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: 'price-high-to-low',
|
|
562
|
+
label: 'Price High to Low'
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: 'product-name-ascending',
|
|
566
|
+
label: 'Product Name A - Z'
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: 'product-name-descending',
|
|
570
|
+
label: 'Product Name Z - A'
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: 'brand',
|
|
574
|
+
label: 'Brand'
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
id: 'most-popular',
|
|
578
|
+
label: 'Most Popular'
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: 'top-sellers',
|
|
582
|
+
label: 'Top Sellers'
|
|
583
|
+
}
|
|
584
|
+
],
|
|
585
|
+
offset: 0,
|
|
586
|
+
total: 0
|
|
587
|
+
}
|
|
588
|
+
|
|
432
589
|
export const mockBasket = {
|
|
433
590
|
adjustedMerchandizeTotalTax: 1.5,
|
|
434
591
|
adjustedShippingTotalTax: 0.3,
|
|
@@ -161,14 +161,13 @@ export class EinsteinAPI {
|
|
|
161
161
|
const endpoint = `/activities/${this.siteId}/viewSearch`
|
|
162
162
|
const method = 'POST'
|
|
163
163
|
|
|
164
|
-
const products =
|
|
165
|
-
this._constructEinsteinProduct(product)
|
|
166
|
-
)
|
|
164
|
+
const products =
|
|
165
|
+
searchResults?.hits?.map((product) => this._constructEinsteinProduct(product)) ?? []
|
|
167
166
|
|
|
168
167
|
const body = {
|
|
169
168
|
searchText,
|
|
170
169
|
products,
|
|
171
|
-
showProducts:
|
|
170
|
+
showProducts: Boolean(products.length), // Needed by Reports and Dashboards to differentiate searches with results vs no results
|
|
172
171
|
...args
|
|
173
172
|
}
|
|
174
173
|
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
mockCategory,
|
|
12
12
|
mockSearchResults,
|
|
13
13
|
mockBasket,
|
|
14
|
-
mockRecommenderDetails
|
|
14
|
+
mockRecommenderDetails,
|
|
15
|
+
mockNoSearchResults
|
|
15
16
|
} from '@salesforce/retail-react-app/app/hooks/einstein-mock-data'
|
|
16
17
|
import fetchMock from 'jest-fetch-mock'
|
|
17
18
|
|
|
@@ -65,6 +66,23 @@ describe('EinsteinAPI', () => {
|
|
|
65
66
|
)
|
|
66
67
|
})
|
|
67
68
|
|
|
69
|
+
test('viewSearch: no search results', async () => {
|
|
70
|
+
const searchTerm = 'dsflksajfdklsafj'
|
|
71
|
+
await einsteinApi.sendViewSearch(searchTerm, mockNoSearchResults, {cookieId: 'test-usid'})
|
|
72
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
73
|
+
'http://localhost/test-path/v3/activities/test-site-id/viewSearch',
|
|
74
|
+
{
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'x-cq-client-id': 'test-id'
|
|
79
|
+
},
|
|
80
|
+
// Most importantly, the body should contain `products=[]` and `showProducts=false`
|
|
81
|
+
body: '{"searchText":"dsflksajfdklsafj","products":[],"showProducts":false,"cookieId":"test-usid","realm":"test","instanceType":"sbx"}'
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
68
86
|
test('viewCategory sends expected api request', async () => {
|
|
69
87
|
await einsteinApi.sendViewCategory(mockCategory, mockSearchResults, {cookieId: 'test-usid'})
|
|
70
88
|
expect(fetch).toHaveBeenCalledWith(
|
|
@@ -110,11 +110,11 @@ const Account = () => {
|
|
|
110
110
|
// If we have customer data and they are not registered, push to login page
|
|
111
111
|
// Using Redirect allows us to store the directed page to location
|
|
112
112
|
// so we can direct users back after they are successfully log in
|
|
113
|
-
// we don't want redirect on server side
|
|
114
113
|
if (customerType !== null && !isRegistered && onClient) {
|
|
115
114
|
const path = buildUrl('/login')
|
|
116
115
|
return <Redirect to={{pathname: path, state: {directedFrom: '/account'}}} />
|
|
117
116
|
}
|
|
117
|
+
|
|
118
118
|
return (
|
|
119
119
|
<Box
|
|
120
120
|
data-testid={isRegistered && isHydrated() ? 'account-page' : 'account-page-skeleton'}
|
|
@@ -35,28 +35,30 @@ import PropTypes from 'prop-types'
|
|
|
35
35
|
const onClient = typeof window !== 'undefined'
|
|
36
36
|
|
|
37
37
|
const OrderProducts = ({productItems, currency}) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
{}
|
|
41
|
-
)
|
|
42
|
-
const ids = Object.keys(productItemsMap).join(',') ?? ''
|
|
43
|
-
const {data: {data: products} = {}, isLoading} = useProducts(
|
|
38
|
+
const orderProductIds = productItems.map((product) => product.productId)
|
|
39
|
+
const {data: products, isLoading} = useProducts(
|
|
44
40
|
{
|
|
45
41
|
parameters: {
|
|
46
|
-
ids:
|
|
42
|
+
ids: orderProductIds
|
|
47
43
|
}
|
|
48
44
|
},
|
|
49
45
|
{
|
|
50
|
-
enabled: !!
|
|
46
|
+
enabled: !!orderProductIds && onClient,
|
|
47
|
+
select: (result) => {
|
|
48
|
+
return result?.data?.reduce((result, item) => {
|
|
49
|
+
const key = item.id
|
|
50
|
+
result[key] = item
|
|
51
|
+
return result
|
|
52
|
+
}, {})
|
|
53
|
+
}
|
|
51
54
|
}
|
|
52
55
|
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const productItem = productItemsMap[product.id]
|
|
56
|
+
const variants = productItems?.map((item) => {
|
|
57
|
+
const product = products?.[item.productId]
|
|
56
58
|
return {
|
|
57
|
-
...
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
...(product ? product : {}),
|
|
60
|
+
isProductUnavailable: !product,
|
|
61
|
+
...item
|
|
60
62
|
}
|
|
61
63
|
})
|
|
62
64
|
|
|
@@ -5,7 +5,7 @@
|
|
|
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
7
|
|
|
8
|
-
import React, {useEffect, useRef, useState} from 'react'
|
|
8
|
+
import React, {forwardRef, useEffect, useRef, useState} from 'react'
|
|
9
9
|
import {FormattedMessage, useIntl} from 'react-intl'
|
|
10
10
|
import {
|
|
11
11
|
Alert,
|
|
@@ -42,7 +42,7 @@ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-cur
|
|
|
42
42
|
* the bounding element will affect the contents size.
|
|
43
43
|
*/
|
|
44
44
|
// eslint-disable-next-line react/prop-types
|
|
45
|
-
const Skeleton = ({children, height, width, ...rest}) => {
|
|
45
|
+
const Skeleton = forwardRef(({children, height, width, ...rest}, ref) => {
|
|
46
46
|
const {data: customer} = useCurrentCustomer()
|
|
47
47
|
const {isRegistered} = customer
|
|
48
48
|
const size = !isRegistered
|
|
@@ -52,15 +52,17 @@ const Skeleton = ({children, height, width, ...rest}) => {
|
|
|
52
52
|
}
|
|
53
53
|
: {}
|
|
54
54
|
return (
|
|
55
|
-
<ChakraSkeleton isLoaded={!customer.isLoading} {...rest} {...size}>
|
|
55
|
+
<ChakraSkeleton ref={ref} isLoaded={!customer.isLoading} {...rest} {...size}>
|
|
56
56
|
{children}
|
|
57
57
|
</ChakraSkeleton>
|
|
58
58
|
)
|
|
59
|
-
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
Skeleton.displayName = 'Skeleton'
|
|
60
62
|
|
|
61
63
|
const ProfileCard = () => {
|
|
62
64
|
const {formatMessage} = useIntl()
|
|
63
|
-
|
|
65
|
+
const headingRef = useRef(null)
|
|
64
66
|
const {data: customer} = useCurrentCustomer()
|
|
65
67
|
const {isRegistered, customerId} = customer
|
|
66
68
|
|
|
@@ -87,14 +89,6 @@ const ProfileCard = () => {
|
|
|
87
89
|
})
|
|
88
90
|
}, [customer?.firstName, customer?.lastName, customer?.email, customer?.phoneHome])
|
|
89
91
|
|
|
90
|
-
const profileHeadingText = formatMessage({
|
|
91
|
-
defaultMessage: 'My Profile',
|
|
92
|
-
id: 'profile_card.title.my_profile'
|
|
93
|
-
})
|
|
94
|
-
const profileHeading = Array.from(document.querySelectorAll('h2')).find(
|
|
95
|
-
(element) => element.textContent === profileHeadingText
|
|
96
|
-
)
|
|
97
|
-
|
|
98
92
|
const submit = async (values) => {
|
|
99
93
|
try {
|
|
100
94
|
form.clearErrors()
|
|
@@ -126,7 +120,7 @@ const ProfileCard = () => {
|
|
|
126
120
|
status: 'success',
|
|
127
121
|
isClosable: true
|
|
128
122
|
})
|
|
129
|
-
|
|
123
|
+
headingRef?.current?.focus()
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
126
|
)
|
|
@@ -139,8 +133,11 @@ const ProfileCard = () => {
|
|
|
139
133
|
<ToggleCard
|
|
140
134
|
id="my-profile"
|
|
141
135
|
title={
|
|
142
|
-
<Skeleton height="30px" width="120px">
|
|
143
|
-
|
|
136
|
+
<Skeleton ref={headingRef} tabIndex="-1" height="30px" width="120px">
|
|
137
|
+
<FormattedMessage
|
|
138
|
+
defaultMessage="My Profile"
|
|
139
|
+
id="profile_card.title.my_profile"
|
|
140
|
+
/>
|
|
144
141
|
</Skeleton>
|
|
145
142
|
}
|
|
146
143
|
editing={isEditing}
|
|
@@ -164,7 +161,8 @@ const ProfileCard = () => {
|
|
|
164
161
|
<FormActionButtons
|
|
165
162
|
onCancel={() => {
|
|
166
163
|
setIsEditing(false)
|
|
167
|
-
|
|
164
|
+
headingRef?.current?.focus()
|
|
165
|
+
form.reset()
|
|
168
166
|
}}
|
|
169
167
|
/>
|
|
170
168
|
</Stack>
|
|
@@ -232,7 +230,7 @@ const ProfileCard = () => {
|
|
|
232
230
|
|
|
233
231
|
const PasswordCard = () => {
|
|
234
232
|
const {formatMessage} = useIntl()
|
|
235
|
-
|
|
233
|
+
const headingRef = useRef(null)
|
|
236
234
|
const {data: customer} = useCurrentCustomer()
|
|
237
235
|
const {isRegistered, customerId, email} = customer
|
|
238
236
|
|
|
@@ -244,14 +242,6 @@ const PasswordCard = () => {
|
|
|
244
242
|
|
|
245
243
|
const form = useForm()
|
|
246
244
|
|
|
247
|
-
const passwordHeadingText = formatMessage({
|
|
248
|
-
defaultMessage: 'Password',
|
|
249
|
-
id: 'password_card.title.password'
|
|
250
|
-
})
|
|
251
|
-
const passwordHeading = Array.from(document.querySelectorAll('h2')).find(
|
|
252
|
-
(element) => element.textContent === passwordHeadingText
|
|
253
|
-
)
|
|
254
|
-
|
|
255
245
|
const submit = async (values) => {
|
|
256
246
|
try {
|
|
257
247
|
form.clearErrors()
|
|
@@ -278,7 +268,7 @@ const PasswordCard = () => {
|
|
|
278
268
|
username: email,
|
|
279
269
|
password: values.password
|
|
280
270
|
})
|
|
281
|
-
|
|
271
|
+
headingRef?.current?.focus()
|
|
282
272
|
form.reset()
|
|
283
273
|
},
|
|
284
274
|
onError: async (err) => {
|
|
@@ -296,8 +286,8 @@ const PasswordCard = () => {
|
|
|
296
286
|
<ToggleCard
|
|
297
287
|
id="password"
|
|
298
288
|
title={
|
|
299
|
-
<Skeleton height="30px" width="120px">
|
|
300
|
-
|
|
289
|
+
<Skeleton ref={headingRef} tabIndex="-1" height="30px" width="120px">
|
|
290
|
+
<FormattedMessage defaultMessage="Password" id="password_card.title.password" />
|
|
301
291
|
</Skeleton>
|
|
302
292
|
}
|
|
303
293
|
editing={isEditing}
|
|
@@ -321,7 +311,8 @@ const PasswordCard = () => {
|
|
|
321
311
|
<FormActionButtons
|
|
322
312
|
onCancel={() => {
|
|
323
313
|
setIsEditing(false)
|
|
324
|
-
|
|
314
|
+
headingRef?.current?.focus()
|
|
315
|
+
form.reset()
|
|
325
316
|
}}
|
|
326
317
|
/>
|
|
327
318
|
</Stack>
|
|
@@ -16,12 +16,13 @@ import {useWishList} from '@salesforce/retail-react-app/app/hooks/use-wish-list'
|
|
|
16
16
|
|
|
17
17
|
import PageActionPlaceHolder from '@salesforce/retail-react-app/app/components/page-action-placeholder'
|
|
18
18
|
import {HeartIcon} from '@salesforce/retail-react-app/app/components/icons'
|
|
19
|
-
import ProductItem from '@salesforce/retail-react-app/app/components/product-item
|
|
19
|
+
import ProductItem from '@salesforce/retail-react-app/app/components/product-item'
|
|
20
20
|
import WishlistPrimaryAction from '@salesforce/retail-react-app/app/pages/account/wishlist/partials/wishlist-primary-action'
|
|
21
21
|
import WishlistSecondaryButtonGroup from '@salesforce/retail-react-app/app/pages/account/wishlist/partials/wishlist-secondary-button-group'
|
|
22
22
|
|
|
23
23
|
import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'
|
|
24
24
|
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
|
|
25
|
+
import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal'
|
|
25
26
|
|
|
26
27
|
const numberOfSkeletonItems = 3
|
|
27
28
|
|
|
@@ -60,6 +61,7 @@ const AccountWishlist = () => {
|
|
|
60
61
|
const deleteCustomerProductListItem = useShopperCustomersMutation(
|
|
61
62
|
'deleteCustomerProductListItem'
|
|
62
63
|
)
|
|
64
|
+
|
|
63
65
|
const {data: customer} = useCurrentCustomer()
|
|
64
66
|
|
|
65
67
|
const handleSecondaryAction = async (itemId, promise) => {
|
|
@@ -75,6 +77,23 @@ const AccountWishlist = () => {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
const handleUnavailableProducts = async (unavailableProductIds) => {
|
|
81
|
+
if (!unavailableProductIds.length) return
|
|
82
|
+
await Promise.all(
|
|
83
|
+
unavailableProductIds.map(async (id) => {
|
|
84
|
+
const item = wishListItems?.find((item) => {
|
|
85
|
+
return item.productId.toString() === id.toString()
|
|
86
|
+
})
|
|
87
|
+
const parameters = {
|
|
88
|
+
customerId: customer.customerId,
|
|
89
|
+
itemId: item?.id,
|
|
90
|
+
listId: wishListData?.id
|
|
91
|
+
}
|
|
92
|
+
await deleteCustomerProductListItem.mutateAsync({parameters})
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
const handleItemQuantityChanged = async (quantity, item) => {
|
|
79
98
|
let isValidChange = false
|
|
80
99
|
setSelectedItem(item.productId)
|
|
@@ -197,6 +216,11 @@ const AccountWishlist = () => {
|
|
|
197
216
|
}
|
|
198
217
|
/>
|
|
199
218
|
))}
|
|
219
|
+
|
|
220
|
+
<UnavailableProductConfirmationModal
|
|
221
|
+
productIds={productIds}
|
|
222
|
+
handleUnavailableProducts={handleUnavailableProducts}
|
|
223
|
+
/>
|
|
200
224
|
</Stack>
|
|
201
225
|
)
|
|
202
226
|
}
|
package/app/pages/cart/index.jsx
CHANGED
|
@@ -26,7 +26,7 @@ import CartTitle from '@salesforce/retail-react-app/app/pages/cart/partials/cart
|
|
|
26
26
|
import ConfirmationModal from '@salesforce/retail-react-app/app/components/confirmation-modal'
|
|
27
27
|
import EmptyCart from '@salesforce/retail-react-app/app/pages/cart/partials/empty-cart'
|
|
28
28
|
import OrderSummary from '@salesforce/retail-react-app/app/components/order-summary'
|
|
29
|
-
import ProductItem from '@salesforce/retail-react-app/app/components/product-item
|
|
29
|
+
import ProductItem from '@salesforce/retail-react-app/app/components/product-item'
|
|
30
30
|
import ProductViewModal from '@salesforce/retail-react-app/app/components/product-view-modal'
|
|
31
31
|
import RecommendedProducts from '@salesforce/retail-react-app/app/components/recommended-products'
|
|
32
32
|
|
|
@@ -56,13 +56,13 @@ import {
|
|
|
56
56
|
useShopperCustomersMutation
|
|
57
57
|
} from '@salesforce/commerce-sdk-react'
|
|
58
58
|
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
|
|
59
|
+
import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal'
|
|
59
60
|
|
|
60
61
|
const DEBOUNCE_WAIT = 750
|
|
61
62
|
const Cart = () => {
|
|
62
63
|
const {data: basket, isLoading} = useCurrentBasket()
|
|
63
|
-
|
|
64
64
|
const productIds = basket?.productItems?.map(({productId}) => productId).join(',') ?? ''
|
|
65
|
-
const {data: products} = useProducts(
|
|
65
|
+
const {data: products, isLoading: isProductsLoading} = useProducts(
|
|
66
66
|
{
|
|
67
67
|
parameters: {
|
|
68
68
|
ids: productIds,
|
|
@@ -72,7 +72,6 @@ const Cart = () => {
|
|
|
72
72
|
{
|
|
73
73
|
enabled: Boolean(productIds),
|
|
74
74
|
select: (result) => {
|
|
75
|
-
// Convert array into key/value object with key is the product id
|
|
76
75
|
return result?.data?.reduce((result, item) => {
|
|
77
76
|
const key = item.id
|
|
78
77
|
result[key] = item
|
|
@@ -81,9 +80,9 @@ const Cart = () => {
|
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
)
|
|
83
|
+
|
|
84
84
|
const {data: customer} = useCurrentCustomer()
|
|
85
85
|
const {customerId, isRegistered} = customer
|
|
86
|
-
|
|
87
86
|
/*****************Basket Mutation************************/
|
|
88
87
|
const updateItemInBasketMutation = useShopperBasketsMutation('updateItemInBasket')
|
|
89
88
|
const removeItemFromBasketMutation = useShopperBasketsMutation('removeItemFromBasket')
|
|
@@ -298,6 +297,18 @@ const Cart = () => {
|
|
|
298
297
|
setSelectedItem(undefined)
|
|
299
298
|
}
|
|
300
299
|
}
|
|
300
|
+
|
|
301
|
+
const handleUnavailableProducts = async (unavailableProductIds) => {
|
|
302
|
+
const productItems = basket?.productItems?.filter((item) =>
|
|
303
|
+
unavailableProductIds?.includes(item.productId)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
await Promise.all(
|
|
307
|
+
productItems.map(async (item) => {
|
|
308
|
+
await handleRemoveItem(item)
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
}
|
|
301
312
|
/***************************** Update Cart **************************/
|
|
302
313
|
|
|
303
314
|
/***************************** Update quantity **************************/
|
|
@@ -448,6 +459,9 @@ const Cart = () => {
|
|
|
448
459
|
...productItem,
|
|
449
460
|
...(products &&
|
|
450
461
|
products[productItem.productId]),
|
|
462
|
+
isProductUnavailable: !isProductsLoading
|
|
463
|
+
? !products?.[productItem.productId]
|
|
464
|
+
: undefined,
|
|
451
465
|
price: productItem.price,
|
|
452
466
|
quantity: localQuantity[productItem.itemId]
|
|
453
467
|
? localQuantity[productItem.itemId]
|
|
@@ -536,7 +550,6 @@ const Cart = () => {
|
|
|
536
550
|
>
|
|
537
551
|
<CartCta />
|
|
538
552
|
</Box>
|
|
539
|
-
|
|
540
553
|
<ConfirmationModal
|
|
541
554
|
{...REMOVE_CART_ITEM_CONFIRMATION_DIALOG_CONFIG}
|
|
542
555
|
onPrimaryAction={() => {
|
|
@@ -545,6 +558,11 @@ const Cart = () => {
|
|
|
545
558
|
onAlternateAction={() => {}}
|
|
546
559
|
{...modalProps}
|
|
547
560
|
/>
|
|
561
|
+
|
|
562
|
+
<UnavailableProductConfirmationModal
|
|
563
|
+
productIds={productIds.split(',')}
|
|
564
|
+
handleUnavailableProducts={handleUnavailableProducts}
|
|
565
|
+
/>
|
|
548
566
|
</Box>
|
|
549
567
|
)
|
|
550
568
|
}
|
|
@@ -29,7 +29,18 @@ import OrderSummary from '@salesforce/retail-react-app/app/components/order-summ
|
|
|
29
29
|
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
|
|
30
30
|
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
|
|
31
31
|
import CheckoutSkeleton from '@salesforce/retail-react-app/app/pages/checkout/partials/checkout-skeleton'
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
useUsid,
|
|
34
|
+
useShopperOrdersMutation,
|
|
35
|
+
useShopperBasketsMutation
|
|
36
|
+
} from '@salesforce/commerce-sdk-react'
|
|
37
|
+
import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal'
|
|
38
|
+
import {
|
|
39
|
+
API_ERROR_MESSAGE,
|
|
40
|
+
TOAST_MESSAGE_REMOVED_ITEM_FROM_CART
|
|
41
|
+
} from '@salesforce/retail-react-app/app/constants'
|
|
42
|
+
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
|
|
43
|
+
import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
|
|
33
44
|
|
|
34
45
|
const Checkout = () => {
|
|
35
46
|
const {formatMessage} = useIntl()
|
|
@@ -51,11 +62,6 @@ const Checkout = () => {
|
|
|
51
62
|
setIsLoading(true)
|
|
52
63
|
try {
|
|
53
64
|
const order = await createOrder({
|
|
54
|
-
// We send the SLAS usid via this header. This is required by ECOM to map
|
|
55
|
-
// Einstein events sent via the API with the finishOrder event fired by ECOM
|
|
56
|
-
// when an Order transitions from Created to New status.
|
|
57
|
-
// Without this, various order conversion metrics will not appear on reports and dashboards
|
|
58
|
-
headers: {_sfdc_customer_id: usid},
|
|
59
65
|
body: {basketId: basket.basketId}
|
|
60
66
|
})
|
|
61
67
|
navigate(`/checkout/confirmation/${order.orderNo}`)
|
|
@@ -163,6 +169,43 @@ const Checkout = () => {
|
|
|
163
169
|
const CheckoutContainer = () => {
|
|
164
170
|
const {data: customer} = useCurrentCustomer()
|
|
165
171
|
const {data: basket} = useCurrentBasket()
|
|
172
|
+
const {formatMessage} = useIntl()
|
|
173
|
+
const productIds = basket?.productItems?.map(({productId}) => productId) ?? []
|
|
174
|
+
const removeItemFromBasketMutation = useShopperBasketsMutation('removeItemFromBasket')
|
|
175
|
+
const toast = useToast()
|
|
176
|
+
const [isDeletingUnavailableItem, setIsDeletingUnavailableItem] = useState(false)
|
|
177
|
+
|
|
178
|
+
const handleRemoveItem = async (product) => {
|
|
179
|
+
await removeItemFromBasketMutation.mutateAsync(
|
|
180
|
+
{
|
|
181
|
+
parameters: {basketId: basket.basketId, itemId: product.itemId}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
onSuccess: () => {
|
|
185
|
+
toast({
|
|
186
|
+
title: formatMessage(TOAST_MESSAGE_REMOVED_ITEM_FROM_CART, {quantity: 1}),
|
|
187
|
+
status: 'success'
|
|
188
|
+
})
|
|
189
|
+
},
|
|
190
|
+
onError: () => {
|
|
191
|
+
toast({
|
|
192
|
+
title: formatMessage(API_ERROR_MESSAGE),
|
|
193
|
+
status: 'error'
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
const handleUnavailableProducts = async (unavailableProductIds) => {
|
|
200
|
+
setIsDeletingUnavailableItem(true)
|
|
201
|
+
const productItems = basket?.productItems?.filter((item) =>
|
|
202
|
+
unavailableProductIds?.includes(item.productId)
|
|
203
|
+
)
|
|
204
|
+
for (let item of productItems) {
|
|
205
|
+
await handleRemoveItem(item)
|
|
206
|
+
}
|
|
207
|
+
setIsDeletingUnavailableItem(false)
|
|
208
|
+
}
|
|
166
209
|
|
|
167
210
|
if (!customer || !customer.customerId || !basket || !basket.basketId) {
|
|
168
211
|
return <CheckoutSkeleton />
|
|
@@ -170,7 +213,13 @@ const CheckoutContainer = () => {
|
|
|
170
213
|
|
|
171
214
|
return (
|
|
172
215
|
<CheckoutProvider>
|
|
216
|
+
{isDeletingUnavailableItem && <LoadingSpinner wrapperStyles={{height: '100vh'}} />}
|
|
217
|
+
|
|
173
218
|
<Checkout />
|
|
219
|
+
<UnavailableProductConfirmationModal
|
|
220
|
+
productIds={productIds}
|
|
221
|
+
handleUnavailableProducts={handleUnavailableProducts}
|
|
222
|
+
/>
|
|
174
223
|
</CheckoutProvider>
|
|
175
224
|
)
|
|
176
225
|
}
|