@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/app/components/_app-config/index.jsx +12 -0
  3. package/app/components/confirmation-modal/index.jsx +11 -4
  4. package/app/components/item-variant/item-image.jsx +18 -0
  5. package/app/components/product-item/index.test.js +1 -1
  6. package/app/components/promo-code/index.jsx +25 -28
  7. package/app/components/unavailable-product-confirmation-modal/index.jsx +72 -0
  8. package/app/components/unavailable-product-confirmation-modal/index.test.js +362 -0
  9. package/app/constants.js +18 -0
  10. package/app/hooks/einstein-mock-data.js +157 -0
  11. package/app/hooks/use-einstein.js +3 -4
  12. package/app/hooks/use-einstein.test.js +19 -1
  13. package/app/pages/account/index.jsx +1 -1
  14. package/app/pages/account/order-detail.jsx +16 -14
  15. package/app/pages/account/profile.jsx +21 -30
  16. package/app/pages/account/wishlist/index.jsx +25 -1
  17. package/app/pages/cart/index.jsx +24 -6
  18. package/app/pages/checkout/index.jsx +55 -6
  19. package/app/pages/checkout/index.test.js +23 -7
  20. package/app/pages/checkout/partials/payment.jsx +2 -2
  21. package/app/pages/checkout/partials/shipping-options.jsx +1 -1
  22. package/app/pages/product-detail/index.jsx +1 -1
  23. package/app/pages/product-list/index.jsx +5 -2
  24. package/app/ssr.js +18 -1
  25. package/app/static/translations/compiled/en-GB.json +24 -0
  26. package/app/static/translations/compiled/en-US.json +24 -0
  27. package/app/static/translations/compiled/en-XA.json +56 -0
  28. package/app/utils/test-utils.js +1 -0
  29. package/app/utils/url.js +1 -4
  30. package/package.json +6 -6
  31. package/translations/en-GB.json +13 -0
  32. 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 = searchResults?.hits?.map((product) =>
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: true, // Needed by Reports and Dashboards to differentiate searches with results vs no results
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 productItemsMap = productItems.reduce(
39
- (map, item) => ({...map, [item.productId]: item}),
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: ids
42
+ ids: orderProductIds
47
43
  }
48
44
  },
49
45
  {
50
- enabled: !!ids && onClient
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
- const variants = products?.map((product) => {
55
- const productItem = productItemsMap[product.id]
56
+ const variants = productItems?.map((item) => {
57
+ const product = products?.[item.productId]
56
58
  return {
57
- ...productItem,
58
- ...product,
59
- price: productItem.price
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
- profileHeading?.focus()
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
- {profileHeadingText}
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
- profileHeading?.focus()
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
- passwordHeading?.focus()
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
- {passwordHeadingText}
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
- passwordHeading?.focus()
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/index'
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
  }
@@ -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/index'
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 {useUsid, useShopperOrdersMutation} from '@salesforce/commerce-sdk-react'
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
  }