Skip to content

Commit 9395d61

Browse files
syadupathi-sfdannyphan2000
authored andcommitted
@W-21111863 @W-21109829 @W-21005976 @W-21109850 1CC Bug Fixes (#3638)
* @W-21109850: Continue as Guest skips entering phone number (#3626) * One click checkout * changes to fix install, tests and lint - needs to be reviewed * revert the test change in pwa-kit-runtime * @W-20892497 Show Phone number in Contact Info summary (#3576) * W-20892497 Show Phone number in Contact Info summary * fix lint * @W-20892592 Remove gift messaging for multi shipment (#3579) * W-20892592 Remove gift messaging for multi shipment * translations * @W-20892530 @W-20892577 Billing Address Validation and Using contact phone for user registration (#3583) * W-20892530 Billing Address Validation * W-20892577 save contact info phone * Fix SDK tests (#3593) * fix sdk tests and app bundle size * fix lint * @ W-20540715 Address 1CC feature branch review comments (#3619) * address first set of comments * address rest of code review comments * reverting default.js changes * fix package versions * shipping options fix * attempt to fix flaky tests * passwordless mode updates * @W-21109850: Continue as Guest skips entering phone number Signed-off-by: d.phan <d.phan@salesforce.com> * fix import Signed-off-by: d.phan <d.phan@salesforce.com> * translations * fix user not found error --------- Signed-off-by: d.phan <d.phan@salesforce.com> Co-authored-by: Sushma Yadupathi <syadupathi@salesforce.com> Co-authored-by: syadupathi-sf <66088780+syadupathi-sf@users.noreply.github.com> * W-21111863 Hide user registration for returning shoppers who chose to checkout as guest (#3634) * @W-21005976 Save newly registered user's info when they leave checkout… (#3632) * W-21005976 Save newly registered user's info when they leave checkout and return * code review comments * @W-21109829 Editing shipping options in multi shipment scenarios (#3637) * W-21109829 Editing shipping options in multi shipment scenarios * minor text changes * remove unnecessary test --------- Signed-off-by: d.phan <d.phan@salesforce.com> Signed-off-by: syadupathi-sf <66088780+syadupathi-sf@users.noreply.github.com> Co-authored-by: Danny Phan <125327707+dannyphan2000@users.noreply.github.com>
1 parent a9a4525 commit 9395d61

File tree

9 files changed

+316
-190
lines changed

9 files changed

+316
-190
lines changed

packages/template-retail-react-app/app/pages/checkout-one-click/index.jsx

Lines changed: 8 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
3131
import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
3232
import {
3333
useCheckout,
34-
CheckoutProvider
34+
CheckoutProvider,
35+
getCheckoutGuestChoiceFromStorage,
36+
setCheckoutGuestChoiceInStorage
3537
} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
3638
import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info'
3739
import PickupAddress from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-pickup-address'
@@ -55,16 +57,17 @@ import {
5557
getPaymentInstrumentCardType,
5658
getMaskCreditCardNumber
5759
} from '@salesforce/retail-react-app/app/utils/cc-utils'
58-
import {nanoid} from 'nanoid'
5960

6061
const CheckoutOneClick = () => {
6162
const {formatMessage} = useIntl()
6263
const navigate = useNavigation()
63-
const {step, STEPS, contactPhone} = useCheckout()
64+
const {step, STEPS} = useCheckout()
6465
const showToast = useToast()
6566
const [isLoading, setIsLoading] = useState(false)
6667
const [enableUserRegistration, setEnableUserRegistration] = useState(false)
67-
const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false)
68+
const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(
69+
getCheckoutGuestChoiceFromStorage
70+
)
6871
const [shouldSavePaymentMethod, setShouldSavePaymentMethod] = useState(false)
6972
const [isOtpLoading, setIsOtpLoading] = useState(false)
7073
const [isPlacingOrder, setIsPlacingOrder] = useState(false)
@@ -119,8 +122,6 @@ const CheckoutOneClick = () => {
119122
ShopperBasketsMutations.UpdateBillingAddressForBasket
120123
)
121124
const {mutateAsync: createOrder} = useShopperOrdersMutation(ShopperOrdersMutations.CreateOrder)
122-
const createCustomerAddress = useShopperCustomersMutation('createCustomerAddress')
123-
const updateCustomer = useShopperCustomersMutation('updateCustomer')
124125

125126
const handleSavePreferenceChange = (shouldSave) => {
126127
setShouldSavePaymentMethod(shouldSave)
@@ -382,87 +383,9 @@ const CheckoutOneClick = () => {
382383
fullCardDetails
383384
)
384385
}
385-
386-
// For newly registered guests only, persist shipping address when billing same as shipping
387-
// Skip saving pickup/store addresses - only save delivery addresses
388-
// For multi-shipment orders, save all delivery addresses with the first one as default
389-
if (
390-
enableUserRegistration &&
391-
currentCustomer?.isRegistered &&
392-
!registeredUserChoseGuest
393-
) {
394-
try {
395-
const customerId = order.customerInfo?.customerId
396-
if (!customerId) return
397-
398-
// Get all delivery shipments (not pickup) from the order
399-
// This handles both single delivery and multi-shipment orders
400-
// For BOPIS orders, pickup shipments are filtered out
401-
const deliveryShipments =
402-
order?.shipments?.filter(
403-
(shipment) =>
404-
!isPickupShipment(shipment) && shipment.shippingAddress
405-
) || []
406-
407-
if (deliveryShipments.length > 0) {
408-
// Save all delivery addresses, with the first one as preferred
409-
for (let i = 0; i < deliveryShipments.length; i++) {
410-
const shipment = deliveryShipments[i]
411-
const shipping = shipment.shippingAddress
412-
if (!shipping) continue
413-
414-
// Whitelist fields and strip non-customer fields (e.g., id, _type)
415-
const {
416-
address1,
417-
address2,
418-
city,
419-
countryCode,
420-
firstName,
421-
lastName,
422-
phone,
423-
postalCode,
424-
stateCode
425-
} = shipping || {}
426-
427-
await createCustomerAddress.mutateAsync({
428-
parameters: {customerId},
429-
body: {
430-
addressId: nanoid(),
431-
preferred: i === 0, // First address is preferred
432-
address1,
433-
address2,
434-
city,
435-
countryCode,
436-
firstName,
437-
lastName,
438-
phone,
439-
postalCode,
440-
stateCode
441-
}
442-
})
443-
}
444-
}
445-
446-
// Persist phone number as phoneHome for newly registered guest shoppers
447-
const phoneHome = basket?.billingAddress?.phone || contactPhone
448-
if (phoneHome) {
449-
await updateCustomer.mutateAsync({
450-
parameters: {customerId},
451-
body: {phoneHome}
452-
})
453-
}
454-
} catch (_e) {
455-
// Only surface error if shopper opted to register/save details; otherwise fail silently
456-
showError(
457-
formatMessage({
458-
id: 'checkout.error.cannot_save_address',
459-
defaultMessage: 'Could not save shipping address.'
460-
})
461-
)
462-
}
463-
}
464386
}
465387

388+
setCheckoutGuestChoiceInStorage(false)
466389
navigate(`/checkout/confirmation/${order.orderNo}`)
467390
} catch (error) {
468391
const message = formatMessage({

packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.jsx

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import {
2626
} from '@salesforce/retail-react-app/app/components/shared/ui'
2727
import {useForm} from 'react-hook-form'
2828
import {FormattedMessage, useIntl} from 'react-intl'
29-
import {useCheckout} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
29+
import {
30+
useCheckout,
31+
setCheckoutGuestChoiceInStorage
32+
} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
3033
import useLoginFields from '@salesforce/retail-react-app/app/components/forms/useLoginFields'
3134
import {
3235
ToggleCard,
@@ -110,7 +113,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
110113
const [isCheckingEmail, setIsCheckingEmail] = useState(false)
111114
const [isSubmitting, setIsSubmitting] = useState(false)
112115
const [isBlurChecking, setIsBlurChecking] = useState(false)
113-
const [, setRegisteredUserChoseGuest] = useState(false)
116+
const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false)
114117
const [emailError, setEmailError] = useState('')
115118

116119
// Auto-focus the email field when the component mounts
@@ -270,37 +273,11 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
270273
}
271274

272275
// Handle checkout as guest from OTP modal
273-
const handleCheckoutAsGuest = async () => {
274-
try {
275-
const email = form.getValues('email')
276-
const phone = form.getValues('phone')
277-
// Update basket with guest email
278-
await updateCustomerForBasket.mutateAsync({
279-
parameters: {basketId: basket.basketId},
280-
body: {email: email}
281-
})
282-
283-
// Save phone number to basket billing address for guest shoppers
284-
if (phone) {
285-
await updateBillingAddressForBasket.mutateAsync({
286-
parameters: {basketId: basket.basketId},
287-
body: {
288-
...basket?.billingAddress,
289-
phone: phone
290-
}
291-
})
292-
}
293-
294-
// Set the flag that "Checkout as Guest" was clicked
295-
setRegisteredUserChoseGuest(true)
296-
if (onRegisteredUserChoseGuest) {
297-
onRegisteredUserChoseGuest(true)
298-
}
299-
300-
// Proceed to next step (shipping address)
301-
goToNextStep()
302-
} catch (error) {
303-
setError(error.message)
276+
const handleCheckoutAsGuest = () => {
277+
setRegisteredUserChoseGuest(true)
278+
setCheckoutGuestChoiceInStorage(true)
279+
if (onRegisteredUserChoseGuest) {
280+
onRegisteredUserChoseGuest(true)
304281
}
305282
}
306283

@@ -359,6 +336,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
359336

360337
// Reset guest checkout flag since user is now logged in
361338
setRegisteredUserChoseGuest(false)
339+
setCheckoutGuestChoiceInStorage(false)
362340
if (onRegisteredUserChoseGuest) {
363341
onRegisteredUserChoseGuest(false)
364342
}
@@ -470,7 +448,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
470448
return
471449
}
472450

473-
if (!result.isRegistered) {
451+
if (!result.isRegistered || registeredUserChoseGuest) {
474452
// Guest shoppers must provide phone number before proceeding
475453
const phone = (formData.phone || '').trim()
476454
if (!phone) {

packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.test.js

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import React from 'react'
88
import {screen, waitFor, fireEvent, act} from '@testing-library/react'
99
import ContactInfo from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info'
10+
import {setCheckoutGuestChoiceInStorage} from '@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context'
1011
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
1112
import {rest} from 'msw'
1213
import {AuthHelpers, useCustomerType} from '@salesforce/commerce-sdk-react'
@@ -79,6 +80,7 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-customer', () => (
7980
const mockSetContactPhone = jest.fn()
8081
const mockGoToNextStep = jest.fn()
8182
jest.mock('@salesforce/retail-react-app/app/pages/checkout-one-click/util/checkout-context', () => {
83+
const setCheckoutGuestChoiceInStorage = jest.fn()
8284
return {
8385
useCheckout: jest.fn().mockReturnValue({
8486
customer: null,
@@ -91,7 +93,8 @@ jest.mock('@salesforce/retail-react-app/app/pages/checkout-one-click/util/checko
9193
goToStep: null,
9294
goToNextStep: mockGoToNextStep,
9395
setContactPhone: mockSetContactPhone
94-
})
96+
}),
97+
setCheckoutGuestChoiceInStorage
9598
}
9699
})
97100

@@ -110,13 +113,17 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({
110113
// Mock OtpAuth to expose a verify trigger
111114
jest.mock('@salesforce/retail-react-app/app/components/otp-auth', () => {
112115
// eslint-disable-next-line react/prop-types
113-
return function MockOtpAuth({isOpen, handleOtpVerification, onCheckoutAsGuest}) {
116+
return function MockOtpAuth({isOpen, handleOtpVerification, onCheckoutAsGuest, onClose}) {
117+
const handleGuestClick = () => {
118+
onCheckoutAsGuest?.()
119+
onClose?.()
120+
}
114121
return isOpen ? (
115122
<div>
116123
<div>Confirm it&apos;s you</div>
117124
<p>To log in to your account, enter the code sent to your email.</p>
118125
<div>
119-
<button type="button" onClick={onCheckoutAsGuest}>
126+
<button type="button" onClick={handleGuestClick}>
120127
Checkout as a guest
121128
</button>
122129
<button type="button">Resend Code</button>
@@ -289,7 +296,8 @@ describe('ContactInfo Component', () => {
289296
goToStep: jest.fn(),
290297
goToNextStep: jest.fn(),
291298
setContactPhone: jest.fn()
292-
})
299+
}),
300+
setCheckoutGuestChoiceInStorage: jest.fn()
293301
}
294302
}
295303
)
@@ -606,11 +614,10 @@ describe('ContactInfo Component', () => {
606614
expect(screen.getByText(/Resend Code/i)).toBeInTheDocument()
607615
})
608616

609-
test('shows error message when updateCustomerForBasket fails', async () => {
610-
// Mock OTP authorization to succeed so modal opens
617+
test('clicking "Checkout as a guest" does not update basket or advance step', async () => {
618+
// "Checkout as Guest" only closes the modal and sets registeredUserChoseGuest state;
619+
// basket is updated when the user later submits the form with phone and clicks Continue.
611620
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({})
612-
// Mock update to fail when choosing guest
613-
mockUpdateCustomerForBasket.mutateAsync.mockRejectedValue(new Error('API Error'))
614621

615622
const {user} = renderWithProviders(<ContactInfo />)
616623
const emailInput = screen.getByLabelText('Email')
@@ -625,22 +632,17 @@ describe('ContactInfo Component', () => {
625632
await user.click(submitButton)
626633
await screen.findByTestId('otp-verify')
627634

628-
// Click "Checkout as a guest" which triggers updateCustomerForBasket and should set error
635+
// Click "Checkout as a guest" — should not call basket mutations or goToNextStep
629636
await user.click(screen.getByText(/Checkout as a guest/i))
630637

631638
await waitFor(() => {
632-
expect(mockUpdateCustomerForBasket.mutateAsync).toHaveBeenCalled()
633-
})
634-
// Error alert should be rendered; component maps errors via getPasswordlessErrorMessage to generic message
635-
await waitFor(() => {
636-
const alerts = screen.queryAllByRole('alert')
637-
const hasError = alerts.some(
638-
(n) =>
639-
n.textContent?.includes('Something went wrong') ||
640-
n.textContent?.includes('API Error')
641-
)
642-
expect(hasError).toBe(true)
639+
expect(mockUpdateCustomerForBasket.mutateAsync).not.toHaveBeenCalled()
640+
expect(mockGoToNextStep).not.toHaveBeenCalled()
643641
})
642+
// Modal closes; user stays on Contact Info (Continue button visible again)
643+
expect(
644+
screen.getByRole('button', {name: /continue to shipping address/i})
645+
).toBeInTheDocument()
644646
})
645647

646648
test('does not proceed to next step when OTP modal is already open on form submission', async () => {
@@ -747,37 +749,41 @@ describe('ContactInfo Component', () => {
747749
expect(phoneInput.value).toBe('(555) 123-4567')
748750
})
749751

750-
test('saves phone number to billing address when guest checks out via "Checkout as Guest" button', async () => {
751-
// Mock successful OTP authorization to open modal
752+
test('notifies parent when guest chooses "Checkout as Guest" and stays on Contact Info', async () => {
753+
// Open OTP modal (registered email), click "Checkout as a guest" — modal closes,
754+
// parent is notified via onRegisteredUserChoseGuest(true), user stays on Contact Info.
752755
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({})
753-
mockUpdateCustomerForBasket.mutateAsync.mockResolvedValue({})
754-
mockUpdateBillingAddressForBasket.mutateAsync.mockResolvedValue({})
755756

756-
const {user} = renderWithProviders(<ContactInfo />)
757+
const onRegisteredUserChoseGuestSpy = jest.fn()
758+
const {user} = renderWithProviders(
759+
<ContactInfo onRegisteredUserChoseGuest={onRegisteredUserChoseGuestSpy} />
760+
)
757761

758762
const emailInput = screen.getByLabelText('Email')
759-
const phoneInput = screen.getByLabelText('Phone')
760-
761-
// Enter phone first - use fireEvent to ensure value is set
762-
fireEvent.change(phoneInput, {target: {value: '(727) 555-1234'}})
763763

764-
// Enter email and wait for OTP modal to open
764+
// Enter email and open OTP modal (blur triggers registered-user check)
765765
await user.type(emailInput, validEmail)
766766
fireEvent.change(emailInput, {target: {value: validEmail}})
767767
fireEvent.blur(emailInput)
768768

769-
// Wait for OTP modal to open
770769
await screen.findByTestId('otp-verify')
771770

772-
// Click "Checkout as a guest" button
771+
// Click "Checkout as a guest" — modal closes; parent is notified; no basket update
773772
await user.click(screen.getByText(/Checkout as a guest/i))
774773

774+
expect(onRegisteredUserChoseGuestSpy).toHaveBeenCalledWith(true)
775+
expect(setCheckoutGuestChoiceInStorage).toHaveBeenCalledWith(true)
776+
expect(mockUpdateCustomerForBasket.mutateAsync).not.toHaveBeenCalled()
777+
expect(mockGoToNextStep).not.toHaveBeenCalled()
778+
779+
// Modal closes; user stays on Contact Info (Continue button visible for entering phone)
775780
await waitFor(() => {
776-
expect(mockUpdateBillingAddressForBasket.mutateAsync).toHaveBeenCalled()
777-
const callArgs = mockUpdateBillingAddressForBasket.mutateAsync.mock.calls[0]?.[0]
778-
expect(callArgs?.parameters).toMatchObject({basketId: 'test-basket-id'})
779-
expect(callArgs?.body?.phone).toMatch(/727/)
781+
expect(screen.queryByText("Confirm it's you")).not.toBeInTheDocument()
780782
})
783+
expect(
784+
screen.getByRole('button', {name: /continue to shipping address/i})
785+
).toBeInTheDocument()
786+
expect(screen.getByLabelText('Phone')).toBeInTheDocument()
781787
})
782788

783789
test('uses phone from billing address when persisting to customer profile after OTP verification', async () => {
@@ -836,5 +842,8 @@ describe('ContactInfo Component', () => {
836842
body: {phoneHome: billingPhone}
837843
})
838844
})
845+
846+
// Guest choice storage should be cleared when user signs in via OTP
847+
expect(setCheckoutGuestChoiceInStorage).toHaveBeenCalledWith(false)
839848
})
840849
})

0 commit comments

Comments
 (0)