Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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 @@ -11,6 +11,7 @@
- Verify einstein activity with standard product [#2650](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2650)
- Support standard product as a child item in sets [#2636](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2636)
- Load active data scripts on demand only [#2623](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2623)
- Refactor Add to Cart in PDP [#2664](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2664)

## v6.1.0 (May 22, 2025)

Expand Down
154 changes: 41 additions & 113 deletions packages/template-retail-react-app/app/pages/product-detail/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ import {rebuildPathWithParams} from '@salesforce/retail-react-app/app/utils/url'
import {useHistory, useLocation, useParams} from 'react-router-dom'
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
import {useWishList} from '@salesforce/retail-react-app/app/hooks/use-wish-list'
import {
handleAddToCart,
handleProductBundleAddToCart,
handleProductSetAddToCart
} from '@salesforce/retail-react-app/app/utils/cart-utils'

const ProductDetail = () => {
const {formatMessage} = useIntl()
Expand Down Expand Up @@ -298,35 +303,6 @@ const ProductDetail = () => {
})
}

const handleAddToCart = async (productSelectionValues) => {
try {
const productItems = productSelectionValues.map(({variant, product, quantity}) => ({
productId: variant?.productId || product?.id,
price: variant?.price || product?.price,
quantity
}))

await addItemToNewOrExistingBasket(productItems)

const productItemsForEinstein = productSelectionValues.map(
({product, variant, quantity}) => ({
product,
productId: variant?.productId || product?.id,
price: variant?.price || product?.price,
quantity
})
)
einstein.sendAddToCart(productItemsForEinstein)

// If the items were successfully added, set the return value to be used
// by the add to cart modal.
return productSelectionValues
} catch (error) {
console.log('error', error)
showError(error)
}
}

/**************** Product Set/Bundles Handlers ****************/
const handleChildProductValidation = useCallback(() => {
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 to move this as well ? If Yes, this can go in some util file.
Others can go in some AddToCart hook/or something, as they are all Adding diff products to cart.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

handleChildProductValidation - Moved it back to product-detail since it didn't make sense to extract this one to the util

// Run validation for all child products. This will ensure the error
Expand Down Expand Up @@ -365,80 +341,6 @@ const ProductDetail = () => {
return true
}, [product, childProductSelection])

/**************** Product Set Handlers ****************/
const handleProductSetAddToCart = () => {
// Get all the selected products, and pass them to the addToCart handler which
// accepts an array.
const productSelectionValues = Object.values(childProductSelection)
return handleAddToCart(productSelectionValues)
}

/**************** Product Bundle Handlers ****************/
// Top level bundle does not have variants
const handleProductBundleAddToCart = async (variant, selectedQuantity) => {
try {
const childProductSelections = Object.values(childProductSelection)

const productItems = [
{
productId: product.id,
price: product.price,
quantity: selectedQuantity,
// The add item endpoint in the shopper baskets API does not respect variant selections
// for bundle children, so we have to make a follow up call to update the basket
// with the chosen variant selections
bundledProductItems: childProductSelections.map((child) => {
return {
productId: child.variant?.productId || child.product?.id,
quantity: child.quantity
}
})
}
]

const res = await addItemToNewOrExistingBasket(productItems)

const bundleChildMasterIds = childProductSelections.map((child) => {
return child.product.id
})

// since the returned data includes all products in basket
// here we compare list of productIds in bundleProductItems of each productItem to filter out the
// current bundle that was last added into cart
const currentBundle = res.productItems.find((productItem) => {
if (!productItem.bundledProductItems?.length) return
const bundleChildIds = productItem.bundledProductItems?.map((item) => {
// seek out the bundle child that still uses masterId as product id
return item.productId
})
return bundleChildIds.every((id) => bundleChildMasterIds.includes(id))
})

const itemsToBeUpdated = getUpdateBundleChildArray(
currentBundle,
childProductSelections
)

if (itemsToBeUpdated.length) {
// make a follow up call to update child variant selection for product bundle
// since add item endpoint doesn't currently consider product bundle child variants
await updateItemsInBasketMutation.mutateAsync({
method: 'PATCH',
parameters: {
basketId: res.basketId
},
body: itemsToBeUpdated
})
}

einstein.sendAddToCart(productItems)

return childProductSelections
} catch (error) {
showError(error)
}
}

/**************** Einstein ****************/
useEffect(() => {
if (product && product.type.set) {
Expand Down Expand Up @@ -498,8 +400,24 @@ const ProductDetail = () => {
category={primaryCategory?.parentCategoryTree || []}
addToCart={
isProductASet
? handleProductSetAddToCart
: handleProductBundleAddToCart
? () =>
handleProductSetAddToCart(
childProductSelection,
addItemToNewOrExistingBasket,
einstein,
showError
)
: (variant, selectedQuantity) =>
handleProductBundleAddToCart(
product,
childProductSelection,
selectedQuantity,
addItemToNewOrExistingBasket,
updateItemsInBasketMutation,
einstein,
showError,
getUpdateBundleChildArray
)
}
addToWishlist={handleAddToWishlist}
isProductLoading={isProductLoading}
Expand Down Expand Up @@ -536,13 +454,18 @@ const ProductDetail = () => {
addToCart={
isProductASet
? (variant, quantity) =>
handleAddToCart([
{
product: childProduct,
variant,
quantity
}
])
handleAddToCart(
[
{
product: childProduct,
variant,
quantity
}
],
addItemToNewOrExistingBasket,
einstein,
showError
)
: null
}
addToWishlist={
Expand Down Expand Up @@ -589,7 +512,12 @@ const ProductDetail = () => {
product={product}
category={primaryCategory?.parentCategoryTree || []}
addToCart={(variant, quantity) =>
handleAddToCart([{product, variant, quantity}])
handleAddToCart(
[{product, variant, quantity}],
addItemToNewOrExistingBasket,
einstein,
showError
)
}
addToWishlist={handleAddToWishlist}
isProductLoading={isProductLoading}
Expand Down
154 changes: 154 additions & 0 deletions packages/template-retail-react-app/app/utils/cart-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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
*/

/**
* Handles adding products to cart and sending data to Einstein.
Copy link
Contributor

Choose a reason for hiding this comment

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

@sf-kyle-wright The WI description mentioned one reason to move addToCart out is it can be used from multiple clients like PDP, wishlist etc.
Then utils/cart-utils does not seem the right place to me.
Should this be a hook or something like that instead ?
Hooks can contain logic for data fetching, state management, UI behavior etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya so that's a great thought. There are some tradeoffs here but it looks like at this point it might not make sense to move them into a hook. I asked cursor, TLDR:

Summary:
It doesn't strictly make sense to turn these into a hook unless you want to manage state or context, or provide a more React-friendly API. If you do, a hook can be a good abstraction, but the current logic itself doesn't require it.

cursor_should_i_convert_util_functions.md

Copy link
Contributor

Choose a reason for hiding this comment

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

since this is only working on cart? How about we keep it in cart page? Unless we have plan for these funcs to be used by other components that is not cart

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a larger plan to re-use these functions across wishlist as well

Copy link
Contributor

Choose a reason for hiding this comment

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

if that is the case, let's name the file to be a bit more generic.

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 What do you have in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

add-to-cart-utils?

Copy link
Contributor

Choose a reason for hiding this comment

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

I like that name @sf-emmyzhang

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks. renamed to add-to-cart-utils.js

* @param {Array} productSelectionValues
* @param {Function} addItemToNewOrExistingBasket
* @param {Object} einstein
* @param {Function} showError
* @returns {Promise<Array>|undefined}
*/
export const handleAddToCart = async (
productSelectionValues,
addItemToNewOrExistingBasket,
einstein,
showError
) => {
try {
const productItems = productSelectionValues.map(({variant, product, quantity}) => ({
productId: variant?.productId || product?.id,
price: variant?.price || product?.price,
quantity
}))

await addItemToNewOrExistingBasket(productItems)

const productItemsForEinstein = productSelectionValues.map(
({product, variant, quantity}) => ({
product,
productId: variant?.productId || product?.id,
price: variant?.price || product?.price,
quantity
})
)
einstein.sendAddToCart(productItemsForEinstein)

// If the items were successfully added, set the return value to be used
// by the add to cart modal.
return productSelectionValues
} catch (error) {
console.log('error', error)
showError(error)
}
}

/**
* Handles adding a product bundle to the cart, including updating child variant selections if needed.
* @param {Object} product - The parent product (bundle).
* @param {Object} childProductSelection - Object containing selected child products.
* @param {number} selectedQuantity - Quantity of the bundle to add.
* @param {Function} addItemToNewOrExistingBasket - Function to add items to the basket.
* @param {Object} updateItemsInBasketMutation - Mutation object for updating items in the basket.
* @param {Object} einstein - Einstein tracking object.
* @param {Function} showError - Function to show errors.
* @param {Function} getUpdateBundleChildArray - Utility to get update array for bundle children.
* @returns {Promise<Array>|undefined}
*/
export const handleProductBundleAddToCart = async (
product,
childProductSelection,
selectedQuantity,
addItemToNewOrExistingBasket,
updateItemsInBasketMutation,
einstein,
showError,
getUpdateBundleChildArray
) => {
try {
const childProductSelections = Object.values(childProductSelection)

const productItems = [
{
productId: product.id,
price: product.price,
quantity: selectedQuantity,
// The add item endpoint in the shopper baskets API does not respect variant selections
// for bundle children, so we have to make a follow up call to update the basket
// with the chosen variant selections
bundledProductItems: childProductSelections.map((child) => {
return {
productId: child.variant?.productId || child.product?.id,
quantity: child.quantity
}
})
}
]

const res = await addItemToNewOrExistingBasket(productItems)

const bundleChildMasterIds = childProductSelections.map((child) => {
return child.product.id
})

// since the returned data includes all products in basket
// here we compare list of productIds in bundleProductItems of each productItem to filter out the
// current bundle that was last added into cart
const currentBundle = res.productItems.find((productItem) => {
if (!productItem.bundledProductItems?.length) return
const bundleChildIds = productItem.bundledProductItems?.map((item) => {
// seek out the bundle child that still uses masterId as product id
return item.productId
})
return bundleChildIds.every((id) => bundleChildMasterIds.includes(id))
})

const itemsToBeUpdated = getUpdateBundleChildArray(currentBundle, childProductSelections)

if (itemsToBeUpdated.length) {
// make a follow up call to update child variant selection for product bundle
// since add item endpoint doesn't currently consider product bundle child variants
await updateItemsInBasketMutation.mutateAsync({
method: 'PATCH',
parameters: {
basketId: res.basketId
},
body: itemsToBeUpdated
})
}

einstein.sendAddToCart(productItems)

return childProductSelections
} catch (error) {
showError(error)
}
}

/**
* Handles adding a product set to the cart.
* @param {Object} childProductSelection - Object containing selected child products.
* @param {Function} addItemToNewOrExistingBasket - Function to add items to the basket.
* @param {Object} einstein - Einstein tracking object.
* @param {Function} showError - Function to show errors.
* @returns {Promise<Array>|undefined}
*/
export const handleProductSetAddToCart = (
childProductSelection,
addItemToNewOrExistingBasket,
einstein,
showError
) => {
// Get all the selected products, and pass them to the addToCart handler which accepts an array.
const productSelectionValues = Object.values(childProductSelection)
return handleAddToCart(
productSelectionValues,
addItemToNewOrExistingBasket,
einstein,
showError
)
}
Loading