@salesforce/retail-react-app 2.4.0-dev → 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 +4 -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/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/order-detail.jsx +16 -14
- package/app/pages/account/wishlist/index.jsx +24 -0
- package/app/pages/cart/index.jsx +23 -5
- package/app/pages/checkout/index.jsx +55 -6
- package/app/pages/checkout/index.test.js +23 -7
- package/app/pages/product-list/index.jsx +1 -1
- 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/package.json +7 -7
- 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(
|
|
@@ -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
|
|
|
@@ -22,6 +22,7 @@ import WishlistSecondaryButtonGroup from '@salesforce/retail-react-app/app/pages
|
|
|
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
|
@@ -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
|
}
|
|
@@ -65,7 +65,19 @@ beforeEach(() => {
|
|
|
65
65
|
global.server.use(
|
|
66
66
|
// mock product details
|
|
67
67
|
rest.get('*/products', (req, res, ctx) => {
|
|
68
|
-
return res(
|
|
68
|
+
return res(
|
|
69
|
+
ctx.json({
|
|
70
|
+
data: [
|
|
71
|
+
{
|
|
72
|
+
id: '701643070725M',
|
|
73
|
+
currency: 'GBP',
|
|
74
|
+
name: 'Long Sleeve Crew Neck',
|
|
75
|
+
pricePerUnit: 19.18,
|
|
76
|
+
price: 19.18
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
)
|
|
69
81
|
}),
|
|
70
82
|
// mock the available shipping methods
|
|
71
83
|
rest.get('*/shipments/me/shipping-methods', (req, res, ctx) => {
|
|
@@ -162,11 +174,13 @@ beforeEach(() => {
|
|
|
162
174
|
|
|
163
175
|
// mock place order
|
|
164
176
|
rest.post('*/orders', (req, res, ctx) => {
|
|
165
|
-
|
|
177
|
+
const response = {
|
|
178
|
+
...currentBasket,
|
|
166
179
|
...scapiOrderResponse,
|
|
167
|
-
customerInfo: {...scapiOrderResponse.customerInfo, email: 'customer@test.com'}
|
|
180
|
+
customerInfo: {...scapiOrderResponse.customerInfo, email: 'customer@test.com'},
|
|
181
|
+
status: 'created'
|
|
168
182
|
}
|
|
169
|
-
return res(ctx.json(
|
|
183
|
+
return res(ctx.json(response))
|
|
170
184
|
}),
|
|
171
185
|
|
|
172
186
|
rest.get('*/baskets', (req, res, ctx) => {
|
|
@@ -272,11 +286,13 @@ test('Can proceed through checkout steps as guest', async () => {
|
|
|
272
286
|
|
|
273
287
|
// mock place order
|
|
274
288
|
rest.post('*/orders', (req, res, ctx) => {
|
|
275
|
-
|
|
289
|
+
const response = {
|
|
290
|
+
...currentBasket,
|
|
276
291
|
...scapiOrderResponse,
|
|
277
|
-
customerInfo: {...scapiOrderResponse.customerInfo, email: '
|
|
292
|
+
customerInfo: {...scapiOrderResponse.customerInfo, email: 'customer@test.com'},
|
|
293
|
+
status: 'created'
|
|
278
294
|
}
|
|
279
|
-
return res(ctx.json(
|
|
295
|
+
return res(ctx.json(response))
|
|
280
296
|
}),
|
|
281
297
|
|
|
282
298
|
rest.get('*/baskets', (req, res, ctx) => {
|
package/app/ssr.js
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
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
|
+
/*
|
|
9
|
+
* Developer note! When updating this file, make sure to also update the
|
|
10
|
+
* ssr.js template files in pwa-kit-create-app.
|
|
11
|
+
*
|
|
12
|
+
* In the pwa-kit-create-app, the templates are found under:
|
|
13
|
+
* - assets/bootstrap/js/overrides/app/ssr.js.hbs
|
|
14
|
+
* - assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs
|
|
15
|
+
*/
|
|
16
|
+
|
|
8
17
|
'use strict'
|
|
9
18
|
|
|
10
19
|
import path from 'path'
|
|
@@ -29,7 +38,14 @@ const options = {
|
|
|
29
38
|
// The protocol on which the development Express app listens.
|
|
30
39
|
// Note that http://localhost is treated as a secure context for development,
|
|
31
40
|
// except by Safari.
|
|
32
|
-
protocol: 'http'
|
|
41
|
+
protocol: 'http',
|
|
42
|
+
|
|
43
|
+
// Option for whether to set up a special endpoint for handling
|
|
44
|
+
// private SLAS clients
|
|
45
|
+
// Set this to false if using a SLAS public client
|
|
46
|
+
// When setting this to true, make sure to also set the PWA_KIT_SLAS_CLIENT_SECRET
|
|
47
|
+
// environment variable as this endpoint will return HTTP 501 if it is not set
|
|
48
|
+
useSLASPrivateClient: false
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
const runtime = getRuntime()
|
|
@@ -67,6 +83,7 @@ const {handler} = runtime.createHandler(options, (app) => {
|
|
|
67
83
|
res.set('Cache-Control', `max-age=31536000`)
|
|
68
84
|
res.send()
|
|
69
85
|
})
|
|
86
|
+
|
|
70
87
|
app.get('/robots.txt', runtime.serveStaticFile('static/robots.txt'))
|
|
71
88
|
app.get('/favicon.ico', runtime.serveStaticFile('static/ico/favicon.ico'))
|
|
72
89
|
|
|
@@ -879,12 +879,24 @@
|
|
|
879
879
|
"value": "No, keep item"
|
|
880
880
|
}
|
|
881
881
|
],
|
|
882
|
+
"confirmation_modal.remove_cart_item.action.remove": [
|
|
883
|
+
{
|
|
884
|
+
"type": 0,
|
|
885
|
+
"value": "Remove"
|
|
886
|
+
}
|
|
887
|
+
],
|
|
882
888
|
"confirmation_modal.remove_cart_item.action.yes": [
|
|
883
889
|
{
|
|
884
890
|
"type": 0,
|
|
885
891
|
"value": "Yes, remove item"
|
|
886
892
|
}
|
|
887
893
|
],
|
|
894
|
+
"confirmation_modal.remove_cart_item.message.need_to_remove_due_to_unavailability": [
|
|
895
|
+
{
|
|
896
|
+
"type": 0,
|
|
897
|
+
"value": "Some items are no longer available online and will be removed from your cart."
|
|
898
|
+
}
|
|
899
|
+
],
|
|
888
900
|
"confirmation_modal.remove_cart_item.message.sure_to_remove": [
|
|
889
901
|
{
|
|
890
902
|
"type": 0,
|
|
@@ -897,6 +909,12 @@
|
|
|
897
909
|
"value": "Confirm Remove Item"
|
|
898
910
|
}
|
|
899
911
|
],
|
|
912
|
+
"confirmation_modal.remove_cart_item.title.items_unavailable": [
|
|
913
|
+
{
|
|
914
|
+
"type": 0,
|
|
915
|
+
"value": "Items Unavailable"
|
|
916
|
+
}
|
|
917
|
+
],
|
|
900
918
|
"confirmation_modal.remove_wishlist_item.action.no": [
|
|
901
919
|
{
|
|
902
920
|
"type": 0,
|
|
@@ -1637,6 +1655,12 @@
|
|
|
1637
1655
|
"value": "Sale"
|
|
1638
1656
|
}
|
|
1639
1657
|
],
|
|
1658
|
+
"item_image.label.unavailable": [
|
|
1659
|
+
{
|
|
1660
|
+
"type": 0,
|
|
1661
|
+
"value": "Unavailable"
|
|
1662
|
+
}
|
|
1663
|
+
],
|
|
1640
1664
|
"item_price.label.starting_at": [
|
|
1641
1665
|
{
|
|
1642
1666
|
"type": 0,
|