diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index 54f2244f0f..93a47050f8 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -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) diff --git a/packages/template-retail-react-app/app/pages/product-detail/index.jsx b/packages/template-retail-react-app/app/pages/product-detail/index.jsx index b7fa481a9b..476ca7821f 100644 --- a/packages/template-retail-react-app/app/pages/product-detail/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-detail/index.jsx @@ -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/add-to-cart-utils' const ProductDetail = () => { const {formatMessage} = useIntl() @@ -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(() => { // Run validation for all child products. This will ensure the error @@ -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) { @@ -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} @@ -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={ @@ -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} diff --git a/packages/template-retail-react-app/app/utils/add-to-cart-utils.js b/packages/template-retail-react-app/app/utils/add-to-cart-utils.js new file mode 100644 index 0000000000..dd675db60a --- /dev/null +++ b/packages/template-retail-react-app/app/utils/add-to-cart-utils.js @@ -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. + * @param {Array} productSelectionValues + * @param {Function} addItemToNewOrExistingBasket + * @param {Object} einstein + * @param {Function} showError + * @returns {Promise|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|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|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 + ) +}