Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 26 additions & 25 deletions packages/template-retail-react-app/app/components/_app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {SkipNavLink, SkipNavContent} from '@chakra-ui/skip-nav'

// Contexts
import {CurrencyProvider} from '@salesforce/retail-react-app/app/contexts'
import {BonusProductModalProvider} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-modal'

// Local Project Components
import Header from '@salesforce/retail-react-app/app/components/header'
Expand Down Expand Up @@ -413,32 +414,32 @@ const App = (props) => {
</Box>
{!isOnline && <OfflineBanner />}
<AddToCartModalProvider>
<SkipNavContent
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
outline: 0
}}
>
<Box
as="main"
id="app-main"
role="main"
display="flex"
flexDirection="column"
flex="1"
<BonusProductModalProvider>
<SkipNavContent
style={{
display: 'flex',
flexDirection: 'column',
flex: 1,
outline: 0
}}
>
<OfflineBoundary isOnline={false}>
{children}
</OfflineBoundary>
</Box>
</SkipNavContent>

{!isCheckout ? <Footer /> : <CheckoutFooter />}

<AuthModal {...authModal} />
<DntNotification {...dntNotification} />
<Box
as="main"
id="app-main"
role="main"
display="flex"
flexDirection="column"
flex="1"
>
<OfflineBoundary isOnline={false}>
{children}
</OfflineBoundary>
</Box>
</SkipNavContent>
{!isCheckout ? <Footer /> : <CheckoutFooter />}
<AuthModal {...authModal} />
<DntNotification {...dntNotification} />
</BonusProductModalProvider>
</AddToCartModalProvider>
</Box>
</CurrencyProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/hooks'
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
import {useBonusProductModalContext} from '@salesforce/retail-react-app/app/hooks/use-bonus-product-modal'

// project components
import ImageGallery from '@salesforce/retail-react-app/app/components/image-gallery'
Expand Down Expand Up @@ -131,6 +132,13 @@ const ProductView = forwardRef(
onOpen: onAddToCartModalOpen,
onClose: onAddToCartModalClose
} = useAddToCartModalContext()
const {
isOpen: isBonusProductModalOpen,
onOpen: onBonusProductModalOpen,
onClose: onBonusProductModalClose,
bonusProducts,
addBonusProducts
} = useBonusProductModalContext()
const theme = useTheme()
const [showOptionsMessage, toggleShowOptionsMessage] = useState(false)
const {
Expand Down Expand Up @@ -272,16 +280,49 @@ const ProductView = forwardRef(
return
}
try {
const itemsAdded = await addToCart(variant, quantity)
const addToCartResponse = await addToCart(variant, quantity)

// For regular products: addToCartResponse has productSelectionValues and possibly bonusDiscountLineItems
// For product bundles: addToCartResponse is just the childProductSelections array
const itemsAdded =
addToCartResponse?.productSelectionValues || addToCartResponse
const isValidResponse =
itemsAdded && (Array.isArray(itemsAdded) || itemsAdded.length > 0)

// Compare existing bonus products with new bonus discount line items
// Only regular products (not bundles) can have bonusDiscountLineItems
const newBonusItems =
addToCartResponse?.bonusDiscountLineItems?.filter(
(newItem) =>
Copy link
Contributor

@sf-jhalak-maheshwari sf-jhalak-maheshwari Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change name to bonusItem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can move this to within the "isValidResponse" check, right?

!bonusProducts.some(
(existingItem) => existingItem.id === newItem.id
)
) || []

// Open modal only when `addToCart` returns some data
// It's possible that the item has been added to cart, but we don't want to open the modal.
// See wishlist_primary_action for example.
if (itemsAdded) {
onAddToCartModalOpen({
product,
itemsAdded,
selectedQuantity: quantity
})
if (isValidResponse) {
// Show bonus product modal first if there are bonus items
if (newBonusItems?.length > 0) {
// Update bonusProducts list with the new bonus items
addBonusProducts(newBonusItems)
onBonusProductModalOpen({
newBonusItems,
allBonusItems: addToCartResponse.bonusDiscountLineItems,
openAddToCartModalIfNeeded: true,
product,
itemsAdded,
selectedQuantity: quantity
})
} else {
// If no bonus items, just show add to cart modal
onAddToCartModalOpen({
product,
itemsAdded,
selectedQuantity: quantity
})
}
}
} catch (e) {
showError()
Expand Down Expand Up @@ -364,6 +405,9 @@ const ProductView = forwardRef(
if (isAddToCartModalOpen) {
onAddToCartModalClose()
}
if (isBonusProductModalOpen) {
onBonusProductModalClose()
}
}, [location.pathname])

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useContext, useState, useEffect} from 'react'
import {useLocation} from 'react-router-dom'
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
import PropTypes from 'prop-types'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {
Modal,
ModalCloseButton,
ModalContent,
ModalOverlay,
useBreakpointValue,
ModalHeader,
ModalBody,
Heading
} from '@salesforce/retail-react-app/app/components/shared/ui'

export const BonusProductModalContext = React.createContext()

export const useBonusProductModalContext = () => useContext(BonusProductModalContext)

export const BonusProductModalProvider = ({children}) => {
const {data: basket} = useCurrentBasket()
const bonusProductState = useBonusState(basket)

return (
<BonusProductModalContext.Provider value={bonusProductState}>
{children}
<BonusProductModal />
</BonusProductModalContext.Provider>
)
}
BonusProductModalProvider.propTypes = {
children: PropTypes.node.isRequired
}

export const BonusProductModal = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move components to the component directory

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's break this down into two parts - the hook for business logic (keep it under hooks) and component for presentation (under components)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kzheng-sfdc Sure. This PR was already merged by the time this comment was made. @sf-vrushal-kulkarni will be making this change in his PR

const {isOpen, data, onClose, onOpen} = useBonusProductModalContext()
const size = useBreakpointValue({base: 'full', lg: '2xl', xl: '4xl'})

if (!isOpen) {
return null
}
return (
<Modal size={size} isOpen={isOpen} onClose={onClose} scrollBehavior="inside" isCentered>
<ModalOverlay />
<ModalContent
margin="0"
borderRadius={{base: 'none', md: 'base'}}
bgColor="gray.50"
containerProps={{'data-testid': 'bonus-product-modal'}}
>
<ModalHeader paddingY="8" bgColor="white">
<Heading as="h1" fontSize="2xl"></Heading>
</ModalHeader>
<ModalCloseButton />
{/* Add your modal content here */}
<ModalBody bgColor="white" padding="0" marginBottom={{base: 40, lg: 0}}></ModalBody>
</ModalContent>
</Modal>
)
}

export const useBonusState = (basket) => {
const [state, setState] = useState({
isOpen: false,
data: {},
bonusProducts: basket?.bonusDiscountLineItems || []
})
const {pathname} = useLocation()
const {onOpen: onAddToCartModalOpen} = useAddToCartModalContext()

useEffect(() => {
if (state.isOpen) {
setState((prev) => ({
...prev,
isOpen: false
}))
}
}, [pathname])

// Update bonusProducts when basket changes
useEffect(() => {
setState((prev) => ({
...prev,
bonusProducts: basket?.bonusDiscountLineItems || []
}))
}, [basket])

const addBonusProducts = (newBonusItems) => {
setState((prev) => {
const updatedBonusProducts = [...prev.bonusProducts, ...newBonusItems]
return {
...prev,
bonusProducts: updatedBonusProducts
}
})
}

return {
isOpen: state.isOpen,
data: state.data,
bonusProducts: state.bonusProducts,
addBonusProducts,
onClose: () => {
setState((prev) => ({
...prev,
isOpen: false,
data: {}
}))

if (state.data.openAddToCartModalIfNeeded && state.data.product) {
onAddToCartModalOpen({
product: state.data.product,
itemsAdded: state.data.itemsAdded,
selectedQuantity: state.data.selectedQuantity
})
}
},
onOpen: (data) => {
setState((prev) => ({
...prev,
isOpen: true,
data
}))
}
}
}
Loading
Loading