Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import React, {forwardRef, useEffect, useMemo, useRef, useState} from 'react'
import React, {forwardRef, useEffect, useMemo, useRef, useState, useCallback} from 'react'
import PropTypes from 'prop-types'
import {useLocation} from 'react-router-dom'
import {useIntl, FormattedMessage} from 'react-intl'
Expand Down Expand Up @@ -34,7 +34,7 @@ import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/h
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {useShopperBasketsMutationHelper} from '@salesforce/commerce-sdk-react'
import {useShopperBasketsMutation} from '@salesforce/commerce-sdk-react'
import {
useSFPaymentsEnabled,
useSFPayments
Expand All @@ -58,6 +58,7 @@ import PromoCallout from '@salesforce/retail-react-app/app/components/product-ti
import SFPaymentsExpressButtons from '@salesforce/retail-react-app/app/components/sf-payments-express-buttons'
import {EXPRESS_BUY_NOW} from '@salesforce/retail-react-app/app/hooks/use-sf-payments'
import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
import {useCleanupTemporaryBaskets} from '@salesforce/retail-react-app/app/hooks/use-cleanup-temporary-baskets'

const ProductViewHeader = ({
name,
Expand Down Expand Up @@ -173,7 +174,10 @@ const ProductView = forwardRef(
onOpen: onAddToCartModalOpen,
onClose: onAddToCartModalClose
} = useAddToCartModalContext()
const {addItemToNewOrExistingBasket} = useShopperBasketsMutationHelper()

const {mutateAsync: createBasket} = useShopperBasketsMutation('createBasket')
const {mutateAsync: addItemToBasket} = useShopperBasketsMutation('addItemToBasket')

const theme = useTheme()
const {confirmingBasket} = useSFPayments()
const [showOptionsMessage, toggleShowOptionsMessage] = useState(false)
Expand Down Expand Up @@ -268,6 +272,69 @@ const ProductView = forwardRef(

return hasValidSelection
}
const cleanupTemporaryBaskets = useCleanupTemporaryBaskets()

// prepareBasket is used to prepare the basket for express payments
// useCallback recreates prepareBasket primarily when product or quantity change, along with variant, stockLevel, and product type flags (isProductASet, isProductABundle)
const prepareBasket = useCallback(async () => {
// Validate that all attributes are selected before proceeding
const hasValidSelection = validateOrderability(variant, product, quantity, stockLevel)
let errorMessage = ''

if (!hasValidSelection && !isProductASet && !isProductABundle) {
toggleShowOptionsMessage(true)
if (errorContainerRef.current) {
errorContainerRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
}
errorMessage = intl.formatMessage({
defaultMessage:
'Please select all product options before proceeding with Express Payments',
id: 'product_view.prepareBasket'
})

const error = new Error(errorMessage)
error.isValidationError = true
throw error
}

// Clean up temporary baskets before creating a new one
await cleanupTemporaryBaskets()

// Create a new temporary basket
const newBasket = await createBasket({
parameters: {
temporary: true
},
body: {}
})

const selectedProduct = variant || product
// Use variant's productId if variant is selected, otherwise use product's id
const productIdToUse = selectedProduct?.productId || selectedProduct?.id

if (!productIdToUse) {
errorMessage = intl.formatMessage({
defaultMessage: 'Unable to determine product ID for basket',
id: 'product_view.prepareBasket'
})
throw new Error(errorMessage)
}
// Add the product to the temporary basket
const basketWithItem = await addItemToBasket({
parameters: {basketId: newBasket.basketId},
body: [
{
productId: productIdToUse,
quantity: quantity
}
]
})

return basketWithItem
}, [variant, product, quantity, stockLevel, isProductASet, isProductABundle])

const renderActionButtons = () => {
const buttons = []
Expand Down Expand Up @@ -347,15 +414,6 @@ const ProductView = forwardRef(
addToWishlist(product, variant, quantity)
}

const prepareBasket = async () => {
return addItemToNewOrExistingBasket([
{
productId: variant?.productId || product.id,
quantity: quantity
}
])
}

// child product of bundles do not have add to cart button
if ((addToCart || updateCart) && !isProductPartOfBundle) {
buttons.push(
Expand Down Expand Up @@ -418,7 +476,7 @@ const ProductView = forwardRef(
initialAmount={priceData.currentPrice}
prepareBasket={prepareBasket}
expressButtonLayout="vertical"
maximumButtonCount={1}
maximumButtonCount={3}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import userEvent from '@testing-library/user-event'
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
import frMessages from '@salesforce/retail-react-app/app/static/translations/compiled/fr-FR.json'
import {useSelectedStore} from '@salesforce/retail-react-app/app/hooks/use-selected-store'
import {rest} from 'msw'

// Ensure useMultiSite returns site.id = 'site-1' for all tests
jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({
Expand Down Expand Up @@ -72,7 +73,63 @@ beforeEach(() => {
error: null,
hasSelectedStore: true
}))

// Reset MSW handlers to avoid conflicts
global.server.resetHandlers()

// Add MSW handlers to avoid 403 errors
global.server.use(
rest.get('*/customers/:customerId/product-lists', (req, res, ctx) => {
return res(
ctx.delay(0),
ctx.status(200),
ctx.json({
data: [],
total: 0
})
)
}),
rest.post('*/customers/:customerId/product-lists', (req, res, ctx) => {
return res(
ctx.delay(0),
ctx.status(200),
ctx.json({
id: 'test-list-id',
type: 'wish_list'
})
)
}),
rest.get('*/configuration/shopper-configurations/*', (req, res, ctx) => {
return res(
ctx.delay(0),
ctx.status(200),
ctx.json({
configurations: []
})
)
}),
rest.get('*/product/shopper-products/*', (req, res, ctx) => {
return res(
ctx.delay(0),
ctx.status(200),
ctx.json({
data: []
})
)
}),
rest.get('*/api/payment-metadata', (req, res, ctx) => {
return res(
ctx.delay(0),
ctx.status(200),
ctx.json({
apiKey: 'test-key',
publishableKey: 'pk_test'
})
)
})
)
})

afterEach(() => {
jest.clearAllMocks()
sessionStorage.clear()
Expand Down
Loading
Loading