@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.
- package/CHANGELOG.md +3 -2
- package/app/components/_app/index.jsx +30 -27
- package/app/components/bonus-product-view-modal/index.jsx +356 -0
- package/app/components/bonus-product-view-modal/index.test.js +1021 -0
- package/app/components/bonus-product-view-modal/utils.js +68 -0
- package/app/components/product-item/index.jsx +16 -13
- package/app/components/product-item-list/index.jsx +17 -4
- package/app/components/product-view/index.jsx +41 -3
- package/app/components/product-view/index.test.js +49 -16
- package/app/components/select-bonus-products-button/index.jsx +61 -0
- package/app/components/swatch-group/index.jsx +4 -3
- package/app/hooks/use-add-to-cart-modal.js +80 -2
- package/app/hooks/use-add-to-cart-modal.test.js +145 -0
- package/app/hooks/use-bonus-product-selection-modal/bonus-product-modal-reducer.js +123 -0
- package/app/hooks/use-bonus-product-selection-modal/bonus-product-modal-reducer.test.js +229 -0
- package/app/hooks/use-bonus-product-selection-modal/components/bonus-product-item.js +247 -0
- package/app/hooks/use-bonus-product-selection-modal/components/bonus-product-modal-provider.js +40 -0
- package/app/hooks/use-bonus-product-selection-modal/components/bonus-product-selection-modal.js +191 -0
- package/app/hooks/use-bonus-product-selection-modal/index.js +36 -0
- package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-data.js +149 -0
- package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-data.test.js +518 -0
- package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-modal-state.js +119 -0
- package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-modal-state.test.js +228 -0
- package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-wishlist.js +136 -0
- package/app/hooks/use-bonus-product-selection-modal/use-bonus-product-wishlist.test.js +243 -0
- package/app/hooks/use-bonus-product-selection-modal.js +20 -0
- package/app/hooks/use-bonus-product-selection-modal.test.js +354 -0
- package/app/hooks/use-bonus-product-view-modal.js +19 -0
- package/app/hooks/use-modal-state.js +50 -0
- package/app/pages/cart/index.jsx +332 -67
- package/app/pages/cart/index.test.js +110 -0
- package/app/pages/cart/partials/cart-product-list-with-grouped-bonus-products.jsx +218 -0
- package/app/pages/cart/partials/cart-secondary-button-group.jsx +7 -9
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +51 -3
- package/app/pages/cart/partials/select-bonus-products-card.jsx +197 -0
- package/app/pages/cart/partials/select-bonus-products-card.test.jsx +228 -0
- package/app/static/translations/compiled/en-GB.json +96 -0
- package/app/static/translations/compiled/en-US.json +96 -0
- package/app/static/translations/compiled/en-XA.json +176 -0
- package/app/theme/components/project/add-to-cart-modal.js +80 -0
- package/app/theme/components/project/bonus-product-view-modal.js +14 -0
- package/app/theme/components/project/product-view-modal.js +48 -0
- package/app/utils/bonus-product/business-logic.js +49 -0
- package/app/utils/bonus-product/business-logic.test.js +72 -0
- package/app/utils/bonus-product/calculations.js +55 -0
- package/app/utils/bonus-product/calculations.test.js +81 -0
- package/app/utils/bonus-product/cart.js +336 -0
- package/app/utils/bonus-product/cart.test.js +77 -0
- package/app/utils/bonus-product/common.js +102 -0
- package/app/utils/bonus-product/common.test.js +84 -0
- package/app/utils/bonus-product/discovery.js +258 -0
- package/app/utils/bonus-product/discovery.test.js +162 -0
- package/app/utils/bonus-product/hooks.js +178 -0
- package/app/utils/bonus-product/hooks.test.js +27 -0
- package/app/utils/bonus-product/index.js +22 -0
- package/app/utils/bonus-product/utils.js +51 -0
- package/app/utils/bonus-product/utils.test.js +223 -0
- package/app/utils/test-utils.js +6 -1
- package/config/default.js +5 -0
- package/package.json +7 -7
- package/translations/en-GB.json +30 -0
- package/translations/en-US.json +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## v8.1.0-
|
|
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
|
-
<
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
<
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|