Skip to content

Commit a53ade7

Browse files
authored
@W-19685609 Express on PDP with Temporary Basket (#3474)
* @W-19685609 Express on PDP with Temporary Basket * Address Code Review: Fix comments, remove eagerly created validations, move temp basket to its a hook, i10n for errors, etc * Address Code Review: Fix additional comments, i10 labels, set keepPreviousData to false by default in current basket hook * Address Code Review: do not set keepPreviousData flag to true in useCurrentBasket hook until further testing confirms that it is needed * Address updates to SDK event changes in payment sheet * Remove the optional keepPreviousData property setting in usecurrentbasket hook * Undo i10 call for an error that nees to be looged into console only * Reset maximumButtonCount to 1 * Remove commented line * Replace undefined with empty function for onclick action in payment sheet
1 parent ba5e3d4 commit a53ade7

File tree

18 files changed

+919
-292
lines changed

18 files changed

+919
-292
lines changed

packages/template-retail-react-app/app/components/product-view/index.jsx

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import React, {forwardRef, useEffect, useMemo, useRef, useState} from 'react'
8+
import React, {forwardRef, useEffect, useMemo, useRef, useState, useCallback} from 'react'
99
import PropTypes from 'prop-types'
1010
import {useLocation} from 'react-router-dom'
1111
import {useIntl, FormattedMessage} from 'react-intl'
@@ -34,7 +34,7 @@ import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/h
3434
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'
3535
import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants'
3636
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
37-
import {useShopperBasketsMutationHelper} from '@salesforce/commerce-sdk-react'
37+
import {useShopperBasketsMutation} from '@salesforce/commerce-sdk-react'
3838
import {
3939
useSFPaymentsEnabled,
4040
useSFPayments
@@ -58,6 +58,7 @@ import PromoCallout from '@salesforce/retail-react-app/app/components/product-ti
5858
import SFPaymentsExpressButtons from '@salesforce/retail-react-app/app/components/sf-payments-express-buttons'
5959
import {EXPRESS_BUY_NOW} from '@salesforce/retail-react-app/app/hooks/use-sf-payments'
6060
import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
61+
import {useCleanupTemporaryBaskets} from '@salesforce/retail-react-app/app/hooks/use-cleanup-temporary-baskets'
6162

6263
const ProductViewHeader = ({
6364
name,
@@ -173,7 +174,10 @@ const ProductView = forwardRef(
173174
onOpen: onAddToCartModalOpen,
174175
onClose: onAddToCartModalClose
175176
} = useAddToCartModalContext()
176-
const {addItemToNewOrExistingBasket} = useShopperBasketsMutationHelper()
177+
178+
const {mutateAsync: createBasket} = useShopperBasketsMutation('createBasket')
179+
const {mutateAsync: addItemToBasket} = useShopperBasketsMutation('addItemToBasket')
180+
177181
const theme = useTheme()
178182
const {confirmingBasket} = useSFPayments()
179183
const [showOptionsMessage, toggleShowOptionsMessage] = useState(false)
@@ -268,6 +272,69 @@ const ProductView = forwardRef(
268272

269273
return hasValidSelection
270274
}
275+
const cleanupTemporaryBaskets = useCleanupTemporaryBaskets()
276+
277+
// prepareBasket is used to prepare the basket for express payments
278+
// useCallback recreates prepareBasket primarily when product or quantity change, along with variant, stockLevel, and product type flags (isProductASet, isProductABundle)
279+
const prepareBasket = useCallback(async () => {
280+
// Validate that all attributes are selected before proceeding
281+
const hasValidSelection = validateOrderability(variant, product, quantity, stockLevel)
282+
let errorMessage = ''
283+
284+
if (!hasValidSelection && !isProductASet && !isProductABundle) {
285+
toggleShowOptionsMessage(true)
286+
if (errorContainerRef.current) {
287+
errorContainerRef.current.scrollIntoView({
288+
behavior: 'smooth',
289+
block: 'center'
290+
})
291+
}
292+
errorMessage = intl.formatMessage({
293+
defaultMessage:
294+
'Please select all product options before proceeding with Express Payments',
295+
id: 'product_view.prepareBasket'
296+
})
297+
298+
const error = new Error(errorMessage)
299+
error.isValidationError = true
300+
throw error
301+
}
302+
303+
// Clean up temporary baskets before creating a new one
304+
await cleanupTemporaryBaskets()
305+
306+
// Create a new temporary basket
307+
const newBasket = await createBasket({
308+
parameters: {
309+
temporary: true
310+
},
311+
body: {}
312+
})
313+
314+
const selectedProduct = variant || product
315+
// Use variant's productId if variant is selected, otherwise use product's id
316+
const productIdToUse = selectedProduct?.productId || selectedProduct?.id
317+
318+
if (!productIdToUse) {
319+
errorMessage = intl.formatMessage({
320+
defaultMessage: 'Unable to determine product ID for basket',
321+
id: 'product_view.prepareBasket'
322+
})
323+
throw new Error(errorMessage)
324+
}
325+
// Add the product to the temporary basket
326+
const basketWithItem = await addItemToBasket({
327+
parameters: {basketId: newBasket.basketId},
328+
body: [
329+
{
330+
productId: productIdToUse,
331+
quantity: quantity
332+
}
333+
]
334+
})
335+
336+
return basketWithItem
337+
}, [variant, product, quantity, stockLevel, isProductASet, isProductABundle])
271338

272339
const renderActionButtons = () => {
273340
const buttons = []
@@ -347,15 +414,6 @@ const ProductView = forwardRef(
347414
addToWishlist(product, variant, quantity)
348415
}
349416

350-
const prepareBasket = async () => {
351-
return addItemToNewOrExistingBasket([
352-
{
353-
productId: variant?.productId || product.id,
354-
quantity: quantity
355-
}
356-
])
357-
}
358-
359417
// child product of bundles do not have add to cart button
360418
if ((addToCart || updateCart) && !isProductPartOfBundle) {
361419
buttons.push(

packages/template-retail-react-app/app/components/product-view/index.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import userEvent from '@testing-library/user-event'
2121
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
2222
import frMessages from '@salesforce/retail-react-app/app/static/translations/compiled/fr-FR.json'
2323
import {useSelectedStore} from '@salesforce/retail-react-app/app/hooks/use-selected-store'
24+
import {rest} from 'msw'
2425

2526
// Ensure useMultiSite returns site.id = 'site-1' for all tests
2627
jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({
@@ -72,7 +73,63 @@ beforeEach(() => {
7273
error: null,
7374
hasSelectedStore: true
7475
}))
76+
77+
// Reset MSW handlers to avoid conflicts
78+
global.server.resetHandlers()
79+
80+
// Add MSW handlers to avoid 403 errors
81+
global.server.use(
82+
rest.get('*/customers/:customerId/product-lists', (req, res, ctx) => {
83+
return res(
84+
ctx.delay(0),
85+
ctx.status(200),
86+
ctx.json({
87+
data: [],
88+
total: 0
89+
})
90+
)
91+
}),
92+
rest.post('*/customers/:customerId/product-lists', (req, res, ctx) => {
93+
return res(
94+
ctx.delay(0),
95+
ctx.status(200),
96+
ctx.json({
97+
id: 'test-list-id',
98+
type: 'wish_list'
99+
})
100+
)
101+
}),
102+
rest.get('*/configuration/shopper-configurations/*', (req, res, ctx) => {
103+
return res(
104+
ctx.delay(0),
105+
ctx.status(200),
106+
ctx.json({
107+
configurations: []
108+
})
109+
)
110+
}),
111+
rest.get('*/product/shopper-products/*', (req, res, ctx) => {
112+
return res(
113+
ctx.delay(0),
114+
ctx.status(200),
115+
ctx.json({
116+
data: []
117+
})
118+
)
119+
}),
120+
rest.get('*/api/payment-metadata', (req, res, ctx) => {
121+
return res(
122+
ctx.delay(0),
123+
ctx.status(200),
124+
ctx.json({
125+
apiKey: 'test-key',
126+
publishableKey: 'pk_test'
127+
})
128+
)
129+
})
130+
)
75131
})
132+
76133
afterEach(() => {
77134
jest.clearAllMocks()
78135
sessionStorage.clear()

0 commit comments

Comments
 (0)