-
Notifications
You must be signed in to change notification settings - Fork 201
@W-19425801 Guest Shopper Flow #3417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
6c4a080
2e44b19
7f40cd0
b0d71c0
a526766
b0a763f
2aaa491
4b3b755
0e17ec6
4780100
fed914f
a56a470
eb9b109
2c0aaeb
d41dd32
1d771e7
f895d60
da29757
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| /* | ||
| * 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 | ||
| */ | ||
| import {useCommerceApi} from '@salesforce/commerce-sdk-react' | ||
| import useAuthContext from '@salesforce/commerce-sdk-react/hooks/useAuthContext' | ||
| import {useShopperBasketsMutation} from '@salesforce/commerce-sdk-react' | ||
|
|
||
| /** | ||
| * Reusable basket recovery hook to stabilize basket after OTP/auth swap. | ||
| * - Attempts merge (if caller already merged, pass skipMerge=true) | ||
| * - Hydrates destination basket by id with retry | ||
| * - Fallbacks to create/copy items and re-apply shipping | ||
| */ | ||
| const useBasketRecovery = () => { | ||
| const api = useCommerceApi() | ||
| const auth = useAuthContext() | ||
| // const currentBasketQuery = useCurrentBasket() | ||
|
|
||
| const mergeBasket = useShopperBasketsMutation('mergeBasket') | ||
| const createBasket = useShopperBasketsMutation('createBasket') | ||
| const addItemToBasket = useShopperBasketsMutation('addItemToBasket') | ||
| const updateShippingAddressForShipment = useShopperBasketsMutation( | ||
| 'updateShippingAddressForShipment' | ||
| ) | ||
| const updateShippingMethodForShipment = useShopperBasketsMutation( | ||
| 'updateShippingMethodForShipment' | ||
| ) | ||
|
|
||
| const copyItemsAndShipping = async ( | ||
| destBasketId, | ||
| items = [], | ||
| shipmentSnapshot = null, | ||
| shipmentId = 'me' | ||
| ) => { | ||
| if (items?.length) { | ||
| const payload = items.map((item) => { | ||
| const productId = item.productId || item.product_id || item.id || item.product?.id | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where do all these variations come from? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it somehow stem from ocapi vs scapi response? Wonder if we should 'normalize' the object prior tothis
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. list products vs get product API response differences. would be nice to have a centralized normalization function |
||
| const quantity = item.quantity || item.amount || 1 | ||
| const variationAttributes = | ||
| item.variationAttributes || item.variation_attributes || [] | ||
| const optionItems = item.optionItems || item.option_items || [] | ||
| const mappedVariations = Array.isArray(variationAttributes) | ||
| ? variationAttributes.map((v) => ({ | ||
| attributeId: v.attributeId || v.attribute_id || v.id, | ||
| valueId: v.valueId || v.value_id || v.value | ||
| })) | ||
| : [] | ||
| const mappedOptions = Array.isArray(optionItems) | ||
| ? optionItems.map((o) => ({ | ||
| optionId: o.optionId || o.option_id || o.id, | ||
| optionValueId: | ||
| o.optionValueId || o.optionValue || o.option_value || o.value | ||
| })) | ||
| : [] | ||
| const obj = {productId, quantity} | ||
| if (mappedVariations.length) obj.variationAttributes = mappedVariations | ||
| if (mappedOptions.length) obj.optionItems = mappedOptions | ||
| return obj | ||
| }) | ||
| await addItemToBasket.mutateAsync({parameters: {basketId: destBasketId}, body: payload}) | ||
| } | ||
|
|
||
| if (shipmentSnapshot) { | ||
| const shippingAddress = shipmentSnapshot.shippingAddress | ||
| if (shippingAddress) { | ||
| await updateShippingAddressForShipment.mutateAsync({ | ||
| parameters: {basketId: destBasketId, shipmentId}, | ||
| body: { | ||
| address1: shippingAddress.address1, | ||
| address2: shippingAddress.address2, | ||
| city: shippingAddress.city, | ||
| countryCode: shippingAddress.countryCode, | ||
| firstName: shippingAddress.firstName, | ||
| lastName: shippingAddress.lastName, | ||
| phone: shippingAddress.phone, | ||
| postalCode: shippingAddress.postalCode, | ||
| stateCode: shippingAddress.stateCode | ||
| } | ||
| }) | ||
| } | ||
| const methodId = shipmentSnapshot?.shippingMethod?.id | ||
| if (methodId) { | ||
| await updateShippingMethodForShipment.mutateAsync({ | ||
| parameters: {basketId: destBasketId, shipmentId}, | ||
| body: {id: methodId} | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const recoverBasketAfterAuth = async ({ | ||
| preLoginItems = [], | ||
| shipmentSnapshot = null, | ||
| doMerge = true | ||
| } = {}) => { | ||
| // Ensure fresh token in provider | ||
| await auth.refreshAccessToken() | ||
| // Defer invalidation to the end to avoid duplicate basket/shipping-method refetches | ||
|
|
||
| let destId | ||
| if (doMerge) { | ||
| try { | ||
| const merged = await mergeBasket.mutateAsync({ | ||
| parameters: {createDestinationBasket: true} | ||
| }) | ||
| destId = merged?.basketId || merged?.basket_id || merged?.id | ||
| } catch (_e) { | ||
| /* noop */ | ||
| } | ||
| } | ||
|
|
||
| if (!destId) { | ||
| try { | ||
| const list = await api.shopperCustomers.getCustomerBaskets({ | ||
| parameters: {customerId: 'me'} | ||
| }) | ||
| destId = list?.baskets?.[0]?.basketId | ||
| } catch (_e) { | ||
| /* noop */ | ||
| } | ||
| } | ||
|
|
||
| if (destId) { | ||
| // Avoid triggering a hook-level refetch that can cause UI remounts. | ||
| // Instead, probe the destination basket directly for shipment id. | ||
| let hydrated = null | ||
| try { | ||
| hydrated = await api.shopperBaskets.getBasket({ | ||
| headers: {authorization: `Bearer ${auth.get('access_token')}`}, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this sort of thing the responsibility of the SDK? I haven't seen any other API requests have inline token management. Why exactly is this required?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somehow the user context isn't refreshed when a guest becomes a registered user and we get stale basket or we lose the merged basket. This is an attempt to get the registered user's basket by explicitly specifying the access token. We will be re-visiting the logic in this file in another ticket. I will leave this as-is for now. |
||
| parameters: {basketId: destId} | ||
| }) | ||
| } catch (_e) { | ||
| hydrated = null | ||
| } | ||
| if (!hydrated) { | ||
| try { | ||
| const created = await createBasket.mutateAsync({}) | ||
| destId = created?.basketId || created?.basket_id || created?.id || destId | ||
| await copyItemsAndShipping(destId, preLoginItems, shipmentSnapshot) | ||
| } catch (_e) { | ||
| /* noop */ | ||
| } | ||
| } else if (shipmentSnapshot) { | ||
| // PII (shipping address/method) is not merged by API; re-apply from snapshot | ||
| try { | ||
| const effectiveDestId = hydrated?.basketId || destId | ||
| const destShipmentId = | ||
| hydrated?.shipments?.[0]?.shipmentId || hydrated?.shipments?.[0]?.id || 'me' | ||
| await copyItemsAndShipping( | ||
| effectiveDestId, | ||
| [], | ||
| shipmentSnapshot, | ||
| destShipmentId | ||
| ) | ||
| } catch (_e) { | ||
| /* noop */ | ||
| } | ||
| } | ||
| } else { | ||
| try { | ||
| const created = await createBasket.mutateAsync({}) | ||
| destId = created?.basketId || created?.basket_id || created?.id | ||
| await copyItemsAndShipping(destId, preLoginItems, shipmentSnapshot) | ||
| } catch (_e) { | ||
| /* noop */ | ||
| } | ||
| } | ||
|
|
||
| return destId | ||
| } | ||
|
|
||
| return {recoverBasketAfterAuth} | ||
| } | ||
|
|
||
| export default useBasketRecovery | ||
Uh oh!
There was an error while loading. Please reload this page.