-
Notifications
You must be signed in to change notification settings - Fork 201
Open OTP modal when user clicks proceed to shipping address #3243
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 all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
| * 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 React, {useRef, useState, useEffect} from 'react' | ||
| import React, {useRef, useState} from 'react' | ||
| import PropTypes from 'prop-types' | ||
| import { | ||
| Alert, | ||
|
|
@@ -43,13 +43,13 @@ import { | |
| AuthHelpers, | ||
| useAuthHelper, | ||
| useShopperBasketsMutation, | ||
| useCustomerType, | ||
| useConfig | ||
| useCustomerType | ||
| } from '@salesforce/commerce-sdk-react' | ||
| import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config' | ||
| import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils' | ||
| import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin' | ||
| import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants' | ||
| import {isValidEmail} from '@salesforce/retail-react-app/app/utils/email-utils' | ||
|
|
||
| const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseGuest}) => { | ||
| const {formatMessage} = useIntl() | ||
|
|
@@ -59,9 +59,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG | |
| const currentBasketQuery = useCurrentBasket() | ||
| const {data: basket} = currentBasketQuery | ||
| const {isRegistered} = useCustomerType() | ||
| const config = useConfig() | ||
|
|
||
| const login = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C) | ||
| const logout = useAuthHelper(AuthHelpers.Logout) | ||
| const updateCustomerForBasket = useShopperBasketsMutation('updateCustomerForBasket') | ||
| const mergeBasket = useShopperBasketsMutation('mergeBasket') | ||
|
|
@@ -93,31 +91,13 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG | |
| ? passwordlessConfigCallback | ||
| : `${appOrigin}${passwordlessConfigCallback}` | ||
|
|
||
| // Reset guest checkout flag when user registration status changes | ||
| useEffect(() => { | ||
| if (isRegistered) { | ||
| setRegisteredUserChoseGuest(false) | ||
| if (onRegisteredUserChoseGuest) { | ||
| onRegisteredUserChoseGuest(false) | ||
| } | ||
| } | ||
| }, [isRegistered, onRegisteredUserChoseGuest]) | ||
|
|
||
| // Modal controls for OtpAuth | ||
| const { | ||
| isOpen: isOtpModalOpen, | ||
| onOpen: onOtpModalOpen, | ||
| onClose: onOtpModalClose | ||
| } = useDisclosure() | ||
|
|
||
| // Helper function to validate email format | ||
| const isValidEmail = (email) => { | ||
|
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. I can see this being needed in another component so I just moved it to a util that has it's own dedicated testing |
||
| const emailRegex = | ||
| /^[\p{L}\p{N}._!#$%&'*+/=?^`{|}~-]+@(?:[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?\.)+[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?$/u | ||
|
|
||
| return emailRegex.test(email) | ||
| } | ||
|
|
||
| // Handle email field blur/focus events | ||
| const handleEmailBlur = async (e) => { | ||
| // Call original React Hook Form blur handler if it exists | ||
|
|
@@ -280,7 +260,7 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG | |
| } | ||
|
|
||
| setShowContinueButton(false) | ||
| goToNextStep() | ||
| handleSendEmailOtp(data.email) | ||
| } | ||
|
|
||
| return ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,86 +171,9 @@ describe('ContactInfo Component', () => { | |
| await user.tab() | ||
|
|
||
| expect(screen.getByText('Please enter a valid email address.')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('validates different types of valid emails correctly', async () => { | ||
|
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. The tests |
||
| const {user} = renderWithProviders(<ContactInfo />) | ||
|
|
||
| // Test various valid email formats | ||
| const validEmails = [ | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| 'josé@mañana.com', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| '[email protected]', | ||
| 'email@mañana.com', | ||
| 'josé@example.españa', | ||
| 'email@bücher.de', | ||
| '用户@例子.中国', | ||
| '!#$%&*+/=?^_{|}[email protected]' | ||
| ] | ||
|
|
||
| for (const email of validEmails) { | ||
| const {user: testUser} = renderWithProviders(<ContactInfo />) | ||
| const emailInput = screen.getByLabelText('Email') | ||
|
|
||
| await testUser.type(emailInput, email) | ||
|
|
||
| // Trigger blur event to validate | ||
| await testUser.tab() | ||
|
|
||
| // Should not show email format error for valid emails | ||
| expect( | ||
| screen.queryByText('Please enter a valid email address.') | ||
| ).not.toBeInTheDocument() | ||
|
|
||
| // Should not show required email error | ||
| expect(screen.queryByText('Please enter your email address.')).not.toBeInTheDocument() | ||
|
|
||
| // Clean up | ||
| cleanup() | ||
| } | ||
| }) | ||
|
|
||
| test('validates different types of invalid emails correctly', async () => { | ||
| // Test various invalid email formats that are definitely rejected by the current regex | ||
| const invalidEmails = [ | ||
| 'plainaddress', // Missing @ symbol | ||
| '@missinglocal.com', // Missing local part | ||
| 'missingdomain@', // Missing domain | ||
| 'user@', // Missing domain completely | ||
| '[email protected]', // Domain starting with dot | ||
| '[email protected].', // Domain ending with dot | ||
| '[email protected]', // Domain starting with hyphen | ||
| '[email protected]' // Domain ending with hyphen | ||
| ] | ||
|
|
||
| for (const email of invalidEmails) { | ||
| const {user: testUser} = renderWithProviders(<ContactInfo />) | ||
| const emailInput = screen.getByLabelText('Email') | ||
|
|
||
| await testUser.type(emailInput, email) | ||
|
|
||
| // Trigger blur event to validate | ||
| await testUser.tab() | ||
|
|
||
| // Should show email format error for invalid emails | ||
| expect(screen.getByText('Please enter a valid email address.')).toBeInTheDocument() | ||
|
|
||
| // Clean up | ||
| cleanup() | ||
| } | ||
| // Should not show required email error | ||
| expect(screen.queryByText('Please enter your email address.')).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('allows guest checkout with valid email', async () => { | ||
|
|
@@ -285,9 +208,6 @@ describe('ContactInfo Component', () => { | |
| }) | ||
| }) | ||
|
|
||
| // Note: The OTP modal opens on email blur after successful authorization | ||
| // Submitting the form directly progresses the flow instead of opening the modal. | ||
|
|
||
| test('renders continue button for guest checkout', async () => { | ||
| // Mock the passwordless login to fail (email not found) | ||
| mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockRejectedValue( | ||
|
|
@@ -376,4 +296,34 @@ describe('ContactInfo Component', () => { | |
| expect(screen.getByText('Checkout as a guest')).toBeInTheDocument() | ||
| expect(screen.getByText('Resend code')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('opens OTP modal when form is submitted by clicking submit button', async () => { | ||
| // Mock successful OTP authorization | ||
| mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockResolvedValue({ | ||
| success: true | ||
| }) | ||
|
|
||
| const {user} = renderWithProviders(<ContactInfo />) | ||
|
|
||
| const emailInput = screen.getByLabelText('Email') | ||
| await user.type(emailInput, validEmail) | ||
|
|
||
| // Find and click the submit button | ||
| const submitButton = screen.getByRole('button', { | ||
| name: /continue to shipping address/i | ||
| }) | ||
| await user.click(submitButton) | ||
|
|
||
| // Wait for OTP modal to appear after form submission | ||
| await waitFor(() => { | ||
| expect(screen.getByText("Confirm it's you")).toBeInTheDocument() | ||
| }) | ||
|
|
||
| // Verify modal content is present | ||
| expect( | ||
| screen.getByText('To use your account information enter the code sent to your email.') | ||
| ).toBeInTheDocument() | ||
| expect(screen.getByText('Checkout as a guest')).toBeInTheDocument() | ||
| expect(screen.getByText('Resend code')).toBeInTheDocument() | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /* | ||
| * 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 | ||
| */ | ||
| export const isValidEmail = (email) => { | ||
| const emailRegex = | ||
| /^[\p{L}\p{N}._!#$%&'*+/=?^`{|}~-]+@(?:[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?\.)+[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?$/u | ||
|
|
||
| return emailRegex.test(email) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| /* | ||
| * 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 {isValidEmail} from '@salesforce/retail-react-app/app/utils/email-utils' | ||
|
|
||
| describe('isValidEmail', () => { | ||
| describe('valid email addresses', () => { | ||
| test('should return true for basic email format', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with subdomain', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with numbers', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with special characters', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with international characters', () => { | ||
| expect(isValidEmail('tëst@éxämplé.com')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with various special characters', () => { | ||
| expect(isValidEmail("user!#$%&'*+/=?^`{|}[email protected]")).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with long domain', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with single character local part', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with single character domain', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for very long email addresses', () => { | ||
| const longEmail = 'a'.repeat(50) + '@' + 'b'.repeat(50) + '.com' | ||
| expect(isValidEmail(longEmail)).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with maximum valid length', () => { | ||
| const maxLengthEmail = 'a'.repeat(64) + '@' + 'b'.repeat(63) + '.com' | ||
| expect(isValidEmail(maxLengthEmail)).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with mixed case', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with numbers in domain', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with hyphen in domain', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email with consecutive dots (regex allows this)', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email starting with dot (regex allows this)', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
|
|
||
| test('should return true for email ending with dot (regex allows this)', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| describe('invalid email addresses', () => { | ||
| test('should return false for empty string', () => { | ||
| expect(isValidEmail('')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for null', () => { | ||
| expect(isValidEmail(null)).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for undefined', () => { | ||
| expect(isValidEmail(undefined)).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email without @ symbol', () => { | ||
| expect(isValidEmail('testexample.com')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email with multiple @ symbols', () => { | ||
| expect(isValidEmail('test@@example.com')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email without domain', () => { | ||
| expect(isValidEmail('test@')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email without local part', () => { | ||
| expect(isValidEmail('@example.com')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email with spaces', () => { | ||
| expect(isValidEmail('test @example.com')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email with invalid characters', () => { | ||
| expect(isValidEmail('test()@example.com')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for domain without TLD', () => { | ||
| expect(isValidEmail('test@example')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for domain with invalid TLD', () => { | ||
| expect(isValidEmail('test@example.')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for domain with consecutive dots', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for domain starting with dot', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for domain ending with dot', () => { | ||
| expect(isValidEmail('[email protected].')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for non-string input', () => { | ||
| expect(isValidEmail(123)).toBe(false) | ||
| expect(isValidEmail({})).toBe(false) | ||
| expect(isValidEmail([])).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email with hyphen at start of domain part', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(false) | ||
| }) | ||
|
|
||
| test('should return false for email with hyphen at end of domain part', () => { | ||
| expect(isValidEmail('[email protected]')).toBe(false) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe this was necessary and the usage of useEffect causes unnecessary re-renders