Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -12,6 +12,7 @@
- Support saving default shipping address on user registration from order confirmation [#2706](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2706)
- Minor updates to support BOPIS E2E tests [#2716](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2716)
- Provide support for partial hydration [#2696](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2696)
- Show Automatic Bonus Products on Cart Page [#2704](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2704)


## 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,75 @@ test('component renders product bundles without variant data', async () => {
})
})
})

const renderComponent = (variant, props = {}) => {
renderWithProviders(
<ItemVariantProvider variant={variant}>
<ItemAttributes {...props} />
</ItemVariantProvider>
)
}

describe('bonus product', () => {
test('renders Bonus Product when bonusProductLineItem is true', async () => {
const mockVariantWithBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: true
}

renderComponent(mockVariantWithBonusProduct, {excludeBonusLabel: true})

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

renderComponent(mockVariantWithBonusProduct, {excludeBonusLabel: false})

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

test('renders Bonus Product when excludeBonusLabel is not set', async () => {
const mockVariantWithOutBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: false
}

renderComponent(mockVariantWithOutBonusProduct)

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

const mockVariantWithBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: true
}

renderComponent(mockVariantWithBonusProduct)

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

test('does not render Bonus Product when bonusProductLineItem is false', async () => {
const mockVariantWithoutBonusProduct = {
...mockBundledProductItemsVariant,
bonusProductLineItem: false
}

renderComponent(mockVariantWithoutBonusProduct, {excludeBonusLabel: true})

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

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,45 @@
/*
* 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, Skeleton} 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

<Skeleton isLoaded={product}>
<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>
</Skeleton>
)
}

BonusProductQuantity.propTypes = {
product: PropTypes.object
}

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,38 @@
/*
* 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')
})

test('renders skeleton when product is undefined', () => {
renderWithIntl(<BonusProductQuantity product={undefined} />)
// The Skeleton component from Chakra UI renders a div with class "chakra-skeleton"
expect(document.querySelector('.chakra-skeleton')).toBeInTheDocument()
})
})
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