{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 */}
>
) : (
- {!showPasswordField ? messages.alreadyHaveAccount : messages.checkoutAsGuest}
-
+ <>
+
+ {!showPasswordField ? messages.alreadyHaveAccount : messages.checkoutAsGuest}
+
+ {/* @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