Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
802b9d3
Trigger a modal if bonus products exist in AddToCart response
sf-madhuri-uppu Jun 9, 2025
790a72b
skip changelog
sf-madhuri-uppu Jun 9, 2025
c2021fa
skip changelog
sf-madhuri-uppu Jun 9, 2025
0aadd8b
fixed import issue
sf-madhuri-uppu Jun 9, 2025
69d49b2
Added bonus products on the cart page
ddiazccrz Jun 10, 2025
bfb54b2
Updated change log
ddiazccrz Jun 10, 2025
a47d9b0
Removed unused import
ddiazccrz Jun 10, 2025
c693bea
Updated cart secondary buttons remove is gift and wishlist for bonus …
ddiazccrz Jun 10, 2025
92ec48e
Add bonus products title partial test
ddiazccrz Jun 10, 2025
84775e5
Fixed linting issues
ddiazccrz Jun 10, 2025
7830475
Review suggestions
ddiazccrz Jun 10, 2025
83b1dbb
removed localstorage and updated bonus products info from current basket
sf-madhuri-uppu Jun 10, 2025
0a697ef
removed unnecessary code
sf-madhuri-uppu Jun 10, 2025
4a0950c
nit space
sf-madhuri-uppu Jun 10, 2025
5c7b0bb
Code review comments
ddiazccrz Jun 10, 2025
afb7dc9
fixed 2 failing tests
sf-madhuri-uppu Jun 10, 2025
2600c99
Fix linting issues
ddiazccrz Jun 10, 2025
bd8f645
fixed a unit test
sf-madhuri-uppu Jun 10, 2025
392c52f
fixed lint errors
sf-madhuri-uppu Jun 10, 2025
90226ae
Moved mock data, removed un-needed index
ddiazccrz Jun 11, 2025
9414838
Addressed PR comments
sf-madhuri-uppu Jun 11, 2025
f8fd65e
use current basket query
sf-madhuri-uppu Jun 11, 2025
e441ee9
nit
sf-madhuri-uppu Jun 11, 2025
4848f14
removed unnecessary code
sf-madhuri-uppu Jun 12, 2025
3e7d1f9
Switched to test from it
ddiazccrz Jun 12, 2025
9f6cfe5
fixed infinite re-render issue
sf-madhuri-uppu Jun 12, 2025
06f0de5
Merge pull request #2541 from SalesforceCommerceCloud/t/cc-shark/W-18…
sf-madhuri-uppu Jun 12, 2025
4f8a0f5
Merged Develop in to Feature
ddiazccrz Jun 12, 2025
fb4c351
fixed lint issue when merging
ddiazccrz Jun 12, 2025
bc28fc3
Merge pull request #2560 from SalesforceCommerceCloud/feature/bonus-p…
ddiazccrz Jun 12, 2025
be2eb2b
Merge branch 'feature/bonus-products' into W-18407625/bonusProductsOn…
ddiazccrz Jun 13, 2025
8e0a562
Update CHANGELOG.md
ddiazccrz Jun 13, 2025
21771bd
Removed label check since no longer present
ddiazccrz Jun 13, 2025
5db3f9c
Removed partials and made them components
ddiazccrz Jun 13, 2025
3826c2d
Updated translations
ddiazccrz Jun 13, 2025
e29bcd2
Merge pull request #2547 from SalesforceCommerceCloud/W-18407625/bonu…
ddiazccrz Jun 16, 2025
a9d065d
Update translation and missing label
ddiazccrz Jun 16, 2025
48e883c
Merge branch 'feature/bonus-products' of github.com:SalesforceCommerc…
ddiazccrz Jun 16, 2025
8d640d0
Reverted bonus product modal
ddiazccrz Jul 1, 2025
f3d883c
Removed bonus product from test util
ddiazccrz Jul 1, 2025
d1f7b59
Merged in develop
ddiazccrz Jul 1, 2025
a9e65e2
Fixed whitespace issues
ddiazccrz Jul 1, 2025
c6536e2
Fixed change log
ddiazccrz Jul 1, 2025
90be03e
Fixed more whitespace
ddiazccrz Jul 1, 2025
2a05b9f
Merged in BOPIS
ddiazccrz Jul 1, 2025
09dac15
Added in the PR: 2562
ddiazccrz Jul 2, 2025
a4f9d6f
Refactored onblur and onChange to be functions
ddiazccrz Jul 2, 2025
71f3090
Update packages/template-retail-react-app/app/pages/cart/partials/bon…
ddiazccrz Jul 2, 2025
09b5ccf
Update packages/template-retail-react-app/app/mocks/mock-data.js
ddiazccrz Jul 3, 2025
755f63f
Code Review Changes
ddiazccrz Jul 3, 2025
adb5603
Fixed unit test
ddiazccrz Jul 3, 2025
c6e15a9
Merge remote-tracking branch 'origin/feature/auto-bonus-product' into…
ddiazccrz Jul 4, 2025
facc6d0
Fixed lint
ddiazccrz Jul 4, 2025
5f37a75
Merge branch 'develop' into feature/auto-bonus-product
sf-deepali-bharmal Jul 7, 2025
3c2b693
Merge branch 'develop' into feature/auto-bonus-product
sf-deepali-bharmal Jul 7, 2025
99e8c91
Merge branch 'develop' into feature/auto-bonus-product
ddiazccrz Jul 8, 2025
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
1 change: 1 addition & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Added support for Buy Online Pick up In Store (BOPIS) [#2646](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2646)
- Load active data scripts on demand only [#2623](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2623)
- Provide base image for convenient perf optimizations [#2642](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2642)
- Show Bonus Products on Cart Page [#2547](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2547)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we have different kind of bonus ( I think there will be more type of bonus coming in?), how about we make clearer?. Show auto bonus....


## v6.1.0 (May 22, 2025)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import {getDisplayVariationValues} from '@salesforce/retail-react-app/app/utils/
* In the context of a cart product item variant, this component renders a styled
* list of the selected variation values as well as any promos (w/ info popover).
*/
const ItemAttributes = ({includeQuantity, currency, ...props}) => {
const ItemAttributes = ({includeQuantity, currency, excludeBonusLabel, ...props}) => {
const variant = useItemVariant()
const {data: basket} = useCurrentBasket()
const {currency: activeCurrency} = useCurrency()
const promotionIds = variant.priceAdjustments?.map((adj) => adj.promotionId) ?? []
const intl = useIntl()

const displayBonusProductLabel = !excludeBonusLabel && variant?.bonusProductLineItem

// Fetch all the promotions given by price adjustments. We display this info in
// the promotion info popover when applicable.
const {data: res} = usePromotions(
Expand Down Expand Up @@ -91,6 +93,15 @@ const ItemAttributes = ({includeQuantity, currency, ...props}) => {

return (
<Stack spacing={1.5} flex={1} {...props}>
{displayBonusProductLabel && (
<Text lineHeight={1} color="black.600" fontSize="sm">
<FormattedMessage
defaultMessage="Bonus Product"
id="item_attributes.label.bonus_product"
/>
</Text>
)}

{variationValues &&
Object.keys(variationValues).map((key) => (
<Text
Expand Down Expand Up @@ -210,7 +221,8 @@ const ItemAttributes = ({includeQuantity, currency, ...props}) => {

ItemAttributes.propTypes = {
includeQuantity: PropTypes.bool,
currency: PropTypes.string
currency: PropTypes.string,
excludeBonusLabel: PropTypes.bool
}

export default ItemAttributes
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,80 @@ test('component renders product bundles without variant data', async () => {
})
})
})

// Helper function to render the component with a given variant and props
const renderComponent = (variant, props = {}) => {
renderWithProviders(
<ItemVariantProvider variant={variant}>
<ItemAttributes {...props} />
</ItemVariantProvider>
)
}

test('renders Bonus Product when bonusProductLineItem is true', async () => {
Copy link
Contributor

@alexvuong alexvuong Jul 3, 2025

Choose a reason for hiding this comment

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

Let's group all of these tests under a

describe('bonus product', () => {
  test('test', () => {})
})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

const mockVariantWithBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: true // Simulate the bonus product flag
}

// Case 1: excludeBonusLabel is true
renderComponent(mockVariantWithBonusProduct, {excludeBonusLabel: true})

await waitFor(() => {
expect(screen.queryByText(/Bonus Product/i)).not.toBeInTheDocument() // Fixed assertion
})

// Case 3: excludeBonusLabel is false
renderComponent(mockVariantWithBonusProduct, {excludeBonusLabel: false})

await waitFor(() => {
expect(screen.getByText(/Bonus Product/i)).toBeInTheDocument()
})
})

test('renders Bonus Product when excludeBonusLabel is not set', async () => {
// Case 1: Variant has no bonus product
const mockVariantWithOutBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: false // Simulate the bonus product flag
}

renderComponent(mockVariantWithOutBonusProduct)

await waitFor(() => {
expect(screen.queryByText(/Bonus Product/i)).not.toBeInTheDocument() // Fixed assertion
})

// Case 2: Variant has bonus product
const mockVariantWithBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: true // Simulate the bonus product flag
}

renderComponent(mockVariantWithBonusProduct)

await waitFor(() => {
expect(screen.queryByText(/Bonus Product/i)).toBeInTheDocument() // Fixed assertion
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need these comments after assertioon?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nope

})
})

test('does not render Bonus Product when bonusProductLineItem is false', async () => {
const mockVariantWithoutBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: false // Simulate the absence of the bonus product flag
}

// Case 1: excludeBonusLabel is true
renderComponent(mockVariantWithoutBonusProduct, {excludeBonusLabel: true})

await waitFor(() => {
expect(screen.queryByText(/Bonus Product/i)).not.toBeInTheDocument()
})

// Case 2: excludeBonusLabel is false
renderComponent(mockVariantWithoutBonusProduct, {excludeBonusLabel: false})

await waitFor(() => {
expect(screen.queryByText(/Bonus Product/i)).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025, salesforce.com, 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 from 'react'
import PropTypes from 'prop-types'
import {FormattedMessage, useIntl} from 'react-intl'
import {Text} from '@salesforce/retail-react-app/app/components/shared/ui'

const BonusProductQuantity = ({product}) => {
const intl = useIntl()
return (
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if product is undefined. How about we wrap it in Skeleton component when product is still undefined? https://v2.chakra-ui.com/docs/components/skeleton

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexvuong done

<Text
fontSize="sm"
color="gray.700"
aria-label={intl.formatMessage(
{
id: 'item_variant.quantity.label',
defaultMessage:
'Quantity selector for {productName}. Selected quantity is {quantity}'
},
{
quantity: product?.quantity,
productName: product?.name
}
)}
>
<FormattedMessage
defaultMessage="Quantity: {quantity}"
id="bonus_product_item.label.quantity"
values={{quantity: product?.quantity}}
/>
</Text>
)
}

BonusProductQuantity.propTypes = {
product: PropTypes.object.isRequired
}

export default BonusProductQuantity
Copy link
Contributor

Choose a reason for hiding this comment

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

This is great, I love small components with a very specific purpose.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025, salesforce.com, 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 from 'react'
import {render, screen} from '@testing-library/react'
import {IntlProvider} from 'react-intl'
import BonusProductQuantity from '@salesforce/retail-react-app/app/components/product-item/bonus-product-quantity'

const mockProduct = {quantity: 1}

const renderWithIntl = (component) =>
render(
<IntlProvider locale="en" defaultLocale="en">
{component}
</IntlProvider>
)

describe('BonusProductQuantity', () => {
test('renders the quantity text', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a test when skeleton is rendering

Copy link
Contributor Author

Choose a reason for hiding this comment

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

skeleton test added

renderWithIntl(<BonusProductQuantity product={mockProduct} />)
expect(screen.getByText(/Quantity: 1/)).toBeInTheDocument()
})

test('applies correct aria-label', () => {
renderWithIntl(<BonusProductQuantity product={mockProduct} />)
const quantityElement = screen.getByText(/Quantity: 1/)
expect(quantityElement).toHaveAttribute('aria-label')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,9 @@
*/
import React from 'react'
import PropTypes from 'prop-types'
import {FormattedMessage, useIntl} from 'react-intl'

// Chakra Components
import {
Box,
Fade,
Flex,
Stack,
Text,
VisuallyHidden
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {Box, Fade, Flex, Stack, Text} from '@salesforce/retail-react-app/app/components/shared/ui'

// Project Components
import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
Expand All @@ -26,7 +18,8 @@ import CartItemVariantName from '@salesforce/retail-react-app/app/components/ite
import CartItemVariantAttributes from '@salesforce/retail-react-app/app/components/item-variant/item-attributes'
import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/item-variant/item-price'
import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
import QuantityPicker from '@salesforce/retail-react-app/app/components/quantity-picker'
import BonusProductQuantity from '@salesforce/retail-react-app/app/components/product-item/bonus-product-quantity'
import ProductQuantityPicker from '@salesforce/retail-react-app/app/components/product-item/product-quantity-picker'

// Utilities
import {noop} from '@salesforce/retail-react-app/app/utils/utils'
Expand All @@ -53,7 +46,6 @@ const ProductItem = ({
const {stepQuantity, showInventoryMessage, inventoryMessage, quantity, setQuantity} =
useDerivedProduct(product)
const {currency: activeCurrency} = useCurrency()
const intl = useIntl()
return (
<Box
position="relative"
Expand All @@ -67,7 +59,7 @@ const ProductItem = ({
<Stack spacing={3} flex={1}>
<Stack spacing={1}>
<CartItemVariantName />
<CartItemVariantAttributes />
<CartItemVariantAttributes excludeBonusLabel />
<HideOnDesktop>
<Box marginTop={2}>
<CartItemVariantPrice
Expand All @@ -80,67 +72,17 @@ const ProductItem = ({

<Flex align="flex-end" justify="space-between">
<Stack spacing={1}>
<Text
fontSize="sm"
color="gray.700"
aria-label={intl.formatMessage(
{
id: 'item_variant.quantity.label',
defaultMessage:
'Quantity selector for {productName}. Selected quantity is {quantity}'
},
{
quantity: product?.quantity,
productName: product?.name
}
)}
>
<FormattedMessage
defaultMessage="Quantity:"
id="product_item.label.quantity"
{product.bonusProductLineItem ? (
<BonusProductQuantity product={product} />
) : (
<ProductQuantityPicker
product={product}
onItemQuantityChange={onItemQuantityChange}
stepQuantity={stepQuantity}
quantity={quantity}
setQuantity={setQuantity}
Copy link
Contributor

Choose a reason for hiding this comment

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

Great job on separating these logic into different components. 👍

/>
</Text>
<QuantityPicker
step={stepQuantity}
value={quantity}
min={0}
clampValueOnBlur={false}
onBlur={(e) => {
// Default to last known quantity if a user leaves the box with an invalid value
const {value} = e.target

if (!value) {
setQuantity(product.quantity)
}
}}
onChange={(stringValue, numberValue) => {
// Set the Quantity of product to value of input if value number
if (numberValue >= 0) {
// Call handler
onItemQuantityChange(numberValue).then(
(isValidChange) =>
isValidChange && setQuantity(numberValue)
)
} else if (stringValue === '') {
// We want to allow the use to clear the input to start a new input so here we set the quantity to '' so NAN is not displayed
// User will not be able to add '' quantity to the cart due to the add to cart button enablement rules
setQuantity(stringValue)
}
}}
productName={product?.name}
/>
<VisuallyHidden role="status">
{product?.name}
{intl.formatMessage(
{
id: 'item_variant.assistive_msg.quantity',
defaultMessage: 'Quantity {quantity}'
},
{
quantity: product?.quantity
}
)}
</VisuallyHidden>
)}
</Stack>
<Stack>
<HideOnMobile>
Expand Down
Loading
Loading