Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,77 @@ const ProductView = forwardRef(

return hasValidSelection
}
const cleanupTemporaryBaskets = useCleanupTemporaryBaskets()

// prepareBasket is used to prepare the basket for express payments
// useCallback ensures the function reference is stable across renders
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: {}
})

if (!newBasket || !newBasket.basketId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a case where basketId might be null/undefined for a created basket?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This check matches the pattern in useShopperBasketsMutationHelper (helpers.ts:65).
If the API contract guarantees basketId is always present on successful createBasket
responses, we could simplify to just check !newBasket. Should I update both places thought didn't want to mess with the sdk package unless I need to?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the reference to the helper. Best I can tell, that check was done because basketId is passed into another mutation immediately after the check. You also pass the newly created basket's basketId into addItemToBasket so I agree the cases are similar.

The SCAPI docs do say a successful basket creation will contain a basketId. In that sense, it would be unexpected and obviously not backward compatible should the API not return it.

This means if you leave the check here, you either write coverage for the conditional branch or you don't. If you write it, then you'd be able to validate what the code would do if this incompatible case ever happened. If you don't write it, then IMO you'd be saying "this case will never happen anyway so why bother".

If it were me I'd avoid the decision altogether by leaving out the check. Less code, easier to understand, focusing the reference application logic on a case that actually might happen - what to do if the API fails to create a new temporary basket for this shopper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can remove it since it was added to be defensive. The caller of prepareBasket (express buttons) has error handling. Looks like mutation calls (ex: createBasket) throw errors (if its not 200) unless I am not following the commerce-sdk-isomorphic rep right.

errorMessage = intl.formatMessage({
defaultMessage: 'Failed to create temporary basket',
id: 'product_view.prepareBasket'
})
throw new Error(errorMessage)
}

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 +422,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 +484,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