Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

so when this checkbox is auto checked, and say the user unchecks it and goes back to cart, would this get enabled again when the user comes back to checkout?

useEffect(() => {
if (hasAutoCheckedSaveForNewlyRegisteredRef.current) return
if (!customer?.isRegistered) return
const hasNoSavedPayments =
Copy link
Contributor

Choose a reason for hiding this comment

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

This would apply to all registered users with no saved payments, so if if anyone decided to not save any payment method their payment will be saved regardless? This could be undesirable UX. IMO this should apply only to those shoppers who really registered now

!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)
Expand Down
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, {useRef, useState, useEffect} from 'react'
import {FormattedMessage} from 'react-intl'
import {FormattedMessage, useIntl} from 'react-intl'
import PropTypes from 'prop-types'
import {
Box,
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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', () =>
Expand Down
Loading