From 8109de6b96fe9caf4e06655c8a4ea5d3da4b9d22 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:51:18 -0400 Subject: [PATCH 1/9] add a new modal --- .../components/_app/partials/app-layout.jsx | 51 +++---- .../src/hooks/use-add-to-cart-modal.js | 70 +++++++--- .../src/hooks/use-bonus-product-modal.js | 125 ++++++++++++++++++ .../src/hooks/use-bonus-product-modal.test.js | 90 +++++++++++++ 4 files changed, 291 insertions(+), 45 deletions(-) create mode 100644 packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.js create mode 100644 packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js diff --git a/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx b/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx index b0657a65f2..ad239d3487 100644 --- a/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx +++ b/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx @@ -13,6 +13,7 @@ import ScrollToTop from '../../scroll-to-top' import OfflineBanner from '../../offline-banner' import OfflineBoundary from '../../offline-boundary' import {AddToCartModalProvider} from '../../../hooks/use-add-to-cart-modal' +import {BonusProductModalProvider} from '../../../hooks/use-bonus-product-modal' /** * AppLayout component that provides the main layout structure @@ -37,33 +38,35 @@ const AppLayout = ({ {/* Offline Banner */} {isOnline === false && } - - - + + - {children} - - + + {children} + + - {/* Footer */} - {footerComponent} + {/* Footer */} + {footerComponent} - {/* Modals */} - {modalsComponent} - + {/* Modals */} + {modalsComponent} + + ) diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js index d8c9d9a03d..eb8d9c3cc4 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js @@ -21,6 +21,7 @@ import { useBreakpointValue } from '@chakra-ui/react' import {useCurrentBasket} from './use-current-basket' +import {usePromotions} from '@salesforce/commerce-sdk-react' import Link from '../components/link' import RecommendedProducts from '../components/recommended-products' import {LockIcon} from '../components/icons' @@ -29,6 +30,7 @@ import {getPriceData, getDisplayVariationValues} from '../utils/product-utils' import {EINSTEIN_RECOMMENDERS} from '../../config/constants' import DisplayPrice from '../components/display-price' import SafePortal from '../components/safe-portal' +import {useBonusProductModalContext} from './use-bonus-product-modal' /** * This is the context for managing the AddToCartModal. @@ -52,10 +54,24 @@ AddToCartModalProvider.propTypes = { /** * Visual feedback (a modal) for adding item to the cart. */ -export const AddToCartModal = () => { +export const AddToCartModal = ({ onSelectBonusProductsClick }) => { const {isOpen, onClose, data} = useAddToCartModalContext() - const {product, itemsAdded = [], selectedQuantity} = data || {} + const bonusProductContext = useBonusProductModalContext() + const {onOpen: onOpenBonusModal} = bonusProductContext || {} + const {product, itemsAdded = [], selectedQuantity, bonusDiscountLineItems = []} = data || {} const isProductABundle = !!product?.type.bundle + + // Extract unique promotion IDs + const promotionIds = [...new Set( + bonusDiscountLineItems.map(item => item.promotionId).filter(Boolean) + )]; + // Fetch promotion details + const { data: promotions, isLoading: isPromotionsLoading } = usePromotions( + { parameters: { ids: promotionIds.join(',') } }, + { enabled: promotionIds.length > 0 } + ); + // Get the first promotion's details + const promotionText = promotions?.data?.[0]?.details || ''; const intl = useIntl() const {formatMessage} = intl @@ -332,25 +348,36 @@ export const AddToCartModal = () => { ) })} - {/* TODO: replace with text fetched from promotion */} - - {'Bonus products available!'} - - + {bonusDiscountLineItems && bonusDiscountLineItems.length > 0 && ( + <> + + {promotionText} + + + + )} useContext(BonusProductModalContext) + +export const BonusProductModalProvider = ({children}) => { + const bonusProductModal = useBonusProductModal() + return ( + + {children} + + + ) +} + +BonusProductModalProvider.propTypes = { + children: PropTypes.node.isRequired +} + +/** + * Modal for selecting from available bonus products. + */ +export const BonusProductModal = () => { + const {isOpen, onClose, data} = useBonusProductModalContext() + const size = useBreakpointValue({base: 'full', lg: 'lg', xl: 'xl'}) + + if (!isOpen) { + return null + } + + // todo: this component will be replaced in the next work item. The component will display bonus products available for selection. + return ( + + + + + + + Bonus Product Modal + + {data && ( + + + Received Data: + + + {JSON.stringify(data, null, 2)} + + + )} + + + + + + + + ) +} + +export const useBonusProductModal = () => { + const [state, setState] = useState({ + isOpen: false, + data: null + }) + + const {pathname} = useLocation() + useEffect(() => { + if (state.isOpen) { + setState({ + ...state, + isOpen: false + }) + } + }, [pathname]) + + return { + isOpen: state.isOpen, + data: state.data, + onOpen: (data) => { + setState({ + isOpen: true, + data + }) + }, + onClose: () => { + setState({ + isOpen: false, + data: null + }) + } + } +} \ No newline at end of file diff --git a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js b/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js new file mode 100644 index 0000000000..bbedffc2ce --- /dev/null +++ b/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js @@ -0,0 +1,90 @@ +/* + * 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 {screen, act} from '@testing-library/react' +import {renderWithChakraProvider} from '../utils/test-utils' +import {useBonusProductModal, BonusProductModalProvider, useBonusProductModalContext} from './use-bonus-product-modal' + +// Mock react-router-dom +jest.mock('react-router-dom', () => ({ + useLocation: () => ({pathname: '/test'}) +})) + +const BonusProductSelectionModal = () => { + const {isOpen, onOpen, onClose, data} = useBonusProductModalContext() + + return ( +
+
{isOpen.toString()}
+
{JSON.stringify(data)}
+ + +
+ ) +} + +describe('useBonusProductModal', () => { + it('should provide initial state', () => { + const TestHook = () => { + const modal = useBonusProductModal() + return ( +
+
{modal.isOpen.toString()}
+
{JSON.stringify(modal.data)}
+
+ ) + } + + renderWithChakraProvider() + + expect(screen.getByTestId('is-open')).toHaveTextContent('false') + expect(screen.getByTestId('data')).toHaveTextContent('null') + }) + + it('should open modal with data', async () => { + renderWithChakraProvider( + + + + ) + + const openButton = screen.getByTestId('open-button') + await act(async () => { + openButton.click() + }) + + expect(screen.getByTestId('is-open')).toHaveTextContent('true') + expect(screen.getByTestId('data')).toHaveTextContent('{"test":"data"}') + }) + + it('should close modal', async () => { + renderWithChakraProvider( + + + + ) + + const openButton = screen.getByTestId('open-button') + const closeButton = screen.getByTestId('close-button') + + await act(async () => { + openButton.click() + }) + expect(screen.getByTestId('is-open')).toHaveTextContent('true') + + await act(async () => { + closeButton.click() + }) + expect(screen.getByTestId('is-open')).toHaveTextContent('false') + expect(screen.getByTestId('data')).toHaveTextContent('null') + }) +}) \ No newline at end of file From b2607ac095797475b31db54843a6051e5be1d901 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:12:14 -0400 Subject: [PATCH 2/9] lint fix --- .../src/hooks/use-add-to-cart-modal.js | 86 ++++++++++--------- .../src/hooks/use-bonus-product-modal.js | 16 +--- .../src/hooks/use-bonus-product-modal.test.js | 16 ++-- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js index eb8d9c3cc4..a7ff59bce6 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js @@ -54,24 +54,24 @@ AddToCartModalProvider.propTypes = { /** * Visual feedback (a modal) for adding item to the cart. */ -export const AddToCartModal = ({ onSelectBonusProductsClick }) => { +export const AddToCartModal = ({onSelectBonusProductsClick}) => { const {isOpen, onClose, data} = useAddToCartModalContext() const bonusProductContext = useBonusProductModalContext() const {onOpen: onOpenBonusModal} = bonusProductContext || {} const {product, itemsAdded = [], selectedQuantity, bonusDiscountLineItems = []} = data || {} const isProductABundle = !!product?.type.bundle - + // Extract unique promotion IDs - const promotionIds = [...new Set( - bonusDiscountLineItems.map(item => item.promotionId).filter(Boolean) - )]; + const promotionIds = [ + ...new Set(bonusDiscountLineItems.map((item) => item.promotionId).filter(Boolean)) + ] // Fetch promotion details - const { data: promotions, isLoading: isPromotionsLoading } = usePromotions( - { parameters: { ids: promotionIds.join(',') } }, - { enabled: promotionIds.length > 0 } - ); + const {data: promotions, isLoading: isPromotionsLoading} = usePromotions( + {parameters: {ids: promotionIds.join(',')}}, + {enabled: promotionIds.length > 0} + ) // Get the first promotion's details - const promotionText = promotions?.data?.[0]?.details || ''; + const promotionText = promotions?.data?.[0]?.details || '' const intl = useIntl() const {formatMessage} = intl @@ -348,36 +348,42 @@ export const AddToCartModal = ({ onSelectBonusProductsClick }) => { ) })} - {bonusDiscountLineItems && bonusDiscountLineItems.length > 0 && ( - <> - - {promotionText} - - - - )} + {bonusDiscountLineItems && + bonusDiscountLineItems.length > 0 && ( + <> + + {promotionText} + + + + )}
{ )} - @@ -122,4 +112,4 @@ export const useBonusProductModal = () => { }) } } -} \ No newline at end of file +} diff --git a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js b/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js index bbedffc2ce..309da74b63 100644 --- a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js +++ b/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js @@ -8,7 +8,11 @@ import React from 'react' import {screen, act} from '@testing-library/react' import {renderWithChakraProvider} from '../utils/test-utils' -import {useBonusProductModal, BonusProductModalProvider, useBonusProductModalContext} from './use-bonus-product-modal' +import { + useBonusProductModal, + BonusProductModalProvider, + useBonusProductModalContext +} from './use-bonus-product-modal' // Mock react-router-dom jest.mock('react-router-dom', () => ({ @@ -17,7 +21,7 @@ jest.mock('react-router-dom', () => ({ const BonusProductSelectionModal = () => { const {isOpen, onOpen, onClose, data} = useBonusProductModalContext() - + return (
{isOpen.toString()}
@@ -45,7 +49,7 @@ describe('useBonusProductModal', () => { } renderWithChakraProvider() - + expect(screen.getByTestId('is-open')).toHaveTextContent('false') expect(screen.getByTestId('data')).toHaveTextContent('null') }) @@ -75,16 +79,16 @@ describe('useBonusProductModal', () => { const openButton = screen.getByTestId('open-button') const closeButton = screen.getByTestId('close-button') - + await act(async () => { openButton.click() }) expect(screen.getByTestId('is-open')).toHaveTextContent('true') - + await act(async () => { closeButton.click() }) expect(screen.getByTestId('is-open')).toHaveTextContent('false') expect(screen.getByTestId('data')).toHaveTextContent('null') }) -}) \ No newline at end of file +}) From a625b64c1b72c1c3f8a4256405678e26b46ed355 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:59:21 -0400 Subject: [PATCH 3/9] update tests --- .../src/hooks/use-add-to-cart-modal.test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.test.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.test.js index 36bc5a57ea..f1cd12ab4b 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.test.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.test.js @@ -601,6 +601,11 @@ test('Renders AddToCartModal properly', () => { id: '701642811399M', quantity: 22 } + ], + bonusDiscountLineItems: [ + { + promotionId: 'ChoiceOfBonusProdect-ProductLevel-ruleBased' + } ] } @@ -621,9 +626,6 @@ test('Renders AddToCartModal properly', () => { const numOfRowsRendered = screen.getAllByTestId('product-added').length expect(numOfRowsRendered).toEqual(MOCK_DATA.itemsAdded.length) - // Check that the promotional message is displayed - expect(screen.getByText('Bonus products available!')).toBeInTheDocument() //todo: update tests after static text is removed - // Check that the "Select Bonus Products" button is displayed expect(screen.getByText('Select Bonus Products')).toBeInTheDocument() }) From c1b7f13381896083bae71c47ff4aed5550dccfac Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Wed, 13 Aug 2025 12:42:34 -0400 Subject: [PATCH 4/9] renamed to BonusProductSelectionModal --- .../components/_app/partials/app-layout.jsx | 6 ++-- .../src/hooks/index.js | 5 ++++ .../src/hooks/use-add-to-cart-modal.js | 4 +-- ...s => use-bonus-product-selection-modal.js} | 25 +++++++++-------- ...use-bonus-product-selection-modal.test.js} | 28 +++++++++---------- 5 files changed, 37 insertions(+), 31 deletions(-) rename packages/template-chakra-storefront/src/hooks/{use-bonus-product-modal.js => use-bonus-product-selection-modal.js} (78%) rename packages/template-chakra-storefront/src/hooks/{use-bonus-product-modal.test.js => use-bonus-product-selection-modal.test.js} (77%) diff --git a/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx b/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx index ad239d3487..50f1967181 100644 --- a/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx +++ b/packages/template-chakra-storefront/src/components/_app/partials/app-layout.jsx @@ -13,7 +13,7 @@ import ScrollToTop from '../../scroll-to-top' import OfflineBanner from '../../offline-banner' import OfflineBoundary from '../../offline-boundary' import {AddToCartModalProvider} from '../../../hooks/use-add-to-cart-modal' -import {BonusProductModalProvider} from '../../../hooks/use-bonus-product-modal' +import {BonusProductSelectionModalProvider} from '../../../hooks/use-bonus-product-selection-modal' /** * AppLayout component that provides the main layout structure @@ -38,7 +38,7 @@ const AppLayout = ({ {/* Offline Banner */} {isOnline === false && } - + - + ) diff --git a/packages/template-chakra-storefront/src/hooks/index.js b/packages/template-chakra-storefront/src/hooks/index.js index 39d310b7c2..84248bb433 100644 --- a/packages/template-chakra-storefront/src/hooks/index.js +++ b/packages/template-chakra-storefront/src/hooks/index.js @@ -18,3 +18,8 @@ export {useCurrency} from './use-currency' export {useCurrentCustomer} from './use-current-customer' export {useCurrentBasket} from './use-current-basket' export {useManualBonusProducts} from './use-manual-bonus-products' +export { + BonusProductSelectionModalProvider, + useBonusProductSelectionModalContext, + useBonusProductSelectionModal +} from './use-bonus-product-selection-modal' diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js index 11aa94f3e8..6d10ca4cd4 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js @@ -30,7 +30,7 @@ import {getPriceData, getDisplayVariationValues} from '../utils/product-utils' import {EINSTEIN_RECOMMENDERS} from '../../config/constants' import DisplayPrice from '../components/display-price' import SafePortal from '../components/safe-portal' -import {useBonusProductModalContext} from './use-bonus-product-modal' +import {useBonusProductSelectionModalContext} from './use-bonus-product-selection-modal' import {addToCartModalTheme} from '../theme/components/project/add-to-cart-modal' /** @@ -73,7 +73,7 @@ AddToCartModalProvider.propTypes = { */ export const AddToCartModal = ({onSelectBonusProductsClick}) => { const {isOpen, onClose, data} = useAddToCartModalContext() - const bonusProductContext = useBonusProductModalContext() + const bonusProductContext = useBonusProductSelectionModalContext() const {onOpen: onOpenBonusModal} = bonusProductContext || {} const {product, itemsAdded = [], selectedQuantity, bonusDiscountLineItems = []} = data || {} const isProductABundle = !!product?.type.bundle diff --git a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.js b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js similarity index 78% rename from packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.js rename to packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js index b850c1f1f2..a5bf667a89 100644 --- a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js @@ -11,31 +11,32 @@ import PropTypes from 'prop-types' import {Dialog, Button, Text, Box, useBreakpointValue} from '@chakra-ui/react' /** - * Context for managing the BonusProductModal. + * Context for managing the BonusProductSelectionModal. * Used in top level App component. */ -export const BonusProductModalContext = React.createContext() -export const useBonusProductModalContext = () => useContext(BonusProductModalContext) +export const BonusProductSelectionModalContext = React.createContext() +export const useBonusProductSelectionModalContext = () => + useContext(BonusProductSelectionModalContext) -export const BonusProductModalProvider = ({children}) => { - const bonusProductModal = useBonusProductModal() +export const BonusProductSelectionModalProvider = ({children}) => { + const bonusProductSelectionModal = useBonusProductSelectionModal() return ( - + {children} - - + + ) } -BonusProductModalProvider.propTypes = { +BonusProductSelectionModalProvider.propTypes = { children: PropTypes.node.isRequired } /** * Modal for selecting from available bonus products. */ -export const BonusProductModal = () => { - const {isOpen, onClose, data} = useBonusProductModalContext() +export const BonusProductSelectionModal = () => { + const {isOpen, onClose, data} = useBonusProductSelectionModalContext() const size = useBreakpointValue({base: 'full', lg: 'lg', xl: 'xl'}) if (!isOpen) { @@ -80,7 +81,7 @@ export const BonusProductModal = () => { ) } -export const useBonusProductModal = () => { +export const useBonusProductSelectionModal = () => { const [state, setState] = useState({ isOpen: false, data: null diff --git a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.test.js similarity index 77% rename from packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js rename to packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.test.js index 309da74b63..197ff43ccf 100644 --- a/packages/template-chakra-storefront/src/hooks/use-bonus-product-modal.test.js +++ b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.test.js @@ -9,18 +9,18 @@ import React from 'react' import {screen, act} from '@testing-library/react' import {renderWithChakraProvider} from '../utils/test-utils' import { - useBonusProductModal, - BonusProductModalProvider, - useBonusProductModalContext -} from './use-bonus-product-modal' + useBonusProductSelectionModal, + BonusProductSelectionModalProvider, + useBonusProductSelectionModalContext +} from './use-bonus-product-selection-modal' // Mock react-router-dom jest.mock('react-router-dom', () => ({ useLocation: () => ({pathname: '/test'}) })) -const BonusProductSelectionModal = () => { - const {isOpen, onOpen, onClose, data} = useBonusProductModalContext() +const BonusProductSelectionModalTest = () => { + const {isOpen, onOpen, onClose, data} = useBonusProductSelectionModalContext() return (
@@ -36,10 +36,10 @@ const BonusProductSelectionModal = () => { ) } -describe('useBonusProductModal', () => { +describe('useBonusProductSelectionModal', () => { it('should provide initial state', () => { const TestHook = () => { - const modal = useBonusProductModal() + const modal = useBonusProductSelectionModal() return (
{modal.isOpen.toString()}
@@ -56,9 +56,9 @@ describe('useBonusProductModal', () => { it('should open modal with data', async () => { renderWithChakraProvider( - - - + + + ) const openButton = screen.getByTestId('open-button') @@ -72,9 +72,9 @@ describe('useBonusProductModal', () => { it('should close modal', async () => { renderWithChakraProvider( - - - + + + ) const openButton = screen.getByTestId('open-button') From 10958a8bd4ee16b1c63a3bca7d26869ecfafd962 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:58:22 -0400 Subject: [PATCH 5/9] extract out button --- .../select-bonus-products-button/index.jsx | 60 +++++++++++++++++++ .../src/hooks/use-add-to-cart-modal.js | 31 +++------- 2 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx diff --git a/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx b/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx new file mode 100644 index 0000000000..53ed140435 --- /dev/null +++ b/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021, 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 {Button} from '@chakra-ui/react' +import {useIntl} from 'react-intl' + +const SelectBonusProductsButton = ({ + bonusDiscountLineItems, + product, + itemsAdded, + onOpenBonusModal, + onClose, + ...buttonProps +}) => { + const intl = useIntl() + + const handleClick = () => { + if (onOpenBonusModal) { + onOpenBonusModal({ + bonusDiscountLineItems, + product, + itemsAdded + }) + } + if (onClose) onClose() + } + + return ( + + ) +} + +SelectBonusProductsButton.propTypes = { + bonusDiscountLineItems: PropTypes.array, + product: PropTypes.object, + itemsAdded: PropTypes.array, + onOpenBonusModal: PropTypes.func, + onClose: PropTypes.func +} + +export default SelectBonusProductsButton \ No newline at end of file diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js index 6d10ca4cd4..673772921d 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js @@ -32,6 +32,7 @@ import DisplayPrice from '../components/display-price' import SafePortal from '../components/safe-portal' import {useBonusProductSelectionModalContext} from './use-bonus-product-selection-modal' import {addToCartModalTheme} from '../theme/components/project/add-to-cart-modal' +import SelectBonusProductsButton from '../components/select-bonus-products-button' /** * Local configuration for component-specific styling @@ -397,29 +398,13 @@ export const AddToCartModal = ({onSelectBonusProductsClick}) => { > {promotionText} - + )} From 606f912e083a8902bf458cadb4c0f2c82b1cadcf Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:31:36 -0400 Subject: [PATCH 6/9] color and padding fix --- .../src/components/select-bonus-products-button/index.jsx | 1 + .../src/hooks/use-bonus-product-selection-modal.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx b/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx index 53ed140435..494d45684d 100644 --- a/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx +++ b/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx @@ -35,6 +35,7 @@ const SelectBonusProductsButton = ({ onClick={handleClick} width="100%" variant="outline-gray" + color="blue.600" size="md" height={9} minWidth={11} diff --git a/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js index a5bf667a89..eb27f77fd4 100644 --- a/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js @@ -55,7 +55,7 @@ export const BonusProductSelectionModal = () => { - + Bonus Product Modal @@ -70,7 +70,7 @@ export const BonusProductSelectionModal = () => { )} - + From 90b1c1a3dc0f1853750fb56202a4305c51582622 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:35:49 -0400 Subject: [PATCH 7/9] extract reusable custom hook --- .../select-bonus-products-button/index.jsx | 2 +- .../src/hooks/use-add-to-cart-modal.js | 35 ++----------- .../use-bonus-product-selection-modal.js | 35 ++----------- .../src/hooks/use-modal-state.js | 50 +++++++++++++++++++ 4 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 packages/template-chakra-storefront/src/hooks/use-modal-state.js diff --git a/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx b/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx index 494d45684d..ac3fc9bffd 100644 --- a/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx +++ b/packages/template-chakra-storefront/src/components/select-bonus-products-button/index.jsx @@ -58,4 +58,4 @@ SelectBonusProductsButton.propTypes = { onClose: PropTypes.func } -export default SelectBonusProductsButton \ No newline at end of file +export default SelectBonusProductsButton diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js index 673772921d..a4a6f01dc2 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js @@ -33,6 +33,7 @@ import SafePortal from '../components/safe-portal' import {useBonusProductSelectionModalContext} from './use-bonus-product-selection-modal' import {addToCartModalTheme} from '../theme/components/project/add-to-cart-modal' import SelectBonusProductsButton from '../components/select-bonus-products-button' +import {useModalState} from './use-modal-state' /** * Local configuration for component-specific styling @@ -514,35 +515,9 @@ AddToCartModal.propTypes = { } export const useAddToCartModal = () => { - const [state, setState] = useState({ - isOpen: false, - data: null + const {isOpen, data, onOpen, onClose} = useModalState({ + closeOnRouteChange: true, + resetDataOnClose: true }) - - const {pathname} = useLocation() - useEffect(() => { - if (state.isOpen) { - setState({ - ...state, - isOpen: false - }) - } - }, [pathname]) - - return { - isOpen: state.isOpen, - data: state.data, - onOpen: (data) => { - setState({ - isOpen: true, - data - }) - }, - onClose: () => { - setState({ - isOpen: false, - data: null - }) - } - } + return {isOpen, data, onOpen, onClose} } diff --git a/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js index eb27f77fd4..2d24a866a7 100644 --- a/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-bonus-product-selection-modal.js @@ -9,6 +9,7 @@ import React, {useContext, useState, useEffect} from 'react' import {useLocation} from 'react-router-dom' import PropTypes from 'prop-types' import {Dialog, Button, Text, Box, useBreakpointValue} from '@chakra-ui/react' +import {useModalState} from './use-modal-state' /** * Context for managing the BonusProductSelectionModal. @@ -82,35 +83,9 @@ export const BonusProductSelectionModal = () => { } export const useBonusProductSelectionModal = () => { - const [state, setState] = useState({ - isOpen: false, - data: null + const {isOpen, data, onOpen, onClose} = useModalState({ + closeOnRouteChange: true, + resetDataOnClose: true }) - - const {pathname} = useLocation() - useEffect(() => { - if (state.isOpen) { - setState({ - ...state, - isOpen: false - }) - } - }, [pathname]) - - return { - isOpen: state.isOpen, - data: state.data, - onOpen: (data) => { - setState({ - isOpen: true, - data - }) - }, - onClose: () => { - setState({ - isOpen: false, - data: null - }) - } - } + return {isOpen, data, onOpen, onClose} } diff --git a/packages/template-chakra-storefront/src/hooks/use-modal-state.js b/packages/template-chakra-storefront/src/hooks/use-modal-state.js new file mode 100644 index 0000000000..1c6b02593c --- /dev/null +++ b/packages/template-chakra-storefront/src/hooks/use-modal-state.js @@ -0,0 +1,50 @@ +/* + * 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 {useEffect, useState} from 'react' +import {useLocation} from 'react-router-dom' + +/** + * Reusable modal state hook + * - Manages isOpen and optional data payload + * - Provides onOpen(data) and onClose() handlers + * - Optionally auto-closes on route changes + */ +export const useModalState = ({closeOnRouteChange = true, resetDataOnClose = true} = {}) => { + const [state, setState] = useState({ + isOpen: false, + data: null + }) + + const {pathname} = useLocation() + + useEffect(() => { + if (closeOnRouteChange && state.isOpen) { + setState({ + ...state, + isOpen: false + }) + } + }, [pathname]) + + return { + isOpen: state.isOpen, + data: state.data, + onOpen: (data) => { + setState({ + isOpen: true, + data + }) + }, + onClose: () => { + setState({ + isOpen: false, + data: resetDataOnClose ? null : state.data + }) + } + } +} From 407faa95de2d9bd8d9e9d2ddc1bdf9745cf550f1 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:27:43 -0400 Subject: [PATCH 8/9] Merge branch 'feature/manual-bonus-products-v4' into t/cc-sharks/W-19168138/add-new-modal/main merge conflict in packages/template-chakra-storefront/src/hooks/index.js where the recently added line "export {useManualBonusProducts} from './use-manual-bonus-products'" was not being used as the file doesn't exist. I removed it --- .github/workflows/test.yml | 2 +- packages/pwa-kit-create-app/CHANGELOG.md | 1 + .../assets/plugin-config.js | 3 + .../chakra-storefront/config/default.js.hbs | 14 +- .../scripts/create-mobify-app.js | 74 +- .../template-chakra-storefront/CHANGELOG.md | 3 + .../config/default.js | 3 +- .../bonus-product-view-modal/index.jsx | 144 +++ .../bonus-product-view-modal/index.test.jsx | 303 +++++ .../src/components/header/index.test.js | 8 - .../src/components/login/index.jsx | 8 +- .../components/passwordless-login/index.jsx | 12 +- .../passwordless-login/index.test.js | 4 +- .../components/product-tile/promo-callout.jsx | 16 +- .../components/product-view-modal/index.jsx | 26 +- .../src/components/product-view/index.jsx | 72 +- .../src/components/standard-login/index.jsx | 8 +- .../components/standard-login/index.test.js | 4 +- .../src/config/constants.js | 198 --- .../src/hooks/index.js | 1 - .../src/hooks/use-auth-modal.js | 7 +- .../src/hooks/use-manual-bonus-products.js | 467 ------- .../hooks/use-manual-bonus-products.test.js | 1097 ----------------- .../src/pages/cart/index.jsx | 4 - .../src/pages/checkout/index.jsx | 4 +- .../pages/checkout/partials/contact-info.jsx | 12 +- .../checkout/partials/contact-info.test.js | 12 +- .../pages/checkout/partials/login-state.jsx | 32 +- .../checkout/partials/login-state.test.js | 30 +- .../src/pages/login/index.jsx | 4 +- .../hooks/use-product-detail-data.js | 95 +- .../template-chakra-storefront/src/routes.tsx | 21 +- .../components/project/product-view-modal.js | 49 + .../translations/da-DK.json | 6 + .../translations/de-DE.json | 6 + .../translations/en-GB.json | 6 + .../translations/en-US.json | 6 + .../translations/es-MX.json | 6 + .../translations/fi-FI.json | 6 + .../translations/fr-FR.json | 6 + .../translations/it-IT.json | 6 + .../translations/ja-JP.json | 6 + .../translations/ko-KR.json | 6 + .../translations/nl-NL.json | 6 + .../translations/no-NO.json | 6 + .../translations/pl-PL.json | 6 + .../translations/pt-BR.json | 6 + .../translations/sv-SE.json | 6 + .../translations/zh-CN.json | 6 + .../translations/zh-TW.json | 6 + 50 files changed, 850 insertions(+), 1990 deletions(-) create mode 100644 packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.jsx create mode 100644 packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.test.jsx delete mode 100644 packages/template-chakra-storefront/src/config/constants.js delete mode 100644 packages/template-chakra-storefront/src/hooks/use-manual-bonus-products.js delete mode 100644 packages/template-chakra-storefront/src/hooks/use-manual-bonus-products.test.js create mode 100644 packages/template-chakra-storefront/src/theme/components/project/product-view-modal.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e41c12fdf7..f67d92adb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -393,4 +393,4 @@ jobs: uses: "./.github/actions/bundle_size_test" with: cwd: ${{ env.PROJECT_DIR }} - config: '{"build/main.js": "10kB", "build/vendor.js": "390kB"}' + config: '{"build/main.js": "59kB", "build/vendor.js": "390kB"}' diff --git a/packages/pwa-kit-create-app/CHANGELOG.md b/packages/pwa-kit-create-app/CHANGELOG.md index 73d9456e65..1e13b453bc 100644 --- a/packages/pwa-kit-create-app/CHANGELOG.md +++ b/packages/pwa-kit-create-app/CHANGELOG.md @@ -3,6 +3,7 @@ - Deprecate V3 Extensibility and experimental V4 Extensibility (#2573) - Move extensibility logic to generator (#2573) - Apply prettier to trimmed files (#2688) +- Convert Social Login feature into an extension [#3017](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3017) ## v3.10.0 (Feb 18, 2025) - Add Data Cloud API configuration to `default.js`. [#2318] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2229) diff --git a/packages/pwa-kit-create-app/assets/plugin-config.js b/packages/pwa-kit-create-app/assets/plugin-config.js index f7d3c03089..2b1384953f 100644 --- a/packages/pwa-kit-create-app/assets/plugin-config.js +++ b/packages/pwa-kit-create-app/assets/plugin-config.js @@ -17,5 +17,8 @@ module.exports = { // SFDC_EXT_HELLO_WORLD_ENABLED: { // description: 'The Hello World Extension' // }, + SFDC_EXT_SOCIAL_LOGIN: { + description: 'Social login Extension' + } } } diff --git a/packages/pwa-kit-create-app/assets/templates/chakra-storefront/config/default.js.hbs b/packages/pwa-kit-create-app/assets/templates/chakra-storefront/config/default.js.hbs index c2fc337d0f..09c154b876 100644 --- a/packages/pwa-kit-create-app/assets/templates/chakra-storefront/config/default.js.hbs +++ b/packages/pwa-kit-create-app/assets/templates/chakra-storefront/config/default.js.hbs @@ -91,14 +91,16 @@ module.exports = { login: { passwordless: { enabled: {{#if answers.project.demo.enableDemoSettings}}true{{else}}false{{/if}}, - callbackURI: process.env.PASSWORDLESS_LOGIN_CALLBACK_URI || '/passwordless-login-callback', + callbackURI: + process.env.PASSWORDLESS_LOGIN_CALLBACK_URI || '/passwordless-login-callback', landingPath: '/passwordless-login-landing' }, + {{#if selectedPlugins.SFDC_EXT_SOCIAL_LOGIN}} social: { - enabled: {{#if answers.project.demo.enableDemoSettings}}true{{else}}false{{/if}}, idps: ['google', 'apple'], redirectURI: process.env.SOCIAL_LOGIN_REDIRECT_URI || '/social-callback' }, + {{/if}} resetPassword: { callbackURI: process.env.RESET_PASSWORD_CALLBACK_URI || '/reset-password-callback', landingPath: '/reset-password-landing' @@ -188,13 +190,7 @@ module.exports = { ], ssrEnabled: true, ssrOnly: ['ssr.js', 'ssr.js.map', 'node_modules/**/*.*'], - ssrShared: [ - 'static/favicon.ico', - 'static/robots.txt', - '**/*.js', - '**/*.js.map', - '**/*.json' - ], + ssrShared: ['static/favicon.ico', 'static/robots.txt', '**/*.js', '**/*.js.map', '**/*.json'], ssrParameters: { ssrFunctionNodeVersion: '22.x', proxyConfigs: [ diff --git a/packages/pwa-kit-create-app/scripts/create-mobify-app.js b/packages/pwa-kit-create-app/scripts/create-mobify-app.js index 350a1a0332..845684dedc 100755 --- a/packages/pwa-kit-create-app/scripts/create-mobify-app.js +++ b/packages/pwa-kit-create-app/scripts/create-mobify-app.js @@ -278,7 +278,8 @@ const PRESETS = [ 'project.einstein.siteId': 'aaij-MobileFirst', 'project.dataCloud.appSourceId': '7ae070a6-f4ec-4def-a383-d9cacc3f20a1', 'project.dataCloud.tenantId': 'g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd', - 'project.demo.enableDemoSettings': false + 'project.demo.enableDemoSettings': false, + 'project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN': false }, assets: ['translations'], private: false @@ -313,7 +314,8 @@ const PRESETS = [ ['project.einstein.siteId']: 'aaij-MobileFirst', ['project.dataCloud.appSourceId']: 'fb81edab-24c6-4b40-8684-b67334dfdf32', ['project.dataCloud.tenantId']: 'mmyw8zrxhfsg09lfmzrd1zjqmg', - ['project.demo.enableDemoSettings']: true // True only for presets deployed to demo environments like pwa-kit.mobify-storefront.com + ['project.demo.enableDemoSettings']: true, // True only for presets deployed to demo environments like pwa-kit.mobify-storefront.com + ['project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN']: false }, assets: ['translations'], private: true @@ -324,7 +326,7 @@ const PRESETS = [ description: '', templateSource: { type: TEMPLATE_SOURCE_BUNDLE, - id: 'typescript-minimal' + id: 'chakra-storefront' }, answers: { 'project.hybrid': false, @@ -339,7 +341,8 @@ const PRESETS = [ 'project.einstein.siteId': 'aaij-MobileFirst', 'project.dataCloud.appSourceId': 'fb81edab-24c6-4b40-8684-b67334dfdf32', 'project.dataCloud.tenantId': 'mmyw8zrxhfsg09lfmzrd1zjqmg', - 'project.demo.enableDemoSettings': false + 'project.demo.enableDemoSettings': false, + 'project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN': true }, assets: ['translations'], private: true @@ -365,7 +368,8 @@ const PRESETS = [ 'project.einstein.siteId': 'aaij-MobileFirst', 'project.dataCloud.appSourceId': 'fb81edab-24c6-4b40-8684-b67334dfdf32', 'project.dataCloud.tenantId': 'mmyw8zrxhfsg09lfmzrd1zjqmg', - 'project.demo.enableDemoSettings': false + 'project.demo.enableDemoSettings': false, + 'project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN': false }, assets: ['translations'], private: true @@ -391,7 +395,8 @@ const PRESETS = [ 'project.dataCloud.appSourceId': 'fb81edab-24c6-4b40-8684-b67334dfdf32', 'project.dataCloud.tenantId': 'mmyw8zrxhfsg09lfmzrd1zjqmg', 'project.commerce.isSlasPrivate': true, - 'project.demo.enableDemoSettings': false + 'project.demo.enableDemoSettings': false, + 'project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN': false }, assets: ['translations'], private: true @@ -417,7 +422,8 @@ const PRESETS = [ 'project.commerce.isSlasPrivate': true, 'project.dataCloud.appSourceId': 'fb81edab-24c6-4b40-8684-b67334dfdf32', 'project.dataCloud.tenantId': 'mmyw8zrxhfsg09lfmzrd1zjqmg', - 'project.demo.enableDemoSettings': false + 'project.demo.enableDemoSettings': false, + 'project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN': false }, assets: ['translations'], private: true @@ -443,7 +449,8 @@ const PRESETS = [ 'project.commerce.isSlasPrivate': false, 'project.dataCloud.appSourceId': 'fb81edab-24c6-4b40-8684-b67334dfdf32', 'project.dataCloud.tenantId': 'mmyw8zrxhfsg09lfmzrd1zjqmg', - 'project.demo.enableDemoSettings': false + 'project.demo.enableDemoSettings': false, + 'project.selectedPlugins.SFDC_EXT_SOCIAL_LOGIN': false }, assets: ['translations'], private: true @@ -660,7 +667,7 @@ const expandKey = (key, value) => * const expandedObj = expand({'coolthings.babynames': 'Preseley', 'coolthings.cars': 'bmws'}) * console.log(expandedObj) // {coolthings: { babynames: 'Presley', cars: 'bmws'}} * - * @param {Object} answers + * @param {Object} answer * @returns {Object} The expanded object. * */ @@ -884,6 +891,27 @@ const main = async (opts) => { if (interactive) { const questions = getQuestions ? getQuestions() : [] const projectAnswers = await prompt(questions, answers) + // Only prompt for plugin selection on interactive presets + if (Object.keys(pluginConfig?.plugins || {}).length > 0) { + const pluginChoices = Object.entries(pluginConfig.plugins).map(([key, config]) => ({ + name: config.description, + value: key + })) + + const pluginAnswers = await inquirer.prompt([ + { + type: 'checkbox', + name: 'selectedPlugins', + message: 'Which extensions would you like to enable?', + choices: pluginChoices + } + ]) + + // Convert selected plugins array to object with true values + pluginAnswers.selectedPlugins.forEach((plugin) => { + selectedPlugins[plugin] = true + }) + } context = merge(context, { answers: expandObject(projectAnswers) }) @@ -892,28 +920,14 @@ const main = async (opts) => { answers: expandObject(answers) }) } - - // Prompt user for plugin selection - if (Object.keys(pluginConfig?.plugins || {}).length > 0) { - const pluginChoices = Object.entries(pluginConfig.plugins).map(([key, config]) => ({ - name: config.description, - value: key - })) - - const pluginAnswers = await inquirer.prompt([ - { - type: 'checkbox', - name: 'selectedPlugins', - message: 'Which extensions would you like to enable?', - choices: pluginChoices + // load plugin selected answer from context object to selectedPlugins (which used for code trimming process) + Object.entries(context.answers?.project?.selectedPlugins || {}).forEach( + ([pluginKey, enabled]) => { + if (pluginConfig?.plugins?.[pluginKey]) { + selectedPlugins[pluginKey] = enabled } - ]) - - // Convert selected plugins array to object with true values - pluginAnswers.selectedPlugins.forEach((plugin) => { - selectedPlugins[plugin] = true - }) - } + } + ) if (!OUTPUT_DIR_FLAG_ACTIVE) { // For extension projects, use the extension name as the output directory diff --git a/packages/template-chakra-storefront/CHANGELOG.md b/packages/template-chakra-storefront/CHANGELOG.md index e87db3e474..be0aa0354d 100644 --- a/packages/template-chakra-storefront/CHANGELOG.md +++ b/packages/template-chakra-storefront/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.1.1 - Show button "Select Bonus Products" when a product is added, that qualifies the cart for a manual selection bonus product. Also show the corresponding promotional message with the button [#2917](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2917) +- Implemented the "Bonus Product View Modal" [#3043](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3043) ## 0.1.0-extensibility-preview.5 - Fix failing tests in pages folder [#2872](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2872) @@ -7,6 +8,8 @@ - Migrate directory structure from `app/*` to `src/*` [#2693](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2693) - Upgrade to Chakra UI v3 and Decomposition on Cart, PLP, PDP and Cart [2839](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2839), [#2872](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2872), [#2878](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2878), [#2924](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2924) - Create a safe version of `` that won't break the SSR rendering [#2785](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2785) +- Convert Social Login feature into an extension [#3017](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3017) + ## 0.1.0-extensibility-preview.4 - Fix hreflang alternate links [#2269](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2269) diff --git a/packages/template-chakra-storefront/config/default.js b/packages/template-chakra-storefront/config/default.js index 870bc57594..93b8b7c5c1 100644 --- a/packages/template-chakra-storefront/config/default.js +++ b/packages/template-chakra-storefront/config/default.js @@ -95,11 +95,12 @@ module.exports = { process.env.PASSWORDLESS_LOGIN_CALLBACK_URI || '/passwordless-login-callback', landingPath: '/passwordless-login-landing' }, + //@sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN social: { - enabled: false, idps: ['google', 'apple'], redirectURI: process.env.SOCIAL_LOGIN_REDIRECT_URI || '/social-callback' }, + //@sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN resetPassword: { callbackURI: process.env.RESET_PASSWORD_CALLBACK_URI || '/reset-password-callback', landingPath: '/reset-password-landing' diff --git a/packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.jsx b/packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.jsx new file mode 100644 index 0000000000..f8639ee40a --- /dev/null +++ b/packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.jsx @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2021, 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, {useMemo, useCallback} from 'react' +import PropTypes from 'prop-types' +import {Dialog, CloseButton, Button} from '@chakra-ui/react' +import Link from '../link' +import ProductView from '../../components/product-view' +import {useProductViewModal} from '../../hooks/use-product-view-modal' +import SafePortal from '../safe-portal' +import {useIntl} from 'react-intl' +import {productViewModalTheme} from '../../theme/components/project/product-view-modal' +import {useShopperBasketsMutationHelper} from '@salesforce/commerce-sdk-react' + +/** + * A Dialog that contains Bonus Product View using product-view-modal theme + */ +const BonusProductViewModal = ({ + product, + isOpen, + onClose, + bonusDiscountLineItemId, + promotionId, + ...props +}) => { + const productViewModalData = useProductViewModal(product) + const {addItemToNewOrExistingBasket} = useShopperBasketsMutationHelper() + + const intl = useIntl() + const {formatMessage} = intl + + const messages = useMemo( + () => ({ + modalLabel: formatMessage( + { + id: 'bonus_product_view_modal.modal_label', + defaultMessage: 'Bonus product selection modal for {productName}' + }, + {productName: productViewModalData?.product?.name} + ), + viewCart: formatMessage({ + id: 'bonus_product_view_modal.button.view_cart', + defaultMessage: 'View Cart' + }) + }), + [intl] + ) + + // Custom addToCart handler for bonus products that includes bonusDiscountLineItemId + const handleAddToCart = useCallback( + async (variant, quantity) => { + const productItems = [ + { + productId: variant?.productId || product?.id, + price: variant?.price || product?.price, + quantity: quantity, + bonusDiscountLineItemId: bonusDiscountLineItemId + } + ] + + const result = await addItemToNewOrExistingBasket(productItems) + return result + }, + [addItemToNewOrExistingBasket, product, bonusDiscountLineItemId] + ) + + // Custom buttons for the ProductView + const customButtons = useMemo( + () => [ + + ], + [messages.viewCart, onClose] + ) + + return ( + onClose()} + size={productViewModalTheme.modal.size} + scrollBehavior={productViewModalTheme.modal.scrollBehavior} + placement={productViewModalTheme.modal.placement} + closeOnInteractOutside={productViewModalTheme.modal.closeOnInteractOutside} + > + + + + + + + + + + + + + + + ) +} + +BonusProductViewModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onOpen: PropTypes.func, + onClose: PropTypes.func.isRequired, + product: PropTypes.object, + isLoading: PropTypes.bool, + bonusDiscountLineItemId: PropTypes.string, // The 'id' from bonusDiscountLineItems + promotionId: PropTypes.string // The promotion ID to filter promotions in PromoCallout +} + +export default BonusProductViewModal diff --git a/packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.test.jsx b/packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.test.jsx new file mode 100644 index 0000000000..35334a4109 --- /dev/null +++ b/packages/template-chakra-storefront/src/components/bonus-product-view-modal/index.test.jsx @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2021, 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 BonusProductViewModal from './index' +import {renderWithProviders} from '../../utils/test-utils' +import {act, screen, waitFor} from '@testing-library/react' +import {useDisclosure} from '@chakra-ui/react' +import mockProductDetail from '../../../mocks/variant-750518699578M' +import {prependHandlersToServer} from '../../../jest-setup' + +// Mock the useShopperBasketsMutationHelper hook +jest.mock('@salesforce/commerce-sdk-react', () => { + const originalModule = jest.requireActual('@salesforce/commerce-sdk-react') + return { + ...originalModule, + useShopperBasketsMutationHelper: jest.fn() + } +}) + +// Mock the useProductViewModal hook +jest.mock('../../hooks/use-product-view-modal', () => ({ + useProductViewModal: jest.fn() +})) + +// Mock the AddToCartModal context to avoid the itemsAdded.reduce error +jest.mock('../../hooks/use-add-to-cart-modal', () => ({ + AddToCartModalProvider: ({children}) => children, + useAddToCartModal: () => ({ + addToCartModal: { + isOpen: false, + onOpen: jest.fn(), + onClose: jest.fn() + }, + isProductABundle: false, + selectedQuantity: 1, + itemsAdded: [], + basketLoaded: true, + productLoaded: true, + showAddToCartModal: jest.fn(), + updateCartItemsCountAndTotal: jest.fn() + }), + useAddToCartModalContext: () => ({ + addToCartModal: { + isOpen: false, + onOpen: jest.fn(), + onClose: jest.fn() + }, + isProductABundle: false, + selectedQuantity: 1, + itemsAdded: [], + basketLoaded: true, + productLoaded: true, + showAddToCartModal: jest.fn(), + updateCartItemsCountAndTotal: jest.fn() + }) +})) + +const mockAddItemToNewOrExistingBasket = jest.fn() +const mockProductViewModalData = { + product: mockProductDetail, + isFetching: false +} + +const MockComponent = ({product, bonusDiscountLineItemId, promotionId, onClose}) => { + const {open, onOpen, onClose: defaultOnClose} = useDisclosure() + + return ( +
+ + +
+ ) +} + +MockComponent.propTypes = { + product: PropTypes.object, + bonusDiscountLineItemId: PropTypes.string, + promotionId: PropTypes.string, + onClose: PropTypes.func +} + +// Mock product data specifically for bonus products +const mockBonusProduct = { + ...mockProductDetail, + id: 'bonus-product-123', + name: 'Test Bonus Product', + price: 29.99, + productPromotions: [ + { + calloutMsg: 'Special Bonus Promotion - 20% Off', + promotionId: 'bonus-promo-20-off', + promotionalPrice: 23.99 + }, + { + calloutMsg: 'Buy 2 Get 1 Free - Bonus Items', + promotionId: 'bonus-buy2get1', + promotionalPrice: 19.99 + } + ] +} + +beforeEach(() => { + jest.clearAllMocks() + + // Set up the mock implementations + // eslint-disable-next-line @typescript-eslint/no-var-requires + const {useShopperBasketsMutationHelper} = require('@salesforce/commerce-sdk-react') + // eslint-disable-next-line @typescript-eslint/no-var-requires + const {useProductViewModal} = require('../../hooks/use-product-view-modal') + + useShopperBasketsMutationHelper.mockReturnValue({ + addItemToNewOrExistingBasket: mockAddItemToNewOrExistingBasket + }) + + useProductViewModal.mockReturnValue(mockProductViewModalData) + + // Reset the mock function + mockAddItemToNewOrExistingBasket.mockReset() + mockAddItemToNewOrExistingBasket.mockResolvedValue({ + success: true, + basketId: 'test-basket-123' + }) + + prependHandlersToServer([ + { + path: '*/products/:productId', + res: () => { + return mockProductDetail + } + } + ]) +}) + +describe('BonusProductViewModal', () => { + test('component props and structure', () => { + // This test verifies the component can be imported and has the right structure + expect(BonusProductViewModal).toBeDefined() + expect(typeof BonusProductViewModal).toBe('function') + + // Verify PropTypes are defined + expect(BonusProductViewModal.propTypes).toBeDefined() + expect(BonusProductViewModal.propTypes.bonusDiscountLineItemId).toBeDefined() + expect(BonusProductViewModal.propTypes.promotionId).toBeDefined() + }) + + test('renders bonus product view modal when open', async () => { + const {user} = renderWithProviders() + + // Open the modal + const trigger = screen.getByText(/open bonus modal/i) + await act(async () => { + await user.click(trigger) + }) + + // Wait for modal to appear and check if it's rendered + await waitFor(() => { + expect(screen.queryByTestId('bonus-product-view-modal')).toBeInTheDocument() + }) + + // Check if the modal has proper aria attributes + const modal = screen.getByTestId('bonus-product-view-modal') + expect(modal).toHaveAttribute( + 'aria-label', + expect.stringContaining('Bonus product selection modal') + ) + }) + + test('receives bonusDiscountLineItemId prop correctly', () => { + const bonusDiscountLineItemId = 'bonus-discount-123' + + // Test that the component can receive the prop without errors + expect(() => { + renderWithProviders( + + ) + }).not.toThrow() + + // Verify the mock helper is available for testing + expect(mockAddItemToNewOrExistingBasket).toBeDefined() + }) + + test('modal close functionality', async () => { + const mockOnClose = jest.fn() + const {user} = renderWithProviders( + + ) + + // Open the modal + const trigger = screen.getByText(/open bonus modal/i) + await act(async () => { + await user.click(trigger) + }) + + // Wait for modal to appear + await waitFor(() => { + expect(screen.queryByTestId('bonus-product-view-modal')).toBeInTheDocument() + }) + + // Test that modal is rendered properly + const modal = screen.getByTestId('bonus-product-view-modal') + expect(modal).toBeInTheDocument() + }) + + test('handles promotionId prop', () => { + const promotionId = 'bonus-promo-20-off' + + // Test that the component can receive promotionId prop without errors + expect(() => { + renderWithProviders( + + ) + }).not.toThrow() + }) + + test('handles loading state', () => { + // Mock the hook to return loading state + // eslint-disable-next-line @typescript-eslint/no-var-requires + const {useProductViewModal} = require('../../hooks/use-product-view-modal') + useProductViewModal.mockReturnValue({ + product: mockBonusProduct, + isFetching: true + }) + + // Test that the component handles loading state without errors + expect(() => { + renderWithProviders() + }).not.toThrow() + }) + + test('handles missing bonusDiscountLineItemId gracefully', () => { + // Test that the component handles undefined bonusDiscountLineItemId + expect(() => { + renderWithProviders( + + ) + }).not.toThrow() + }) + + test('modal accessibility attributes', async () => { + const {user} = renderWithProviders() + + // Open the modal + const trigger = screen.getByText(/open bonus modal/i) + await act(async () => { + await user.click(trigger) + }) + + // Wait for modal to appear + await waitFor(() => { + const modal = screen.queryByTestId('bonus-product-view-modal') + expect(modal).toBeInTheDocument() + }) + + const modal = screen.getByTestId('bonus-product-view-modal') + expect(modal).toHaveAttribute( + 'aria-label', + expect.stringContaining('Bonus product selection modal') + ) + }) + + test('handleAddToCart function includes bonusDiscountLineItemId', () => { + // This test verifies the handleAddToCart function is properly constructed + // Since we can't easily test the internal handler without complex mocking, + // we verify that the mocked function is available and our setup is correct + + expect(mockAddItemToNewOrExistingBasket).toBeDefined() + expect(typeof mockAddItemToNewOrExistingBasket).toBe('function') + + // Test the structure that would be passed to addItemToNewOrExistingBasket + const testVariant = {productId: 'test-123', price: 29.99} + const testQuantity = 1 + const testBonusDiscountLineItemId = 'bonus-123' + + const expectedProductItems = [ + { + productId: testVariant.productId, + price: testVariant.price, + quantity: testQuantity, + bonusDiscountLineItemId: testBonusDiscountLineItemId + } + ] + + // Verify the structure matches what our component would send + expect(expectedProductItems[0]).toHaveProperty('bonusDiscountLineItemId') + expect(expectedProductItems[0].bonusDiscountLineItemId).toBe(testBonusDiscountLineItemId) + }) +}) diff --git a/packages/template-chakra-storefront/src/components/header/index.test.js b/packages/template-chakra-storefront/src/components/header/index.test.js index 5b80a4d3c1..9fef4d8800 100644 --- a/packages/template-chakra-storefront/src/components/header/index.test.js +++ b/packages/template-chakra-storefront/src/components/header/index.test.js @@ -21,14 +21,6 @@ jest.mock('@chakra-ui/react', () => { } }) -jest.mock('@salesforce/pwa-kit-extension-sdk/react', () => ({ - ...jest.requireActual('@salesforce/pwa-kit-extension-sdk/react'), - useApplicationExtensionsStore: jest.fn().mockReturnValue({ - isModalOpen: false, - closeModal: jest.fn() - }) -})) - const MockedComponent = ({history}) => { const onAccountClick = () => { history.push(createPathWithDefaults('/account')) diff --git a/packages/template-chakra-storefront/src/components/login/index.jsx b/packages/template-chakra-storefront/src/components/login/index.jsx index 64203e60f1..ff75777448 100644 --- a/packages/template-chakra-storefront/src/components/login/index.jsx +++ b/packages/template-chakra-storefront/src/components/login/index.jsx @@ -35,7 +35,7 @@ const LoginForm = ({ clickCreateAccount = noop, form, isPasswordlessEnabled = false, - isSocialEnabled = false, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps = [] }) => { const {formatMessage} = useIntl() @@ -73,14 +73,14 @@ const LoginForm = ({ ) : ( )} @@ -108,7 +108,7 @@ LoginForm.propTypes = { clickCreateAccount: PropTypes.func, form: PropTypes.object, isPasswordlessEnabled: PropTypes.bool, - isSocialEnabled: PropTypes.bool, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps: PropTypes.arrayOf(PropTypes.string) } diff --git a/packages/template-chakra-storefront/src/components/passwordless-login/index.jsx b/packages/template-chakra-storefront/src/components/passwordless-login/index.jsx index 1f1a738bd6..67af34d64a 100644 --- a/packages/template-chakra-storefront/src/components/passwordless-login/index.jsx +++ b/packages/template-chakra-storefront/src/components/passwordless-login/index.jsx @@ -16,7 +16,7 @@ import SocialLogin from '../social-login' const PasswordlessLogin = ({ form, handleForgotPasswordClick, - isSocialEnabled = false, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps = [] }) => { const intl = useIntl() @@ -84,7 +84,9 @@ const PasswordlessLogin = ({ > {messages.password} - {isSocialEnabled && } + {/* @sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN */} + + {/* @sfdc-extension-block-endSFDC_EXT_SOCIAL_LOGIN */} )} @@ -105,9 +107,9 @@ const PasswordlessLogin = ({ PasswordlessLogin.propTypes = { form: PropTypes.object, handleForgotPasswordClick: PropTypes.func, - isSocialEnabled: PropTypes.bool, - idps: PropTypes.arrayOf(PropTypes.string), - hideEmail: PropTypes.bool + hideEmail: PropTypes.bool, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN + idps: PropTypes.arrayOf(PropTypes.string) } export default PasswordlessLogin diff --git a/packages/template-chakra-storefront/src/components/passwordless-login/index.test.js b/packages/template-chakra-storefront/src/components/passwordless-login/index.test.js index ff2ab46050..0d4752298d 100644 --- a/packages/template-chakra-storefront/src/components/passwordless-login/index.test.js +++ b/packages/template-chakra-storefront/src/components/passwordless-login/index.test.js @@ -65,10 +65,12 @@ describe('PasswordlessLogin component', () => { expect(screen.queryByLabelText('Password')).not.toBeInTheDocument() }) + //@sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN test('renders social login buttons', async () => { - renderWithProviders() + renderWithProviders() expect(screen.getByRole('button', {name: /Google/})).toBeInTheDocument() expect(screen.getByRole('button', {name: /Apple/})).toBeInTheDocument() }) + //@sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN }) diff --git a/packages/template-chakra-storefront/src/components/product-tile/promo-callout.jsx b/packages/template-chakra-storefront/src/components/product-tile/promo-callout.jsx index f31bad44b2..89710678c2 100644 --- a/packages/template-chakra-storefront/src/components/product-tile/promo-callout.jsx +++ b/packages/template-chakra-storefront/src/components/product-tile/promo-callout.jsx @@ -8,19 +8,29 @@ import React, {useMemo} from 'react' import PropTypes from 'prop-types' import {findLowestPrice} from '../../utils/product-utils' -const PromoCallout = ({product}) => { +const PromoCallout = ({product, promotionId}) => { const {promotion, data} = useMemo(() => findLowestPrice(product), [product]) // NOTE: API inconsistency - with getProduct call, a variant does not have productPromotions const promos = data?.productPromotions ?? product?.productPromotions ?? [] - const promo = promotion ?? promos[0] + + const promo = useMemo(() => { + // If promotionId is provided, find the specific promotion + if (promotionId) { + const specificPromo = promos.find((p) => p.promotionId === promotionId) + return specificPromo || null + } + // Otherwise, use the default behavior (lowest price promotion or first promotion) + return promotion ?? promos[0] + }, [promotion, promos, promotionId]) // calloutMsg can be html string or just plain text return
} PromoCallout.propTypes = { - product: PropTypes.object + product: PropTypes.object, + promotionId: PropTypes.string } export default PromoCallout diff --git a/packages/template-chakra-storefront/src/components/product-view-modal/index.jsx b/packages/template-chakra-storefront/src/components/product-view-modal/index.jsx index 0848c04258..42a4988fbd 100644 --- a/packages/template-chakra-storefront/src/components/product-view-modal/index.jsx +++ b/packages/template-chakra-storefront/src/components/product-view-modal/index.jsx @@ -12,6 +12,7 @@ import ProductView from '../../components/product-view' import {useProductViewModal} from '../../hooks/use-product-view-modal' import SafePortal from '../safe-portal' import {useIntl} from 'react-intl' +import {productViewModalTheme} from '../../theme/components/project/product-view-modal' /** * A Dialog that contains Product View @@ -39,8 +40,10 @@ const ProductViewModal = ({product, isOpen, onClose, ...props}) => { lazyMount open={isOpen} onOpenChange={() => onClose()} - size="xl" - closeOnInteractOutside={false} + size={productViewModalTheme.modal.size} + scrollBehavior={productViewModalTheme.modal.scrollBehavior} + placement={productViewModalTheme.modal.placement} + closeOnInteractOutside={productViewModalTheme.modal.closeOnInteractOutside} > @@ -48,11 +51,24 @@ const ProductViewModal = ({product, isOpen, onClose, ...props}) => { - + { + // Calculate promotional price data when promotionId is specified + const adjustedPriceData = useMemo(() => { + if (!promotionId || !product?.productPromotions || !priceData) { + return priceData + } + + // Find the specific promotion by promotionId + const specificPromotion = product.productPromotions.find( + (promo) => promo.promotionId === promotionId + ) + + if (!specificPromotion) { + return priceData + } + + // Use the promotionalPrice from the specific promotion, default to 0 if not specified + const promotionalPrice = specificPromotion.promotionalPrice ?? 0.0 + + // Ensure we have all required properties for DisplayPrice + return { + ...priceData, + currentPrice: promotionalPrice, + listPrice: priceData.listPrice || priceData.currentPrice, + isOnSale: + promotionalPrice > 0 && + promotionalPrice < (priceData.listPrice || priceData.currentPrice || 0), + // Ensure other required properties are present + isASet: priceData.isASet || false, + isMaster: priceData.isMaster || false, + isRange: priceData.isRange || false + } + }, [priceData, product?.productPromotions, promotionId]) + return ( {category && ( @@ -111,14 +145,16 @@ const ProductViewHeader = ({ {!isProductPartOfBundle && ( <> - - {priceData?.currentPrice && ( - + + {adjustedPriceData && adjustedPriceData.currentPrice !== undefined && ( + )} - {product?.productPromotions && } + {product?.productPromotions && ( + + )} )} @@ -132,7 +168,8 @@ ProductViewHeader.propTypes = { category: PropTypes.array, priceData: PropTypes.object, product: PropTypes.object, - isProductPartOfBundle: PropTypes.bool + isProductPartOfBundle: PropTypes.bool, + promotionId: PropTypes.string } const ButtonWithRegistration = withRegistration(Button) @@ -166,7 +203,9 @@ const ProductView = forwardRef( !isProductLoading && variant?.orderable && quantity > 0 && quantity <= stockLevel, showImageGallery = true, setSelectedBundleQuantity = () => {}, - selectedBundleParentQuantity = 1 + selectedBundleParentQuantity = 1, + customButtons = [], + promotionId }, ref ) => { @@ -352,6 +391,19 @@ const ProductView = forwardRef( ) } + // Add custom buttons if provided + if (customButtons && customButtons.length > 0) { + customButtons.forEach((customButton, index) => { + buttons.push( + React.cloneElement(customButton, { + key: `custom-button-${index}`, + width: customButton.props.width || '100%', + marginBottom: customButton.props.marginBottom || 4 + }) + ) + }) + } + return buttons } @@ -419,6 +471,7 @@ const ProductView = forwardRef( currency={product?.currency || activeCurrency} category={category} isProductPartOfBundle={isProductPartOfBundle} + promotionId={promotionId} /> @@ -459,6 +512,7 @@ const ProductView = forwardRef( currency={product?.currency || activeCurrency} category={category} isProductPartOfBundle={isProductPartOfBundle} + promotionId={promotionId} /> @@ -690,7 +744,9 @@ ProductView.propTypes = { validateOrderability: PropTypes.func, showImageGallery: PropTypes.bool, setSelectedBundleQuantity: PropTypes.func, - selectedBundleParentQuantity: PropTypes.number + selectedBundleParentQuantity: PropTypes.number, + customButtons: PropTypes.array, + promotionId: PropTypes.string } export default ProductView diff --git a/packages/template-chakra-storefront/src/components/standard-login/index.jsx b/packages/template-chakra-storefront/src/components/standard-login/index.jsx index 724d0d6cea..805875bbad 100644 --- a/packages/template-chakra-storefront/src/components/standard-login/index.jsx +++ b/packages/template-chakra-storefront/src/components/standard-login/index.jsx @@ -31,8 +31,8 @@ const StandardLogin = ({ form, handleForgotPasswordClick, hideEmail = false, - isSocialEnabled = false, setShowPasswordView, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps = [] }) => { const {formatMessage} = useIntl() @@ -55,7 +55,8 @@ const StandardLogin = ({ > {formatMessage(messages.signIn)} - {isSocialEnabled && idps.length > 0 && ( + {/* @sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN */} + {idps.length > 0 && ( <> @@ -66,6 +67,7 @@ const StandardLogin = ({ )} + {/* @sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN */} {hideEmail && ( @@ -264,8 +264,8 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id } ContactInfo.propTypes = { - isSocialEnabled: PropTypes.bool, isPasswordlessEnabled: PropTypes.bool, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps: PropTypes.arrayOf(PropTypes.string) } diff --git a/packages/template-chakra-storefront/src/pages/checkout/partials/contact-info.test.js b/packages/template-chakra-storefront/src/pages/checkout/partials/contact-info.test.js index 3f4eab1539..e17f600753 100644 --- a/packages/template-chakra-storefront/src/pages/checkout/partials/contact-info.test.js +++ b/packages/template-chakra-storefront/src/pages/checkout/partials/contact-info.test.js @@ -52,9 +52,7 @@ afterEach(() => { describe('passwordless and social disabled', () => { test('renders component', async () => { - const {user} = renderWithProviders( - - ) + const {user} = renderWithProviders() // switch to login const trigger = screen.getByText(/Already have an account\? Log in/i) @@ -282,14 +280,12 @@ describe('passwordless enabled', () => { } ) }) - +//@sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN describe('social login enabled', () => { test('renders component', async () => { - const {getByRole} = renderWithProviders( - - ) + const {getByRole} = renderWithProviders() expect(getByRole('button', {name: 'Checkout as Guest'})).toBeInTheDocument() - expect(getByRole('button', {name: 'Password'})).toBeInTheDocument() expect(getByRole('button', {name: /Google/i})).toBeInTheDocument() }) }) +//@sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN diff --git a/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.jsx b/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.jsx index d55fba7d0d..ba629ba546 100644 --- a/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.jsx +++ b/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.jsx @@ -13,11 +13,11 @@ import SocialLogin from '../../../components/social-login' const LoginState = ({ form, handlePasswordlessLoginClick, - isSocialEnabled, isPasswordlessEnabled, - idps, showPasswordField, - togglePasswordField + togglePasswordField, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN + idps = [] }) => { const intl = useIntl() const {formatMessage} = intl @@ -53,7 +53,9 @@ const LoginState = ({ [intl] ) - if (isSocialEnabled || isPasswordlessEnabled) { + // when passwordless enabled, social login buttons will be in the same screen with pwless login + // when pwless is disabled, the social login buttons will stay in the same screen with standard login + if (isPasswordlessEnabled) { return showLoginButtons ? ( <> @@ -89,8 +91,10 @@ const LoginState = ({ {messages.password} )} + {/* @sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN */} {/* Social Login */} - {isSocialEnabled && idps && } + {idps.length > 0 && } + {/* @sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN */} ) : ( + <> + + {/* @sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN */} + + + {messages.orLoginWith} + + {/* Social Login */} + {idps.length > 0 && } + {/* @sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN */} + ) } } @@ -116,8 +130,8 @@ const LoginState = ({ LoginState.propTypes = { form: PropTypes.object, handlePasswordlessLoginClick: PropTypes.func, - isSocialEnabled: PropTypes.bool, isPasswordlessEnabled: PropTypes.bool, + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps: PropTypes.arrayOf(PropTypes.string), showPasswordField: PropTypes.bool, togglePasswordField: PropTypes.func diff --git a/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.test.js b/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.test.js index 396f6d56df..c19e0b70bc 100644 --- a/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.test.js +++ b/packages/template-chakra-storefront/src/pages/checkout/partials/login-state.test.js @@ -8,7 +8,7 @@ import React from 'react' import LoginState from '../../../pages/checkout/partials/login-state' import {renderWithProviders} from '../../../utils/test-utils' import {useForm} from 'react-hook-form' -import {act} from '@testing-library/react' +import {act, screen} from '@testing-library/react' const mockTogglePasswordField = jest.fn() const idps = ['apple', 'google'] @@ -55,14 +55,25 @@ describe('LoginState', () => { const {queryByRole, queryByText} = renderWithProviders( ) - expect(queryByText('Or Login With')).not.toBeInTheDocument() expect(queryByRole('button', {name: 'Secure Link'})).not.toBeInTheDocument() }) - test('shows social login buttons if enabled', async () => { + //@sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN + test('shows social login buttons along with standard login form', async () => { + const {getByRole, getByText} = renderWithProviders() + expect(getByText('Or Login With')).toBeInTheDocument() + expect(getByRole('button', {name: /Google/i})).toBeInTheDocument() + expect(getByRole('button', {name: /Apple/i})).toBeInTheDocument() + expect( + screen.queryByRole('button', {name: 'Back to Sign In Options'}) + ).not.toBeInTheDocument() + }) + + test('shows social login buttons along with passwordless flow', async () => { const {getByRole, getByText, user} = renderWithProviders( - + ) + expect(getByText('Or Login With')).toBeInTheDocument() expect(getByRole('button', {name: /Google/i})).toBeInTheDocument() expect(getByRole('button', {name: /Apple/i})).toBeInTheDocument() @@ -71,15 +82,8 @@ describe('LoginState', () => { await user.click(trigger) }) expect(mockTogglePasswordField).toHaveBeenCalled() + screen.logTestingPlaygroundURL() expect(getByRole('button', {name: 'Back to Sign In Options'})).toBeInTheDocument() }) - - test('does not show social login buttons if disabled', () => { - const {queryByRole, queryByText} = renderWithProviders( - - ) - expect(queryByText('Or Login With')).not.toBeInTheDocument() - expect(queryByRole('button', {name: /Google/i})).not.toBeInTheDocument() - expect(queryByRole('button', {name: /Apple/i})).not.toBeInTheDocument() - }) + //@sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN }) diff --git a/packages/template-chakra-storefront/src/pages/login/index.jsx b/packages/template-chakra-storefront/src/pages/login/index.jsx index f4b07a6dcf..bf00612685 100644 --- a/packages/template-chakra-storefront/src/pages/login/index.jsx +++ b/packages/template-chakra-storefront/src/pages/login/index.jsx @@ -70,7 +70,7 @@ const Login = ({initialView = LOGIN_VIEW}) => { const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless) const {passwordless = {}, social = {}} = loginConfig const isPasswordlessEnabled = !!passwordless?.enabled - const isSocialEnabled = !!social?.enabled + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN const idps = social?.idps const customerId = useCustomerId() @@ -215,7 +215,7 @@ const Login = ({initialView = LOGIN_VIEW}) => { clickCreateAccount={() => navigate('/registration')} handleForgotPasswordClick={() => navigate('/reset-password')} isPasswordlessEnabled={isPasswordlessEnabled} - isSocialEnabled={isSocialEnabled} + //@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN idps={idps} /> )} diff --git a/packages/template-chakra-storefront/src/pages/product-detail/hooks/use-product-detail-data.js b/packages/template-chakra-storefront/src/pages/product-detail/hooks/use-product-detail-data.js index 73bd13d226..dc24a0a514 100644 --- a/packages/template-chakra-storefront/src/pages/product-detail/hooks/use-product-detail-data.js +++ b/packages/template-chakra-storefront/src/pages/product-detail/hooks/use-product-detail-data.js @@ -21,7 +21,6 @@ import {useHistory, useLocation, useParams} from 'react-router-dom' import {useCurrentBasket, useVariant} from '../../../hooks' import useEinstein from '../../../hooks/use-einstein' import {useWishList} from '../../../hooks/use-wish-list' -import {useManualBonusProducts} from '../../../hooks/use-manual-bonus-products' import {normalizeSetBundleProduct, getUpdateBundleChildArray} from '../../../utils/product-utils' import {useErrorHandler} from '../../../hooks/use-errors' @@ -36,17 +35,10 @@ export const useProductDetailData = () => { const {addToWishlist, isPending: isWishlistLoading} = useWishList() /****************************** Basket *********************************/ - const {data: currentBasket, isLoading: isBasketLoading} = useCurrentBasket() + const {isLoading: isBasketLoading} = useCurrentBasket() const {addItemToNewOrExistingBasket} = useShopperBasketsMutationHelper() const updateItemsInBasketMutation = useShopperBasketsMutation('updateItemsInBasket') - /****************************** Manual Bonus Products *********************************/ - const { - createManualBonusProductCollections, - detectNewlyAddedBonusProducts, - analyzeQualifyingProductChanges - } = useManualBonusProducts() - /*************************** Product Detail and Category ********************/ const {productId} = useParams() const urlParams = new URLSearchParams(location.search) @@ -197,52 +189,8 @@ export const useProductDetailData = () => { quantity })) - // Capture current basket state before adding items - const beforeBasket = currentBasket || {} - // Add items to basket - const updatedBasket = await addItemToNewOrExistingBasket(productItems) - - // Get list of product IDs that were just added - const addedProductIds = productItems.map((item) => item.productId) - - // Analyze qualifying product changes - const qualifyingProductChanges = analyzeQualifyingProductChanges( - beforeBasket, - updatedBasket, - addedProductIds - ) - - // Detect newly added bonus products and their associations - const detectionResult = detectNewlyAddedBonusProducts( - beforeBasket, - updatedBasket, - qualifyingProductChanges - ) - - // Create manual bonus product collections based on detection results - if (Object.keys(detectionResult.qualifyingProductToBonusProducts).length > 0) { - createManualBonusProductCollections( - detectionResult.qualifyingProductToBonusProducts - ) - - // Debug log to verify functionality - console.log('Manual bonus product collections created:', { - addedProducts: addedProductIds, - qualifyingProductChanges: detectionResult.qualifyingProductChanges, - newBonusProducts: detectionResult.newBonusProducts.map((bp) => ({ - productId: bp.productId, - productName: bp.productName, - quantity: bp.quantity, - itemId: bp.itemId, - promotionId: bp.promotionId, - bonusDiscountLineItemId: bp.bonusDiscountLineItemId, - bonusDiscountPromotionId: bp.bonusDiscountPromotionId - })), - qualifyingProductToBonusProducts: - detectionResult.qualifyingProductToBonusProducts - }) - } + await addItemToNewOrExistingBasket(productItems) einstein.sendAddToCart(productItems) @@ -318,47 +266,8 @@ export const useProductDetailData = () => { } ] - // Capture current basket state before adding items - const beforeBasket = currentBasket || {} - const res = await addItemToNewOrExistingBasket(productItems) - // Analyze qualifying product changes for bundle - const qualifyingProductChanges = analyzeQualifyingProductChanges(beforeBasket, res, [ - product.id - ]) - - // Detect newly added bonus products and their associations - const detectionResult = detectNewlyAddedBonusProducts( - beforeBasket, - res, - qualifyingProductChanges - ) - - // Create manual bonus product collections for the bundle product that was added - if (Object.keys(detectionResult.qualifyingProductToBonusProducts).length > 0) { - createManualBonusProductCollections( - detectionResult.qualifyingProductToBonusProducts - ) - - // Debug log to verify functionality - console.log('Manual bonus product collection created for bundle:', { - bundleProductId: product.id, - qualifyingProductChanges: detectionResult.qualifyingProductChanges, - newBonusProducts: detectionResult.newBonusProducts.map((bp) => ({ - productId: bp.productId, - productName: bp.productName, - quantity: bp.quantity, - itemId: bp.itemId, - promotionId: bp.promotionId, - bonusDiscountLineItemId: bp.bonusDiscountLineItemId, - bonusDiscountPromotionId: bp.bonusDiscountPromotionId - })), - qualifyingProductToBonusProducts: - detectionResult.qualifyingProductToBonusProducts - }) - } - const bundleChildMasterIds = childProductSelections.map((child) => { return child.product.id }) diff --git a/packages/template-chakra-storefront/src/routes.tsx b/packages/template-chakra-storefront/src/routes.tsx index 2d91d5e54f..ae169847a8 100644 --- a/packages/template-chakra-storefront/src/routes.tsx +++ b/packages/template-chakra-storefront/src/routes.tsx @@ -22,12 +22,6 @@ import {configureRoutes} from '../src/utils/routes-utils' const fallback = // Pages -const Home = loadable(() => import('../src/pages/home'), {fallback}) -const Login = loadable(() => import('../src/pages/login'), {fallback}) -const Registration = loadable(() => import('../src/pages/registration'), { - fallback -}) -const ResetPassword = loadable(() => import('../src/pages/reset-password'), {fallback}) const Account = loadable(() => import('../src/pages/account'), {fallback}) const Cart = loadable(() => import('../src/pages/cart'), {fallback}) const Checkout = loadable(() => import('../src/pages/checkout'), { @@ -36,18 +30,29 @@ const Checkout = loadable(() => import('../src/pages/checkout'), { const CheckoutConfirmation = loadable(() => import('../src/pages/checkout/confirmation'), { fallback }) -const SocialLoginRedirect = loadable(() => import('../src/pages/social-login-redirect'), {fallback}) + const LoginRedirect = loadable(() => import('../src/pages/login-redirect'), {fallback}) +const Login = loadable(() => import('../src/pages/login'), {fallback}) +const Home = loadable(() => import('../src/pages/home'), {fallback}) +const Registration = loadable(() => import('../src/pages/registration'), { + fallback +}) +const ResetPassword = loadable(() => import('../src/pages/reset-password'), {fallback}) const ProductDetail = loadable(() => import('../src/pages/product-detail'), {fallback}) const ProductList = loadable(() => import('../src/pages/product-list'), { fallback }) + +//@sfdc-extension-line SFDC_EXT_SOCIAL_LOGIN +const SocialLoginRedirect = loadable(() => import('../src/pages/social-login-redirect'), {fallback}) + // const StoreLocator = loadable(() => import('../src/pages/store-locator'), { // fallback // }) const Wishlist = loadable(() => import('../src/pages/account/wishlist'), { fallback }) + const PageNotFound = loadable(() => import('../src/pages/page-not-found')) export const routes = [ @@ -99,11 +104,13 @@ export const routes = [ component: LoginRedirect, exact: true }, + //@sfdc-extension-block-start SFDC_EXT_SOCIAL_LOGIN { path: '/social-callback', component: SocialLoginRedirect, exact: true }, + //@sfdc-extension-block-end SFDC_EXT_SOCIAL_LOGIN { path: '/cart', component: Cart, diff --git a/packages/template-chakra-storefront/src/theme/components/project/product-view-modal.js b/packages/template-chakra-storefront/src/theme/components/project/product-view-modal.js new file mode 100644 index 0000000000..384500fcc8 --- /dev/null +++ b/packages/template-chakra-storefront/src/theme/components/project/product-view-modal.js @@ -0,0 +1,49 @@ +/* + * 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 + */ + +export const productViewModalTheme = { + // Modal configuration + modal: { + size: {base: 'full', lg: 'lg', xl: 'xl'}, + placement: 'center', + scrollBehavior: 'inside', + closeOnInteractOutside: false + }, + + // Layout spacing and positioning + layout: { + content: { + // No margin for full utilization of modal space + margin: '0', + borderRadius: {base: 'none', md: 'base'}, + // Constrain height to prevent excessive modal size + maxHeight: '85vh', + overflowY: 'auto' + }, + body: { + // Adequate padding for product content + padding: 6, + paddingBottom: 8, + marginTop: 6, + // White background for product content + background: 'white' + } + }, + + // ProductView component configuration + productView: { + showFullLink: true, + imageSize: 'sm', + showImageGallery: true + }, + + // Color scheme + colors: { + background: 'white', + contentBackground: 'white' + } +} diff --git a/packages/template-chakra-storefront/translations/da-DK.json b/packages/template-chakra-storefront/translations/da-DK.json index e70311486d..649eb47b71 100644 --- a/packages/template-chakra-storefront/translations/da-DK.json +++ b/packages/template-chakra-storefront/translations/da-DK.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Log ind for at fortsætte!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonusproduktvalg modal for {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Se kurv" } } diff --git a/packages/template-chakra-storefront/translations/de-DE.json b/packages/template-chakra-storefront/translations/de-DE.json index 9963d80443..ee6de2ed64 100644 --- a/packages/template-chakra-storefront/translations/de-DE.json +++ b/packages/template-chakra-storefront/translations/de-DE.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Bitte melden Sie sich an, um fortzufahren." + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonusprodukt-Auswahlmodal für {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Warenkorb anzeigen" } } diff --git a/packages/template-chakra-storefront/translations/en-GB.json b/packages/template-chakra-storefront/translations/en-GB.json index 6b89d40b7e..40e1892095 100644 --- a/packages/template-chakra-storefront/translations/en-GB.json +++ b/packages/template-chakra-storefront/translations/en-GB.json @@ -1779,5 +1779,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Please sign in to continue!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonus product selection modal for {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "View Cart" } } diff --git a/packages/template-chakra-storefront/translations/en-US.json b/packages/template-chakra-storefront/translations/en-US.json index 6b89d40b7e..40e1892095 100644 --- a/packages/template-chakra-storefront/translations/en-US.json +++ b/packages/template-chakra-storefront/translations/en-US.json @@ -1779,5 +1779,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Please sign in to continue!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonus product selection modal for {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "View Cart" } } diff --git a/packages/template-chakra-storefront/translations/es-MX.json b/packages/template-chakra-storefront/translations/es-MX.json index 06e26a1306..f78ecc7c7c 100644 --- a/packages/template-chakra-storefront/translations/es-MX.json +++ b/packages/template-chakra-storefront/translations/es-MX.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "¡Regístrese para continuar!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Modal de selección de producto bonus para {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Ver carrito" } } diff --git a/packages/template-chakra-storefront/translations/fi-FI.json b/packages/template-chakra-storefront/translations/fi-FI.json index 9c44afe0a2..de631a8b03 100644 --- a/packages/template-chakra-storefront/translations/fi-FI.json +++ b/packages/template-chakra-storefront/translations/fi-FI.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Kirjaudu sisään jatkaaksesi!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonustuotteen valinta modal tuotteelle {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Näytä kori" } } diff --git a/packages/template-chakra-storefront/translations/fr-FR.json b/packages/template-chakra-storefront/translations/fr-FR.json index ffa1f0460f..85e7043b10 100644 --- a/packages/template-chakra-storefront/translations/fr-FR.json +++ b/packages/template-chakra-storefront/translations/fr-FR.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Veuillez vous connecter pour continuer." + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Modal de sélection de produit bonus pour {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Voir le panier" } } diff --git a/packages/template-chakra-storefront/translations/it-IT.json b/packages/template-chakra-storefront/translations/it-IT.json index 5e5231d6cb..7f10c45c18 100644 --- a/packages/template-chakra-storefront/translations/it-IT.json +++ b/packages/template-chakra-storefront/translations/it-IT.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Accedi per continuare!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Modal di selezione prodotto bonus per {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Visualizza carrello" } } diff --git a/packages/template-chakra-storefront/translations/ja-JP.json b/packages/template-chakra-storefront/translations/ja-JP.json index ace42ff5bb..9892fc3be7 100644 --- a/packages/template-chakra-storefront/translations/ja-JP.json +++ b/packages/template-chakra-storefront/translations/ja-JP.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "先に進むにはサインインしてください!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "{productName}のボーナス商品選択モーダル" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "カートを見る" } } diff --git a/packages/template-chakra-storefront/translations/ko-KR.json b/packages/template-chakra-storefront/translations/ko-KR.json index 17ff66f436..089f5d71cc 100644 --- a/packages/template-chakra-storefront/translations/ko-KR.json +++ b/packages/template-chakra-storefront/translations/ko-KR.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "계속하려면 로그인하십시오." + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "{productName}의 보너스 제품 선택 모달" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "장바구니 보기" } } diff --git a/packages/template-chakra-storefront/translations/nl-NL.json b/packages/template-chakra-storefront/translations/nl-NL.json index 4236241f52..c39ca12590 100644 --- a/packages/template-chakra-storefront/translations/nl-NL.json +++ b/packages/template-chakra-storefront/translations/nl-NL.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Meld je aan om verder te gaan." + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonusproduct selectie modal voor {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Bekijk winkelwagen" } } diff --git a/packages/template-chakra-storefront/translations/no-NO.json b/packages/template-chakra-storefront/translations/no-NO.json index 4404bef2b9..c00a836960 100644 --- a/packages/template-chakra-storefront/translations/no-NO.json +++ b/packages/template-chakra-storefront/translations/no-NO.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Logg på for å fortsette." + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonusproduktvalg modal for {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Vis handlekurv" } } diff --git a/packages/template-chakra-storefront/translations/pl-PL.json b/packages/template-chakra-storefront/translations/pl-PL.json index 93dda25bfe..bd070d9207 100644 --- a/packages/template-chakra-storefront/translations/pl-PL.json +++ b/packages/template-chakra-storefront/translations/pl-PL.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Zaloguj się, aby kontynuować." + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Modal wyboru produktu bonusowego dla {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Zobacz koszyk" } } diff --git a/packages/template-chakra-storefront/translations/pt-BR.json b/packages/template-chakra-storefront/translations/pt-BR.json index 47ca876f90..5cb4f27200 100644 --- a/packages/template-chakra-storefront/translations/pt-BR.json +++ b/packages/template-chakra-storefront/translations/pt-BR.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Faça logon para continuar!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Modal de seleção de produto bônus para {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Ver carrinho" } } diff --git a/packages/template-chakra-storefront/translations/sv-SE.json b/packages/template-chakra-storefront/translations/sv-SE.json index 6b50ab0ea1..3991aa23fa 100644 --- a/packages/template-chakra-storefront/translations/sv-SE.json +++ b/packages/template-chakra-storefront/translations/sv-SE.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "Logga in för att fortsätta!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "Bonusproduktval modal för {productName}" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "Visa kundvagn" } } diff --git a/packages/template-chakra-storefront/translations/zh-CN.json b/packages/template-chakra-storefront/translations/zh-CN.json index 1fd581d6b0..b6c7404212 100644 --- a/packages/template-chakra-storefront/translations/zh-CN.json +++ b/packages/template-chakra-storefront/translations/zh-CN.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "请登录以继续!" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "{productName}的奖励产品选择模态框" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "查看购物车" } } diff --git a/packages/template-chakra-storefront/translations/zh-TW.json b/packages/template-chakra-storefront/translations/zh-TW.json index 2589489cf6..6ccee4d8bb 100644 --- a/packages/template-chakra-storefront/translations/zh-TW.json +++ b/packages/template-chakra-storefront/translations/zh-TW.json @@ -1776,5 +1776,11 @@ }, "with_registration.info.please_sign_in": { "defaultMessage": "請登入以繼續。" + }, + "bonus_product_view_modal.modal_label": { + "defaultMessage": "{productName}的獎勵產品選擇模態框" + }, + "bonus_product_view_modal.button.view_cart": { + "defaultMessage": "查看購物車" } } From 7c993a5a40828c2e0f610193ad54bebdb85fae56 Mon Sep 17 00:00:00 2001 From: Shikhar Prasoon <214730309+sf-shikhar-prasoon@users.noreply.github.com> Date: Fri, 15 Aug 2025 03:50:42 -0400 Subject: [PATCH 9/9] fix data flow --- .../src/hooks/use-add-to-cart-modal.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js index a4a6f01dc2..015a4f81ec 100644 --- a/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js +++ b/packages/template-chakra-storefront/src/hooks/use-add-to-cart-modal.js @@ -77,9 +77,18 @@ export const AddToCartModal = ({onSelectBonusProductsClick}) => { const {isOpen, onClose, data} = useAddToCartModalContext() const bonusProductContext = useBonusProductSelectionModalContext() const {onOpen: onOpenBonusModal} = bonusProductContext || {} - const {product, itemsAdded = [], selectedQuantity, bonusDiscountLineItems = []} = data || {} + const {product, itemsAdded = [], selectedQuantity} = data || {} const isProductABundle = !!product?.type.bundle + const intl = useIntl() + const {formatMessage} = intl + const { + data: basket = {}, + derivedData: {totalItems} + } = useCurrentBasket() + + const {bonusDiscountLineItems = []} = basket || {} + // Extract unique promotion IDs const promotionIds = [ ...new Set(bonusDiscountLineItems.map((item) => item.promotionId).filter(Boolean)) @@ -92,12 +101,6 @@ export const AddToCartModal = ({onSelectBonusProductsClick}) => { // Get the first promotion's details const promotionText = promotions?.data?.[0]?.details || '' - const intl = useIntl() - const {formatMessage} = intl - const { - data: basket = {}, - derivedData: {totalItems} - } = useCurrentBasket() const size = useBreakpointValue(addToCartModalTheme.modal.size) const {currency, productSubTotal} = basket const numberOfItemsAdded = isProductABundle