Skip to content

Commit 17a7bf6

Browse files
@W-19168138 add a new modal to be used for bonus product selection modal (#2988)
1 parent b6ec663 commit 17a7bf6

File tree

8 files changed

+380
-77
lines changed

8 files changed

+380
-77
lines changed

packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ScrollToTop from '../../scroll-to-top'
1313
import OfflineBanner from '../../offline-banner'
1414
import OfflineBoundary from '../../offline-boundary'
1515
import {AddToCartModalProvider} from '../../../hooks/use-add-to-cart-modal'
16+
import {BonusProductSelectionModalProvider} from '../../../hooks/use-bonus-product-selection-modal'
1617

1718
/**
1819
* AppLayout component that provides the main layout structure
@@ -37,33 +38,35 @@ const AppLayout = ({
3738
{/* Offline Banner */}
3839
{isOnline === false && <OfflineBanner isOnline={isOnline} />}
3940

40-
<AddToCartModalProvider>
41-
<SkipNavContent
42-
css={{
43-
display: 'flex',
44-
flexDirection: 'column',
45-
flex: 1,
46-
outline: 0
47-
}}
48-
>
49-
<Box
50-
as="main"
51-
id="app-main"
52-
role="main"
53-
display="flex"
54-
flexDirection="column"
55-
flex="1"
41+
<BonusProductSelectionModalProvider>
42+
<AddToCartModalProvider>
43+
<SkipNavContent
44+
css={{
45+
display: 'flex',
46+
flexDirection: 'column',
47+
flex: 1,
48+
outline: 0
49+
}}
5650
>
57-
<OfflineBoundary isOnline={isOnline}>{children}</OfflineBoundary>
58-
</Box>
59-
</SkipNavContent>
51+
<Box
52+
as="main"
53+
id="app-main"
54+
role="main"
55+
display="flex"
56+
flexDirection="column"
57+
flex="1"
58+
>
59+
<OfflineBoundary isOnline={isOnline}>{children}</OfflineBoundary>
60+
</Box>
61+
</SkipNavContent>
6062

61-
{/* Footer */}
62-
{footerComponent}
63+
{/* Footer */}
64+
{footerComponent}
6365

64-
{/* Modals */}
65-
{modalsComponent}
66-
</AddToCartModalProvider>
66+
{/* Modals */}
67+
{modalsComponent}
68+
</AddToCartModalProvider>
69+
</BonusProductSelectionModalProvider>
6770
</Box>
6871
</>
6972
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
import React from 'react'
8+
import PropTypes from 'prop-types'
9+
import {Button} from '@chakra-ui/react'
10+
import {useIntl} from 'react-intl'
11+
12+
const SelectBonusProductsButton = ({
13+
bonusDiscountLineItems,
14+
product,
15+
itemsAdded,
16+
onOpenBonusModal,
17+
onClose,
18+
...buttonProps
19+
}) => {
20+
const intl = useIntl()
21+
22+
const handleClick = () => {
23+
if (onOpenBonusModal) {
24+
onOpenBonusModal({
25+
bonusDiscountLineItems,
26+
product,
27+
itemsAdded
28+
})
29+
}
30+
if (onClose) onClose()
31+
}
32+
33+
return (
34+
<Button
35+
onClick={handleClick}
36+
width="100%"
37+
variant="outline-gray"
38+
color="blue.600"
39+
size="md"
40+
height={9}
41+
minWidth={11}
42+
textStyle="sm"
43+
{...buttonProps}
44+
>
45+
{intl.formatMessage({
46+
defaultMessage: 'Select Bonus Products',
47+
id: 'add_to_cart_modal.button.select_bonus_products'
48+
})}
49+
</Button>
50+
)
51+
}
52+
53+
SelectBonusProductsButton.propTypes = {
54+
bonusDiscountLineItems: PropTypes.array,
55+
product: PropTypes.object,
56+
itemsAdded: PropTypes.array,
57+
onOpenBonusModal: PropTypes.func,
58+
onClose: PropTypes.func
59+
}
60+
61+
export default SelectBonusProductsButton

packages/template-chakra-storefront/src/hooks/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ export {useDerivedProduct} from './use-derived-product'
1717
export {useCurrency} from './use-currency'
1818
export {useCurrentCustomer} from './use-current-customer'
1919
export {useCurrentBasket} from './use-current-basket'
20+
export {
21+
BonusProductSelectionModalProvider,
22+
useBonusProductSelectionModalContext,
23+
useBonusProductSelectionModal
24+
} from './use-bonus-product-selection-modal'

packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
useBreakpointValue
2222
} from '@chakra-ui/react'
2323
import {useCurrentBasket} from './use-current-basket'
24+
import {usePromotions} from '@salesforce/commerce-sdk-react'
2425
import Link from '../components/link'
2526
import RecommendedProducts from '../components/recommended-products'
2627
import {LockIcon} from '../components/icons'
@@ -29,7 +30,10 @@ import {getPriceData, getDisplayVariationValues} from '../utils/product-utils'
2930
import {EINSTEIN_RECOMMENDERS} from '../../config/constants'
3031
import DisplayPrice from '../components/display-price'
3132
import SafePortal from '../components/safe-portal'
33+
import {useBonusProductSelectionModalContext} from './use-bonus-product-selection-modal'
3234
import {addToCartModalTheme} from '../theme/components/project/add-to-cart-modal'
35+
import SelectBonusProductsButton from '../components/select-bonus-products-button'
36+
import {useModalState} from './use-modal-state'
3337

3438
/**
3539
* Local configuration for component-specific styling
@@ -69,8 +73,10 @@ AddToCartModalProvider.propTypes = {
6973
/**
7074
* Visual feedback (a modal) for adding item to the cart.
7175
*/
72-
export const AddToCartModal = () => {
76+
export const AddToCartModal = ({onSelectBonusProductsClick}) => {
7377
const {isOpen, onClose, data} = useAddToCartModalContext()
78+
const bonusProductContext = useBonusProductSelectionModalContext()
79+
const {onOpen: onOpenBonusModal} = bonusProductContext || {}
7480
const {product, itemsAdded = [], selectedQuantity} = data || {}
7581
const isProductABundle = !!product?.type.bundle
7682

@@ -80,6 +86,21 @@ export const AddToCartModal = () => {
8086
data: basket = {},
8187
derivedData: {totalItems}
8288
} = useCurrentBasket()
89+
90+
const {bonusDiscountLineItems = []} = basket || {}
91+
92+
// Extract unique promotion IDs
93+
const promotionIds = [
94+
...new Set(bonusDiscountLineItems.map((item) => item.promotionId).filter(Boolean))
95+
]
96+
// Fetch promotion details
97+
const {data: promotions, isLoading: isPromotionsLoading} = usePromotions(
98+
{parameters: {ids: promotionIds.join(',')}},
99+
{enabled: promotionIds.length > 0}
100+
)
101+
// Get the first promotion's details
102+
const promotionText = promotions?.data?.[0]?.details || ''
103+
83104
const size = useBreakpointValue(addToCartModalTheme.modal.size)
84105
const {currency, productSubTotal} = basket
85106
const numberOfItemsAdded = isProductABundle
@@ -370,25 +391,26 @@ export const AddToCartModal = () => {
370391
</Flex>
371392
)
372393
})}
373-
{/* TODO: replace with text fetched from promotion */}
374-
<Text mb={2} fontSize="md" fontWeight="normal" textAlign="left">
375-
{'Bonus products available!'}
376-
</Text>
377-
<Button
378-
as={Link}
379-
to="/checkout"
380-
width="100%"
381-
variant="outline-gray"
382-
size="md"
383-
height={9}
384-
minWidth={11}
385-
textStyle="sm"
386-
>
387-
{intl.formatMessage({
388-
defaultMessage: 'Select Bonus Products',
389-
id: 'add_to_cart_modal.button.select_bonus_products'
390-
})}
391-
</Button>
394+
{bonusDiscountLineItems &&
395+
bonusDiscountLineItems.length > 0 && (
396+
<>
397+
<Text
398+
mb={2}
399+
fontSize="md"
400+
fontWeight="normal"
401+
textAlign="left"
402+
>
403+
{promotionText}
404+
</Text>
405+
<SelectBonusProductsButton
406+
bonusDiscountLineItems={bonusDiscountLineItems}
407+
product={product}
408+
itemsAdded={itemsAdded}
409+
onOpenBonusModal={onOpenBonusModal}
410+
onClose={onClose}
411+
/>
412+
</>
413+
)}
392414
</Box>
393415
<Box
394416
display={['none', 'none', 'none', 'block']}
@@ -491,39 +513,14 @@ AddToCartModal.propTypes = {
491513
quantity: PropTypes.number,
492514
isOpen: PropTypes.bool,
493515
onClose: PropTypes.func,
516+
onSelectBonusProductsClick: PropTypes.func,
494517
children: PropTypes.any
495518
}
496519

497520
export const useAddToCartModal = () => {
498-
const [state, setState] = useState({
499-
isOpen: false,
500-
data: null
521+
const {isOpen, data, onOpen, onClose} = useModalState({
522+
closeOnRouteChange: true,
523+
resetDataOnClose: true
501524
})
502-
503-
const {pathname} = useLocation()
504-
useEffect(() => {
505-
if (state.isOpen) {
506-
setState({
507-
...state,
508-
isOpen: false
509-
})
510-
}
511-
}, [pathname])
512-
513-
return {
514-
isOpen: state.isOpen,
515-
data: state.data,
516-
onOpen: (data) => {
517-
setState({
518-
isOpen: true,
519-
data
520-
})
521-
},
522-
onClose: () => {
523-
setState({
524-
isOpen: false,
525-
data: null
526-
})
527-
}
528-
}
525+
return {isOpen, data, onOpen, onClose}
529526
}

packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,11 @@ test('Renders AddToCartModal properly', () => {
601601
id: '701642811399M',
602602
quantity: 22
603603
}
604+
],
605+
bonusDiscountLineItems: [
606+
{
607+
promotionId: 'ChoiceOfBonusProdect-ProductLevel-ruleBased'
608+
}
604609
]
605610
}
606611

@@ -621,9 +626,6 @@ test('Renders AddToCartModal properly', () => {
621626
const numOfRowsRendered = screen.getAllByTestId('product-added').length
622627
expect(numOfRowsRendered).toEqual(MOCK_DATA.itemsAdded.length)
623628

624-
// Check that the promotional message is displayed
625-
expect(screen.getByText('Bonus products available!')).toBeInTheDocument() //todo: update tests after static text is removed
626-
627629
// Check that the "Select Bonus Products" button is displayed
628630
expect(screen.getByText('Select Bonus Products')).toBeInTheDocument()
629631
})
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2025, 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, {useContext, useState, useEffect} from 'react'
9+
import {useLocation} from 'react-router-dom'
10+
import PropTypes from 'prop-types'
11+
import {Dialog, Button, Text, Box, useBreakpointValue} from '@chakra-ui/react'
12+
import {useModalState} from './use-modal-state'
13+
14+
/**
15+
* Context for managing the BonusProductSelectionModal.
16+
* Used in top level App component.
17+
*/
18+
export const BonusProductSelectionModalContext = React.createContext()
19+
export const useBonusProductSelectionModalContext = () =>
20+
useContext(BonusProductSelectionModalContext)
21+
22+
export const BonusProductSelectionModalProvider = ({children}) => {
23+
const bonusProductSelectionModal = useBonusProductSelectionModal()
24+
return (
25+
<BonusProductSelectionModalContext.Provider value={bonusProductSelectionModal}>
26+
{children}
27+
<BonusProductSelectionModal />
28+
</BonusProductSelectionModalContext.Provider>
29+
)
30+
}
31+
32+
BonusProductSelectionModalProvider.propTypes = {
33+
children: PropTypes.node.isRequired
34+
}
35+
36+
/**
37+
* Modal for selecting from available bonus products.
38+
*/
39+
export const BonusProductSelectionModal = () => {
40+
const {isOpen, onClose, data} = useBonusProductSelectionModalContext()
41+
const size = useBreakpointValue({base: 'full', lg: 'lg', xl: 'xl'})
42+
43+
if (!isOpen) {
44+
return null
45+
}
46+
47+
// todo: this component will be replaced in the next work item. The component will display bonus products available for selection.
48+
return (
49+
<Dialog.Root
50+
size={size}
51+
open={isOpen}
52+
onOpenChange={onClose}
53+
scrollBehavior="inside"
54+
placement="center"
55+
>
56+
<Dialog.Backdrop />
57+
<Dialog.Positioner>
58+
<Dialog.Content>
59+
<Dialog.Body bgColor="white" padding={8}>
60+
<Text fontSize="md" mb="4">
61+
Bonus Product Modal
62+
</Text>
63+
{data && (
64+
<Box p="4" bg="gray.100" borderRadius="md" mb="4">
65+
<Text fontSize="sm" fontWeight="bold" mb="2">
66+
Received Data:
67+
</Text>
68+
<Text fontSize="xs" fontFamily="mono">
69+
{JSON.stringify(data, null, 2)}
70+
</Text>
71+
</Box>
72+
)}
73+
</Dialog.Body>
74+
<Dialog.Footer bgColor="white" padding={8}>
75+
<Button onClick={onClose} variant="solid" width="100%">
76+
Close
77+
</Button>
78+
</Dialog.Footer>
79+
</Dialog.Content>
80+
</Dialog.Positioner>
81+
</Dialog.Root>
82+
)
83+
}
84+
85+
export const useBonusProductSelectionModal = () => {
86+
const {isOpen, data, onOpen, onClose} = useModalState({
87+
closeOnRouteChange: true,
88+
resetDataOnClose: true
89+
})
90+
return {isOpen, data, onOpen, onClose}
91+
}

0 commit comments

Comments
 (0)