@salesforce/retail-react-app 8.1.0-preview.1 → 8.1.0-preview.3

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 (62) hide show
  1. package/CHANGELOG.md +3 -2
  2. package/app/components/_app/index.jsx +30 -27
  3. package/app/components/bonus-product-view-modal/index.jsx +356 -0
  4. package/app/components/bonus-product-view-modal/index.test.js +1021 -0
  5. package/app/components/bonus-product-view-modal/utils.js +68 -0
  6. package/app/components/product-item/index.jsx +16 -13
  7. package/app/components/product-item-list/index.jsx +17 -4
  8. package/app/components/product-view/index.jsx +41 -3
  9. package/app/components/product-view/index.test.js +49 -16
  10. package/app/components/select-bonus-products-button/index.jsx +61 -0
  11. package/app/components/swatch-group/index.jsx +4 -3
  12. package/app/hooks/use-add-to-cart-modal.js +80 -2
  13. package/app/hooks/use-add-to-cart-modal.test.js +145 -0
  14. package/app/hooks/use-bonus-product-selection-modal/bonus-product-modal-reducer.js +123 -0
  15. package/app/hooks/use-bonus-product-selection-modal/bonus-product-modal-reducer.test.js +229 -0
  16. package/app/hooks/use-bonus-product-selection-modal/components/bonus-product-item.js +247 -0
  17. package/app/hooks/use-bonus-product-selection-modal/components/bonus-product-modal-provider.js +40 -0
  18. package/app/hooks/use-bonus-product-selection-modal/components/bonus-product-selection-modal.js +191 -0
  19. package/app/hooks/use-bonus-product-selection-modal/index.js +36 -0
  20. package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-data.js +149 -0
  21. package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-data.test.js +518 -0
  22. package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-modal-state.js +119 -0
  23. package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-modal-state.test.js +228 -0
  24. package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-wishlist.js +136 -0
  25. package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-wishlist.test.js +243 -0
  26. package/app/hooks/use-bonus-product-selection-modal.js +20 -0
  27. package/app/hooks/use-bonus-product-selection-modal.test.js +354 -0
  28. package/app/hooks/use-bonus-product-view-modal.js +19 -0
  29. package/app/hooks/use-modal-state.js +50 -0
  30. package/app/pages/cart/index.jsx +332 -67
  31. package/app/pages/cart/index.test.js +110 -0
  32. package/app/pages/cart/partials/cart-product-list-with-grouped-bonus-products.jsx +218 -0
  33. package/app/pages/cart/partials/cart-secondary-button-group.jsx +7 -9
  34. package/app/pages/cart/partials/cart-secondary-button-group.test.js +51 -3
  35. package/app/pages/cart/partials/select-bonus-products-card.jsx +197 -0
  36. package/app/pages/cart/partials/select-bonus-products-card.test.jsx +228 -0
  37. package/app/static/translations/compiled/en-GB.json +96 -0
  38. package/app/static/translations/compiled/en-US.json +96 -0
  39. package/app/static/translations/compiled/en-XA.json +176 -0
  40. package/app/theme/components/project/add-to-cart-modal.js +80 -0
  41. package/app/theme/components/project/bonus-product-view-modal.js +14 -0
  42. package/app/theme/components/project/product-view-modal.js +48 -0
  43. package/app/utils/bonus-product/business-logic.js +49 -0
  44. package/app/utils/bonus-product/business-logic.test.js +72 -0
  45. package/app/utils/bonus-product/calculations.js +55 -0
  46. package/app/utils/bonus-product/calculations.test.js +81 -0
  47. package/app/utils/bonus-product/cart.js +336 -0
  48. package/app/utils/bonus-product/cart.test.js +77 -0
  49. package/app/utils/bonus-product/common.js +102 -0
  50. package/app/utils/bonus-product/common.test.js +84 -0
  51. package/app/utils/bonus-product/discovery.js +258 -0
  52. package/app/utils/bonus-product/discovery.test.js +162 -0
  53. package/app/utils/bonus-product/hooks.js +178 -0
  54. package/app/utils/bonus-product/hooks.test.js +27 -0
  55. package/app/utils/bonus-product/index.js +22 -0
  56. package/app/utils/bonus-product/utils.js +51 -0
  57. package/app/utils/bonus-product/utils.test.js +223 -0
  58. package/app/utils/test-utils.js +6 -1
  59. package/config/default.js +5 -0
  60. package/package.json +7 -7
  61. package/translations/en-GB.json +30 -0
  62. package/translations/en-US.json +30 -0
package/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ## v8.1.0-dev (Sep 04, 2025)
1
+ ## v8.1.0-preview.3 (Sep 23, 2025)
2
2
  - Updated search UX - prices, images, suggestions new layout [#3271](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3271)
3
3
  - Updated the UI for StoreDisplay component which displays pickup in-store information on different pages. [#3248](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3248)
4
4
  - Added warning modal for guest users when toggling between multi ship and ship to one address. [#3280](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3280) [#3302](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3302)
@@ -6,6 +6,7 @@
6
6
  - Enhanced the shopping assistant that integrates Salesforce Embedded Messaging Service with PWA Kit applications, adding comprehensive context support, localization capabilities, and improved user experience features. [#3259](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3259)
7
7
  - Removed domainUrl, locale, basetId properties as part off the ShopperAgent during initialization. [#3259](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3259)
8
8
  - Only show option to deliver to multiple addresses if there are multiple items in the basket. [#3336](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3336)
9
+ - Added support for Choice of Bonus Products feature. Users can now select from available bonus products when they qualify for the associated promotion. The bonus product selection flow can be entered from either the "Item Added to Cart" modal (when adding the qualifying product to the cart) or from the cart page. [#3292] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3292)
9
10
 
10
11
  ## v8.0.0 (Sep 04, 2025)
11
12
  - Add support for environment level base paths on /mobify routes [#2892](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2892)
@@ -36,7 +37,7 @@
36
37
  - Support saving default shipping address on user registration from order confirmation [#2706](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2706)
37
38
  - Minor updates to support BOPIS E2E tests [#2716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2716)
38
39
  - Provide conditional support for partial hydration (feature flag `PARTIAL_HYDRATION_ENABLED`) [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696) [#2846](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2846)
39
- - Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704) [#2760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2760) [#2815](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2815)
40
+ - Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704) [#2760](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2760) [#2815](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2815)
40
41
  - [Breaking] Support Standard Products [2697](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2697)
41
42
  - Introduce store locator [#2542](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2542)
42
43
  - Fix passwordless race conditions in form submission [#2758](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2758)
@@ -58,6 +58,7 @@ import {
58
58
  useDntNotification
59
59
  } from '@salesforce/retail-react-app/app/hooks/use-dnt-notification'
60
60
  import {AddToCartModalProvider} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
61
+ import {BonusProductSelectionModalProvider} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-selection-modal'
61
62
  import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
62
63
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
63
64
  import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
@@ -430,34 +431,36 @@ const App = (props) => {
430
431
  </Island>
431
432
  {!isOnline && <OfflineBanner />}
432
433
  <AddToCartModalProvider>
433
- <SkipNavContent
434
- style={{
435
- display: 'flex',
436
- flexDirection: 'column',
437
- flex: 1,
438
- outline: 0
439
- }}
440
- >
441
- <Box
442
- as="main"
443
- id="app-main"
444
- role="main"
445
- display="flex"
446
- flexDirection="column"
447
- flex="1"
434
+ <BonusProductSelectionModalProvider>
435
+ <SkipNavContent
436
+ style={{
437
+ display: 'flex',
438
+ flexDirection: 'column',
439
+ flex: 1,
440
+ outline: 0
441
+ }}
448
442
  >
449
- <OfflineBoundary isOnline={false}>
450
- {children}
451
- </OfflineBoundary>
452
- </Box>
453
- </SkipNavContent>
454
-
455
- <Island hydrateOn={'visible'}>
456
- {!isCheckout ? <Footer /> : <CheckoutFooter />}
457
- </Island>
458
-
459
- <AuthModal {...authModal} />
460
- <DntNotification {...dntNotification} />
443
+ <Box
444
+ as="main"
445
+ id="app-main"
446
+ role="main"
447
+ display="flex"
448
+ flexDirection="column"
449
+ flex="1"
450
+ >
451
+ <OfflineBoundary isOnline={false}>
452
+ {children}
453
+ </OfflineBoundary>
454
+ </Box>
455
+ </SkipNavContent>
456
+
457
+ <Island hydrateOn={'visible'}>
458
+ {!isCheckout ? <Footer /> : <CheckoutFooter />}
459
+ </Island>
460
+
461
+ <AuthModal {...authModal} />
462
+ <DntNotification {...dntNotification} />
463
+ </BonusProductSelectionModalProvider>
461
464
  </AddToCartModalProvider>
462
465
  </Box>
463
466
  </CurrencyProvider>
@@ -0,0 +1,356 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ import React, {useMemo, useCallback} from 'react'
9
+ import PropTypes from 'prop-types'
10
+ import {
11
+ Modal,
12
+ ModalOverlay,
13
+ ModalContent,
14
+ ModalHeader,
15
+ ModalBody,
16
+ ModalCloseButton,
17
+ Button,
18
+ Box,
19
+ Text,
20
+ Heading
21
+ } from '@salesforce/retail-react-app/app/components/shared/ui'
22
+ import ProductView from '@salesforce/retail-react-app/app/components/product-view'
23
+ import {useProductViewModal} from '@salesforce/retail-react-app/app/hooks/use-product-view-modal'
24
+ import {useIntl} from 'react-intl'
25
+ import {useShopperBasketsMutationHelper} from '@salesforce/commerce-sdk-react'
26
+ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
27
+ import {getRemainingAvailableBonusProductsForProduct} from '@salesforce/retail-react-app/app/utils/bonus-product'
28
+ import {processProductsForBonusCart} from '@salesforce/retail-react-app/app/utils/bonus-product/cart'
29
+ import {useBonusProductCounts} from '@salesforce/retail-react-app/app/utils/bonus-product/hooks'
30
+ import {
31
+ createGetRemainingBonusQuantity,
32
+ checkForRemainingBonusProducts
33
+ } from '@salesforce/retail-react-app/app/components/bonus-product-view-modal/utils'
34
+ import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
35
+ import {productViewModalTheme} from '@salesforce/retail-react-app/app/theme/components/project/product-view-modal'
36
+ import {bonusProductViewModalTheme} from '@salesforce/retail-react-app/app/theme/components/project/bonus-product-view-modal'
37
+ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
38
+ import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
39
+
40
+ /**
41
+ * A Modal that contains Bonus Product View
42
+ */
43
+ const BonusProductViewModal = ({
44
+ product,
45
+ isOpen,
46
+ onClose,
47
+ bonusDiscountLineItemId,
48
+ promotionId,
49
+ onReturnToSelection,
50
+ ...props
51
+ }) => {
52
+ // Ensure a safe product shape for the modal hook
53
+ const safeProduct = useMemo(() => {
54
+ if (!product) return {productId: undefined, variants: [], variationAttributes: []}
55
+ const id = product.productId || product.id
56
+ return {
57
+ productId: id,
58
+ id,
59
+ variants: product.variants || [],
60
+ variationAttributes: product.variationAttributes || [],
61
+ imageGroups: product.imageGroups || [],
62
+ type: product.type || {set: false, bundle: false},
63
+ price: product.price,
64
+ name: product.name || product.productName
65
+ }
66
+ }, [product])
67
+
68
+ const productViewModalData = useProductViewModal(safeProduct)
69
+ const {addItemToNewOrExistingBasket} = useShopperBasketsMutationHelper()
70
+ const {data: basket} = useCurrentBasket()
71
+ const navigate = useNavigation()
72
+
73
+ const intl = useIntl()
74
+ const {formatMessage} = intl
75
+ const showToast = useToast()
76
+
77
+ // Calculate bonus counts using promotionId and custom hook
78
+ const {finalSelectedBonusItems, finalMaxBonusItems} = useBonusProductCounts(basket, promotionId)
79
+
80
+ const messages = useMemo(
81
+ () => ({
82
+ modalLabel: formatMessage(
83
+ {
84
+ id: 'bonus_product_view_modal.modal_label',
85
+ defaultMessage: 'Bonus product selection modal for {productName}'
86
+ },
87
+ {productName: productViewModalData?.product?.name}
88
+ ),
89
+ viewCart: formatMessage({
90
+ id: 'bonus_product_view_modal.button.view_cart',
91
+ defaultMessage: 'View Cart'
92
+ }),
93
+ backToSelection: formatMessage({
94
+ id: 'bonus_product_view_modal.button.back_to_selection',
95
+ defaultMessage: '← Back to Selection'
96
+ })
97
+ }),
98
+ [intl]
99
+ )
100
+
101
+ // Create getRemainingBonusQuantity function using the factory
102
+ const getRemainingBonusQuantity = useMemo(
103
+ () =>
104
+ createGetRemainingBonusQuantity(
105
+ basket,
106
+ product,
107
+ getRemainingAvailableBonusProductsForProduct
108
+ ),
109
+ [basket, product]
110
+ )
111
+
112
+ // Custom addToCart handler for bonus products that includes bonusDiscountLineItemId
113
+ const handleAddToCart = useCallback(
114
+ async (products) => {
115
+ try {
116
+ // Process products using the extracted helper function
117
+ const productItems = processProductsForBonusCart(
118
+ products,
119
+ basket,
120
+ promotionId,
121
+ product,
122
+ getRemainingBonusQuantity
123
+ )
124
+
125
+ if (productItems.length === 0) {
126
+ return null
127
+ }
128
+
129
+ const result = await addItemToNewOrExistingBasket(productItems)
130
+
131
+ // Check for remaining bonus products after successful add to cart
132
+ if (result) {
133
+ // Show success toast notification
134
+ showToast({
135
+ title: formatMessage({
136
+ id: 'bonus_product_view_modal.toast.item_added',
137
+ defaultMessage: 'Bonus item added to cart'
138
+ }),
139
+ status: 'success'
140
+ })
141
+
142
+ // Get updated basket data to check for remaining bonus products
143
+ // addItemToNewOrExistingBasket returns the basket directly
144
+ const updatedBasket = result
145
+
146
+ // Check if there are still remaining bonus products available
147
+ const hasRemainingBonusProducts = checkForRemainingBonusProducts(updatedBasket)
148
+
149
+ if (hasRemainingBonusProducts && onReturnToSelection) {
150
+ // Return to SelectBonusProductModal if there are remaining bonus products
151
+ onReturnToSelection()
152
+ // Return null to prevent AddToCartModal from opening
153
+ return null
154
+ } else {
155
+ // Navigate to cart page if no remaining bonus products or no callback provided
156
+ onClose()
157
+ // Always use a delay to ensure modal closes cleanly
158
+ setTimeout(() => {
159
+ navigate('/cart', 'push')
160
+ }, 200)
161
+ // Return null to prevent AddToCartModal from opening
162
+ return null
163
+ }
164
+ }
165
+
166
+ // For bonus products, don't open add-to-cart modal - just return null
167
+ return null
168
+ } catch (error) {
169
+ console.error('Error adding bonus product to cart:', error)
170
+ return null
171
+ }
172
+ },
173
+ [
174
+ addItemToNewOrExistingBasket,
175
+ basket,
176
+ promotionId,
177
+ product,
178
+ getRemainingBonusQuantity,
179
+ onClose,
180
+ navigate,
181
+ onReturnToSelection,
182
+ showToast,
183
+ formatMessage
184
+ ]
185
+ )
186
+
187
+ // Custom buttons for the ProductView
188
+ const handleViewCart = useCallback(() => {
189
+ // Close modal immediately and navigate with proper delay
190
+ onClose()
191
+ // Always use a delay to ensure modal closes cleanly
192
+ setTimeout(() => {
193
+ navigate('/cart', 'push')
194
+ }, 200)
195
+ }, [onClose, navigate])
196
+
197
+ // Reusable Back to Selection button component
198
+ const BackToSelectionButton = useMemo(
199
+ () => (
200
+ <Text
201
+ as="button"
202
+ color="blue.600"
203
+ cursor="pointer"
204
+ onClick={onReturnToSelection}
205
+ fontSize={{base: 'lg', lg: 'md'}}
206
+ _hover={{
207
+ color: 'blue.700'
208
+ }}
209
+ >
210
+ {messages.backToSelection}
211
+ </Text>
212
+ ),
213
+ [messages.backToSelection, onReturnToSelection]
214
+ )
215
+
216
+ const customButtons = useMemo(
217
+ () => [
218
+ <Button key="view-cart" variant="outline" onClick={handleViewCart}>
219
+ {messages.viewCart}
220
+ </Button>
221
+ ],
222
+ [messages.viewCart, handleViewCart]
223
+ )
224
+
225
+ // Clean product data but preserve variation attributes for size/color selectors
226
+ const productToRender = useMemo(() => {
227
+ const baseProduct = productViewModalData.product || safeProduct
228
+ return {
229
+ ...baseProduct,
230
+ variationAttributes: baseProduct.variationAttributes,
231
+ variants: baseProduct.variants,
232
+ variationParams: baseProduct.variationParams,
233
+ selectedVariationAttributes: baseProduct.selectedVariationAttributes,
234
+ type: baseProduct.type,
235
+ // Ensure proper inventory and quantity defaults for bonus products
236
+ inventory: {
237
+ ...baseProduct.inventory,
238
+ orderable: true,
239
+ stockLevel: 999 // High stock level for bonus products
240
+ },
241
+ minOrderQuantity: 1,
242
+ stepQuantity: 1,
243
+ // Ensure the product is orderable
244
+ orderable: true,
245
+ // Add review data for display
246
+ rating: baseProduct.rating,
247
+ reviewCount: baseProduct.reviewCount
248
+ }
249
+ }, [productViewModalData.product, safeProduct])
250
+
251
+ // Calculate max order quantity for UI
252
+ const maxOrderQuantity = getRemainingBonusQuantity()
253
+
254
+ return (
255
+ <Modal
256
+ isOpen={isOpen}
257
+ onClose={onClose}
258
+ size={productViewModalTheme.modal.size}
259
+ closeOnOverlayClick={true}
260
+ closeOnEsc={true}
261
+ isCentered
262
+ motionPreset="slideInBottom"
263
+ preserveScrollBarGap={true}
264
+ >
265
+ <ModalOverlay />
266
+ <ModalContent
267
+ data-testid="bonus-product-view-modal"
268
+ aria-label={messages.modalLabel}
269
+ margin={productViewModalTheme.layout.content.margin}
270
+ borderRadius={productViewModalTheme.layout.content.borderRadius}
271
+ bg={productViewModalTheme.layout.content.background}
272
+ maxHeight={bonusProductViewModalTheme.layout.content.maxHeight}
273
+ overflowY={productViewModalTheme.layout.content.overflowY}
274
+ >
275
+ <ModalHeader
276
+ bg={productViewModalTheme.colors.contentBackground}
277
+ pb={onReturnToSelection ? {base: 1, lg: 6} : 6}
278
+ px={6}
279
+ pt={6}
280
+ >
281
+ <Heading size="md">
282
+ {formatMessage(
283
+ {
284
+ id: 'bonus_product_view_modal.title',
285
+ defaultMessage:
286
+ 'Select bonus product ({selected} of {max} selected)'
287
+ },
288
+ {selected: finalSelectedBonusItems, max: finalMaxBonusItems}
289
+ )}
290
+ </Heading>
291
+ {/* Mobile-only Back to Selection button */}
292
+ {onReturnToSelection && (
293
+ <HideOnDesktop>
294
+ <Box mt={2} mb={0}>
295
+ {BackToSelectionButton}
296
+ </Box>
297
+ </HideOnDesktop>
298
+ )}
299
+ </ModalHeader>
300
+
301
+ <ModalBody
302
+ bg={productViewModalTheme.layout.body.background}
303
+ px={productViewModalTheme.layout.body.padding}
304
+ pt={
305
+ onReturnToSelection
306
+ ? {base: 1, lg: productViewModalTheme.layout.body.padding}
307
+ : productViewModalTheme.layout.body.padding
308
+ }
309
+ pb={productViewModalTheme.layout.body.paddingBottom}
310
+ >
311
+ {productViewModalData.isFetching && !productViewModalData.product ? (
312
+ <Box p={8} textAlign="center">
313
+ <Text>Loading product details...</Text>
314
+ </Box>
315
+ ) : (
316
+ <ProductView
317
+ showFullLink={false}
318
+ imageSize="sm"
319
+ showImageGallery={true}
320
+ product={productToRender}
321
+ isLoading={false}
322
+ addToCart={handleAddToCart}
323
+ isProductLoading={false}
324
+ customButtons={customButtons}
325
+ promotionId={promotionId}
326
+ maxOrderQuantity={maxOrderQuantity}
327
+ showReviews={true}
328
+ showVariationAttributes={true}
329
+ alignItems="stretch"
330
+ imageGalleryFooter={
331
+ onReturnToSelection ? (
332
+ <HideOnMobile>{BackToSelectionButton}</HideOnMobile>
333
+ ) : null
334
+ }
335
+ {...props}
336
+ />
337
+ )}
338
+ </ModalBody>
339
+ <ModalCloseButton size="sm" />
340
+ </ModalContent>
341
+ </Modal>
342
+ )
343
+ }
344
+
345
+ BonusProductViewModal.propTypes = {
346
+ isOpen: PropTypes.bool.isRequired,
347
+ onOpen: PropTypes.func,
348
+ onClose: PropTypes.func.isRequired,
349
+ product: PropTypes.object,
350
+ isLoading: PropTypes.bool,
351
+ bonusDiscountLineItemId: PropTypes.string, // The 'id' from bonusDiscountLineItems
352
+ promotionId: PropTypes.string, // The promotion ID to filter promotions in PromoCallout
353
+ onReturnToSelection: PropTypes.func // Callback to return to SelectBonusProductModal
354
+ }
355
+
356
+ export default BonusProductViewModal