From b095714d0b30ba67065a05b6b52c9f2e5d890f2b Mon Sep 17 00:00:00 2001 From: Sushma Yadupathi Date: Tue, 3 Feb 2026 15:37:42 -0500 Subject: [PATCH 1/2] W-21005976 Save newly registered user's info when they leave checkout and return --- .../app/pages/checkout-one-click/index.jsx | 84 +----------------- .../partials/one-click-payment.jsx | 14 +++ .../partials/one-click-user-registration.jsx | 88 ++++++++++++++++++- .../one-click-user-registration.test.js | 20 ++++- 4 files changed, 120 insertions(+), 86 deletions(-) diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx index 5f7b338393..4032801fd6 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx @@ -55,12 +55,11 @@ import { getPaymentInstrumentCardType, getMaskCreditCardNumber } from '@salesforce/retail-react-app/app/utils/cc-utils' -import {nanoid} from 'nanoid' const CheckoutOneClick = () => { const {formatMessage} = useIntl() const navigate = useNavigation() - const {step, STEPS, contactPhone} = useCheckout() + const {step, STEPS} = useCheckout() const showToast = useToast() const [isLoading, setIsLoading] = useState(false) const [enableUserRegistration, setEnableUserRegistration] = useState(false) @@ -119,8 +118,6 @@ const CheckoutOneClick = () => { ShopperBasketsMutations.UpdateBillingAddressForBasket ) const {mutateAsync: createOrder} = useShopperOrdersMutation(ShopperOrdersMutations.CreateOrder) - const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress') - const updateCustomer = useShopperCustomersMutation('updateCustomer') const handleSavePreferenceChange = (shouldSave) => { setShouldSavePaymentMethod(shouldSave) @@ -382,85 +379,6 @@ const CheckoutOneClick = () => { fullCardDetails ) } - - // For newly registered guests only, persist shipping address when billing same as shipping - // Skip saving pickup/store addresses - only save delivery addresses - // For multi-shipment orders, save all delivery addresses with the first one as default - if ( - enableUserRegistration && - currentCustomer?.isRegistered && - !registeredUserChoseGuest - ) { - try { - const customerId = order.customerInfo?.customerId - if (!customerId) return - - // Get all delivery shipments (not pickup) from the order - // This handles both single delivery and multi-shipment orders - // For BOPIS orders, pickup shipments are filtered out - const deliveryShipments = - order?.shipments?.filter( - (shipment) => - !isPickupShipment(shipment) && shipment.shippingAddress - ) || [] - - if (deliveryShipments.length > 0) { - // Save all delivery addresses, with the first one as preferred - for (let i = 0; i < deliveryShipments.length; i++) { - const shipment = deliveryShipments[i] - const shipping = shipment.shippingAddress - if (!shipping) continue - - // Whitelist fields and strip non-customer fields (e.g., id, _type) - const { - address1, - address2, - city, - countryCode, - firstName, - lastName, - phone, - postalCode, - stateCode - } = shipping || {} - - await createCustomerAddress.mutateAsync({ - parameters: {customerId}, - body: { - addressId: nanoid(), - preferred: i === 0, // First address is preferred - address1, - address2, - city, - countryCode, - firstName, - lastName, - phone, - postalCode, - stateCode - } - }) - } - } - - // Persist phone number as phoneHome for newly registered guest shoppers - const phoneHome = basket?.billingAddress?.phone || contactPhone - if (phoneHome) { - await updateCustomer.mutateAsync({ - parameters: {customerId}, - body: {phoneHome} - }) - } - } catch (_e) { - // Only surface error if shopper opted to register/save details; otherwise fail silently - showError( - formatMessage({ - id: 'checkout.error.cannot_save_address', - defaultMessage: 'Could not save shipping address.' - }) - ) - } - } } navigate(`/checkout/confirmation/${order.orderNo}`) diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx index 790cc5eab4..e908241efc 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx @@ -126,6 +126,20 @@ const Payment = ({ } }, [shouldSavePaymentMethod, onSavePreferenceChange]) + // When a newly registered shopper leaves checkout and returns, they have no saved payments yet. + // Auto-check "Save for future use" so the next card they enter gets saved to their profile. + const hasAutoCheckedSaveForNewlyRegisteredRef = useRef(false) + useEffect(() => { + if (hasAutoCheckedSaveForNewlyRegisteredRef.current) return + if (!customer?.isRegistered) return + const hasNoSavedPayments = + !customer?.paymentInstruments || customer.paymentInstruments.length === 0 + if (hasNoSavedPayments) { + hasAutoCheckedSaveForNewlyRegisteredRef.current = true + setShouldSavePaymentMethod(true) + } + }, [customer?.isRegistered, customer?.paymentInstruments]) + // Handles user registration checkbox toggle (OTP handled by UserRegistration) const onUserRegistrationToggle = async (checked) => { setEnableUserRegistration(checked) diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.jsx index ca6612ce4a..5c918bef48 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.jsx @@ -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, {useRef, useState, useEffect} from 'react' -import {FormattedMessage} from 'react-intl' +import {FormattedMessage, useIntl} from 'react-intl' import PropTypes from 'prop-types' import { Box, @@ -23,7 +23,12 @@ import { import OtpAuth from '@salesforce/retail-react-app/app/components/otp-auth' import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket' import {useCustomerType, useAuthHelper, AuthHelpers} from '@salesforce/commerce-sdk-react' +import {useShopperCustomersMutation} from '@salesforce/commerce-sdk-react' import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site' +import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context' +import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast' +import {isPickupShipment} from '@salesforce/retail-react-app/app/utils/shipment-utils' +import {nanoid} from 'nanoid' export default function UserRegistration({ enableUserRegistration, @@ -36,15 +41,28 @@ export default function UserRegistration({ onLoadingChange }) { const {data: basket} = useCurrentBasket() + const {contactPhone} = useCheckout() const {isGuest} = useCustomerType() const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless) const loginPasswordless = useAuthHelper(AuthHelpers.LoginPasswordlessUser) const {locale} = useMultiSite() + const {formatMessage} = useIntl() + const showToast = useToast() + const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress') + const updateCustomer = useShopperCustomersMutation('updateCustomer') + const {isOpen: isOtpOpen, onOpen: onOtpOpen, onClose: onOtpClose} = useDisclosure() const otpSentRef = useRef(false) const [registrationSucceeded, setRegistrationSucceeded] = useState(false) const [isLoadingOtp, setIsLoadingOtp] = useState(false) + const showError = (message) => { + showToast({ + title: message, + status: 'error' + }) + } + const handleOtpClose = () => { otpSentRef.current = false onOtpClose() @@ -92,13 +110,79 @@ export default function UserRegistration({ } }, [isOtpOpen, isLoadingOtp, onLoadingChange]) + const saveAddressesAndPhoneToProfile = async (customerId) => { + if (!basket || !customerId) return + const deliveryShipments = + basket.shipments?.filter( + (shipment) => !isPickupShipment(shipment) && shipment.shippingAddress + ) || [] + try { + if (deliveryShipments.length > 0) { + for (let i = 0; i < deliveryShipments.length; i++) { + const shipment = deliveryShipments[i] + const shipping = shipment.shippingAddress + if (!shipping) continue + + const { + address1, + address2, + city, + countryCode, + firstName, + lastName, + phone, + postalCode, + stateCode + } = shipping || {} + + await createCustomerAddress.mutateAsync({ + parameters: {customerId}, + body: { + addressId: nanoid(), + preferred: i === 0, + address1, + address2, + city, + countryCode, + firstName, + lastName, + phone, + postalCode, + stateCode + } + }) + } + } + + const phoneHome = basket.billingAddress?.phone || contactPhone + if (phoneHome) { + await updateCustomer.mutateAsync({ + parameters: {customerId}, + body: {phoneHome} + }) + } + } catch (_e) { + showError( + formatMessage({ + id: 'checkout.error.cannot_save_address', + defaultMessage: 'Could not save shipping address.' + }) + ) + } + } + const handleOtpVerification = async (otpCode) => { try { - await loginPasswordless.mutateAsync({ + const token = await loginPasswordless.mutateAsync({ pwdlessLoginToken: otpCode, register_customer: true }) + const customerId = token?.customer_id || token?.customerId + if (customerId && basket) { + await saveAddressesAndPhoneToProfile(customerId) + } + if (onRegistered) { await onRegistered(basket?.basketId) } diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.test.js b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.test.js index d210a8d31c..421a659aaa 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.test.js +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration.test.js @@ -15,6 +15,13 @@ import useAuthContext from '@salesforce/commerce-sdk-react/hooks/useAuthContext' jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket') +jest.mock( + '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context', + () => ({ + useCheckout: () => ({contactPhone: ''}) + }) +) + const {AuthHelpers} = jest.requireActual('@salesforce/commerce-sdk-react') const TEST_MESSAGES = { @@ -29,12 +36,23 @@ const mockAuthHelperFunctions = { [AuthHelpers.LoginPasswordlessUser]: {mutateAsync: jest.fn()} } +const mockCreateCustomerAddress = {mutateAsync: jest.fn().mockResolvedValue({})} +const mockUpdateCustomer = {mutateAsync: jest.fn().mockResolvedValue({})} +const mockCreateCustomerPaymentInstrument = {mutateAsync: jest.fn().mockResolvedValue({})} + jest.mock('@salesforce/commerce-sdk-react', () => { const original = jest.requireActual('@salesforce/commerce-sdk-react') return { ...original, useCustomerType: jest.fn(), - useAuthHelper: jest.fn((helper) => mockAuthHelperFunctions[helper]) + useAuthHelper: jest.fn((helper) => mockAuthHelperFunctions[helper]), + useShopperCustomersMutation: jest.fn((mutationType) => { + if (mutationType === 'createCustomerAddress') return mockCreateCustomerAddress + if (mutationType === 'updateCustomer') return mockUpdateCustomer + if (mutationType === 'createCustomerPaymentInstrument') + return mockCreateCustomerPaymentInstrument + return {mutateAsync: jest.fn()} + }) } }) jest.mock('@salesforce/commerce-sdk-react/hooks/useAuthContext', () => From ee11d3d8dcd6fdf1fff144aa0d1a650e6d57a40f Mon Sep 17 00:00:00 2001 From: Sushma Yadupathi Date: Wed, 4 Feb 2026 14:16:02 -0500 Subject: [PATCH 2/2] code review comments --- .../partials/one-click-payment.jsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx index e908241efc..790cc5eab4 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-payment.jsx @@ -126,20 +126,6 @@ const Payment = ({ } }, [shouldSavePaymentMethod, onSavePreferenceChange]) - // When a newly registered shopper leaves checkout and returns, they have no saved payments yet. - // Auto-check "Save for future use" so the next card they enter gets saved to their profile. - const hasAutoCheckedSaveForNewlyRegisteredRef = useRef(false) - useEffect(() => { - if (hasAutoCheckedSaveForNewlyRegisteredRef.current) return - if (!customer?.isRegistered) return - const hasNoSavedPayments = - !customer?.paymentInstruments || customer.paymentInstruments.length === 0 - if (hasNoSavedPayments) { - hasAutoCheckedSaveForNewlyRegisteredRef.current = true - setShouldSavePaymentMethod(true) - } - }, [customer?.isRegistered, customer?.paymentInstruments]) - // Handles user registration checkbox toggle (OTP handled by UserRegistration) const onUserRegistrationToggle = async (checked) => { setEnableUserRegistration(checked)