diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.jsx b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.jsx index b9daf566c6..92d6113d0d 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.jsx +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.jsx @@ -112,7 +112,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG const [isCheckingEmail, setIsCheckingEmail] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [isBlurChecking, setIsBlurChecking] = useState(false) - const [, setRegisteredUserChoseGuest] = useState(false) + const [registeredUserChoseGuest, setRegisteredUserChoseGuest] = useState(false) const [emailError, setEmailError] = useState('') // Auto-focus the email field when the component mounts @@ -244,8 +244,15 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG lastEmailSentRef.current = normalizedEmail return {isRegistered: true} } catch (error) { - const message = formatMessage(getPasswordlessErrorMessage(error.message)) - setError(message) + const errMsg = error?.message || '' + const isUserNotFound = + /user not found|email not found|unknown user|no account|no user|error getting user info/i.test( + errMsg + ) + if (!isUserNotFound) { + const message = formatMessage(getPasswordlessErrorMessage(errMsg)) + setError(message) + } // Keep continue button visible if email is valid (for unregistered users) if (isValidEmail(email)) { setShowContinueButton(true) @@ -272,37 +279,10 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG } // Handle checkout as guest from OTP modal - const handleCheckoutAsGuest = async () => { - try { - const email = form.getValues('email') - const phone = form.getValues('phone') - // Update basket with guest email - await updateCustomerForBasket.mutateAsync({ - parameters: {basketId: basket.basketId}, - body: {email: email} - }) - - // Save phone number to basket billing address for guest shoppers - if (phone) { - await updateBillingAddressForBasket.mutateAsync({ - parameters: {basketId: basket.basketId}, - body: { - ...basket?.billingAddress, - phone: phone - } - }) - } - - // Set the flag that "Checkout as Guest" was clicked - setRegisteredUserChoseGuest(true) - if (onRegisteredUserChoseGuest) { - onRegisteredUserChoseGuest(true) - } - - // Proceed to next step (shipping address) - goToNextStep() - } catch (error) { - setError(error.message) + const handleCheckoutAsGuest = () => { + setRegisteredUserChoseGuest(true) + if (onRegisteredUserChoseGuest) { + onRegisteredUserChoseGuest(true) } } @@ -472,7 +452,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG return } - if (!result.isRegistered) { + if (!result.isRegistered || registeredUserChoseGuest) { // Guest shoppers must provide phone number before proceeding const phone = (formData.phone || '').trim() if (!phone) { diff --git a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.test.js b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.test.js index 0a2ee05fee..586b0f8ae1 100644 --- a/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.test.js +++ b/packages/template-retail-react-app/app/pages/checkout-one-click/partials/one-click-contact-info.test.js @@ -110,13 +110,17 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-multi-site', () => ({ // Mock OtpAuth to expose a verify trigger jest.mock('@salesforce/retail-react-app/app/components/otp-auth', () => { // eslint-disable-next-line react/prop-types - return function MockOtpAuth({isOpen, handleOtpVerification, onCheckoutAsGuest}) { + return function MockOtpAuth({isOpen, handleOtpVerification, onCheckoutAsGuest, onClose}) { + const handleGuestClick = () => { + onCheckoutAsGuest?.() + onClose?.() + } return isOpen ? (
Confirm it's you

To log in to your account, enter the code sent to your email.

- @@ -523,6 +527,27 @@ describe('ContactInfo Component', () => { }) }) + test('does not show error message when login returns user/email not found (guest flow)', async () => { + mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockRejectedValue( + new Error('Email not found') + ) + + const {user} = renderWithProviders() + + const emailInput = screen.getByLabelText('Email') + await user.type(emailInput, validEmail) + fireEvent.blur(emailInput) + + await waitFor(() => { + expect( + mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync + ).toHaveBeenCalled() + }) + + // Red error banner should NOT be shown when user is not found (guest can continue) + expect(screen.queryByText(/Something went wrong\. Try again!/i)).not.toBeInTheDocument() + }) + test('renders contact info title', () => { renderWithProviders() @@ -606,11 +631,10 @@ describe('ContactInfo Component', () => { expect(screen.getByText(/Resend Code/i)).toBeInTheDocument() }) - test('shows error message when updateCustomerForBasket fails', async () => { - // Mock OTP authorization to succeed so modal opens + test('clicking "Checkout as a guest" does not update basket or advance step', async () => { + // "Checkout as Guest" only closes the modal and sets registeredUserChoseGuest state; + // basket is updated when the user later submits the form with phone and clicks Continue. mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({}) - // Mock update to fail when choosing guest - mockUpdateCustomerForBasket.mutateAsync.mockRejectedValue(new Error('API Error')) const {user} = renderWithProviders() const emailInput = screen.getByLabelText('Email') @@ -625,22 +649,17 @@ describe('ContactInfo Component', () => { await user.click(submitButton) await screen.findByTestId('otp-verify') - // Click "Checkout as a guest" which triggers updateCustomerForBasket and should set error + // Click "Checkout as a guest" — should not call basket mutations or goToNextStep await user.click(screen.getByText(/Checkout as a guest/i)) await waitFor(() => { - expect(mockUpdateCustomerForBasket.mutateAsync).toHaveBeenCalled() - }) - // Error alert should be rendered; component maps errors via getPasswordlessErrorMessage to generic message - await waitFor(() => { - const alerts = screen.queryAllByRole('alert') - const hasError = alerts.some( - (n) => - n.textContent?.includes('Something went wrong') || - n.textContent?.includes('API Error') - ) - expect(hasError).toBe(true) + expect(mockUpdateCustomerForBasket.mutateAsync).not.toHaveBeenCalled() + expect(mockGoToNextStep).not.toHaveBeenCalled() }) + // Modal closes; user stays on Contact Info (Continue button visible again) + expect( + screen.getByRole('button', {name: /continue to shipping address/i}) + ).toBeInTheDocument() }) test('does not proceed to next step when OTP modal is already open on form submission', async () => { @@ -747,37 +766,40 @@ describe('ContactInfo Component', () => { expect(phoneInput.value).toBe('(555) 123-4567') }) - test('saves phone number to billing address when guest checks out via "Checkout as Guest" button', async () => { - // Mock successful OTP authorization to open modal + test('notifies parent when guest chooses "Checkout as Guest" and stays on Contact Info', async () => { + // Open OTP modal (registered email), click "Checkout as a guest" — modal closes, + // parent is notified via onRegisteredUserChoseGuest(true), user stays on Contact Info. mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({}) - mockUpdateCustomerForBasket.mutateAsync.mockResolvedValue({}) - mockUpdateBillingAddressForBasket.mutateAsync.mockResolvedValue({}) - const {user} = renderWithProviders() + const onRegisteredUserChoseGuestSpy = jest.fn() + const {user} = renderWithProviders( + + ) const emailInput = screen.getByLabelText('Email') - const phoneInput = screen.getByLabelText('Phone') - - // Enter phone first - use fireEvent to ensure value is set - fireEvent.change(phoneInput, {target: {value: '(727) 555-1234'}}) - // Enter email and wait for OTP modal to open + // Enter email and open OTP modal (blur triggers registered-user check) await user.type(emailInput, validEmail) fireEvent.change(emailInput, {target: {value: validEmail}}) fireEvent.blur(emailInput) - // Wait for OTP modal to open await screen.findByTestId('otp-verify') - // Click "Checkout as a guest" button + // Click "Checkout as a guest" — modal closes; parent is notified; no basket update await user.click(screen.getByText(/Checkout as a guest/i)) + expect(onRegisteredUserChoseGuestSpy).toHaveBeenCalledWith(true) + expect(mockUpdateCustomerForBasket.mutateAsync).not.toHaveBeenCalled() + expect(mockGoToNextStep).not.toHaveBeenCalled() + + // Modal closes; user stays on Contact Info (Continue button visible for entering phone) await waitFor(() => { - expect(mockUpdateBillingAddressForBasket.mutateAsync).toHaveBeenCalled() - const callArgs = mockUpdateBillingAddressForBasket.mutateAsync.mock.calls[0]?.[0] - expect(callArgs?.parameters).toMatchObject({basketId: 'test-basket-id'}) - expect(callArgs?.body?.phone).toMatch(/727/) + expect(screen.queryByText("Confirm it's you")).not.toBeInTheDocument() }) + expect( + screen.getByRole('button', {name: /continue to shipping address/i}) + ).toBeInTheDocument() + expect(screen.getByLabelText('Phone')).toBeInTheDocument() }) test('uses phone from billing address when persisting to customer profile after OTP verification', async () => {