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 a9196bd5f7..8dc896e1cf 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
@@ -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,16 +91,6 @@ 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,
@@ -110,14 +98,6 @@ const ContactInfo = ({isSocialEnabled = false, idps = [], onRegisteredUserChoseG
onClose: onOtpModalClose
} = useDisclosure()
- // Helper function to validate email format
- 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)
- }
-
// 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 (
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 e34623dc36..f9459ffa34 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
@@ -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 () => {
- const {user} = renderWithProviders()
-
- // Test various valid email formats
- const validEmails = [
- 'simple@example.com',
- 'user.name@domain.com',
- 'user+tag@example.org',
- 'user-name@subdomain.example.co.uk',
- 'user123@domain123.net',
- 'user.name+tag@example-domain.com',
- 'user@example-domain.com',
- 'user@subdomain1.subdomain2.example.com',
- 'user.name@example.co.uk',
- 'user@example-domain123.com',
- 'josé@mañana.com',
- 'firstname.lastname@example.co.uk',
- 'email@subdomain.example.com',
- 'user+mailbox@example.com',
- 'user-name@example.org',
- '12345@example.com',
- 'email@mañana.com',
- 'josé@example.españa',
- 'email@bücher.de',
- '用户@例子.中国',
- '!#$%&*+/=?^_{|}~-@example.com'
- ]
-
- for (const email of validEmails) {
- const {user: testUser} = renderWithProviders()
- 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
- 'user@.domain.com', // Domain starting with dot
- 'user@domain.com.', // Domain ending with dot
- 'user@-domain.com', // Domain starting with hyphen
- 'user@domain-.com' // Domain ending with hyphen
- ]
-
- for (const email of invalidEmails) {
- const {user: testUser} = renderWithProviders()
- 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()
+
+ 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()
+ })
})
diff --git a/packages/template-retail-react-app/app/utils/email-utils.js b/packages/template-retail-react-app/app/utils/email-utils.js
new file mode 100644
index 0000000000..04add05042
--- /dev/null
+++ b/packages/template-retail-react-app/app/utils/email-utils.js
@@ -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)
+}
diff --git a/packages/template-retail-react-app/app/utils/email-utils.test.js b/packages/template-retail-react-app/app/utils/email-utils.test.js
new file mode 100644
index 0000000000..49351e378f
--- /dev/null
+++ b/packages/template-retail-react-app/app/utils/email-utils.test.js
@@ -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('test@example.com')).toBe(true)
+ })
+
+ test('should return true for email with subdomain', () => {
+ expect(isValidEmail('user@mail.example.com')).toBe(true)
+ })
+
+ test('should return true for email with numbers', () => {
+ expect(isValidEmail('user123@example123.com')).toBe(true)
+ })
+
+ test('should return true for email with special characters', () => {
+ expect(isValidEmail('user.name+tag@example-domain.co.uk')).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!#$%&'*+/=?^`{|}~-@example.com")).toBe(true)
+ })
+
+ test('should return true for email with long domain', () => {
+ expect(isValidEmail('test@very-long-domain-name-that-is-still-valid.com')).toBe(true)
+ })
+
+ test('should return true for email with single character local part', () => {
+ expect(isValidEmail('a@example.com')).toBe(true)
+ })
+
+ test('should return true for email with single character domain', () => {
+ expect(isValidEmail('test@a.com')).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('Test.User@Example.COM')).toBe(true)
+ })
+
+ test('should return true for email with numbers in domain', () => {
+ expect(isValidEmail('test@example123.com')).toBe(true)
+ })
+
+ test('should return true for email with hyphen in domain', () => {
+ expect(isValidEmail('test@example-domain.com')).toBe(true)
+ })
+
+ test('should return true for email with consecutive dots (regex allows this)', () => {
+ expect(isValidEmail('test..user@example.com')).toBe(true)
+ })
+
+ test('should return true for email starting with dot (regex allows this)', () => {
+ expect(isValidEmail('.test@example.com')).toBe(true)
+ })
+
+ test('should return true for email ending with dot (regex allows this)', () => {
+ expect(isValidEmail('test.@example.com')).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('test@example..com')).toBe(false)
+ })
+
+ test('should return false for domain starting with dot', () => {
+ expect(isValidEmail('test@.example.com')).toBe(false)
+ })
+
+ test('should return false for domain ending with dot', () => {
+ expect(isValidEmail('test@example.com.')).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('test@-example.com')).toBe(false)
+ })
+
+ test('should return false for email with hyphen at end of domain part', () => {
+ expect(isValidEmail('test@example-.com')).toBe(false)
+ })
+ })
+})