Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -42,11 +42,20 @@ export default function UserRegistration({
const {isOpen: isOtpOpen, onOpen: onOtpOpen, onClose: onOtpClose} = useDisclosure()
const otpSentRef = useRef(false)

const handleOtpClose = () => {
otpSentRef.current = false
onOtpClose()
}

const handleUserRegistrationChange = async (e) => {
const checked = e.target.checked
setEnableUserRegistration(checked)
// Treat opting into registration as opting to save for future
if (onSavePreferenceChange) onSavePreferenceChange(checked)
// If user unchecks, allow OTP to be re-triggered upon re-check
if (!checked) {
otpSentRef.current = false
}
// Kick off OTP for guests when they opt in
if (checked && isGuest && basket?.customerInfo?.email && !otpSentRef.current) {
try {
Expand Down Expand Up @@ -75,7 +84,7 @@ export default function UserRegistration({
if (onRegistered) {
await onRegistered(basket?.basketId)
}
onOtpClose()
handleOtpClose()
} catch (_e) {
// Let OtpAuth surface errors via its own UI/toast
}
Expand Down Expand Up @@ -133,7 +142,7 @@ export default function UserRegistration({
{/* OTP modal lives with registration now */}
<OtpAuth
isOpen={isOtpOpen}
onClose={onOtpClose}
onClose={handleOtpClose}
isGuestRegistration
form={{
getValues: (name) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ import {render, screen, waitFor} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import UserRegistration from '@salesforce/retail-react-app/app/pages/checkout-one-click/partials/one-click-user-registration'
import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket'
import {useCustomerType, useAuthHelper} from '@salesforce/commerce-sdk-react'
import {useCustomerType} from '@salesforce/commerce-sdk-react'
import useAuthContext from '@salesforce/commerce-sdk-react/hooks/useAuthContext'

jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket')

const {AuthHelpers} = jest.requireActual('@salesforce/commerce-sdk-react')

const TEST_MESSAGES = {
'checkout.title.user_registration': 'Save Checkout Info for Future Use',
'checkout.label.user_registration': 'Create an account to check out faster',
'checkout.message.user_registration':
'Your payment, address, and contact information will be saved in a new account.'
}

const mockAuthHelperFunctions = {
[AuthHelpers.AuthorizePasswordless]: {mutateAsync: jest.fn()},
[AuthHelpers.LoginPasswordlessUser]: {mutateAsync: jest.fn()}
Expand All @@ -36,13 +43,16 @@ jest.mock('@salesforce/commerce-sdk-react/hooks/useAuthContext', () =>

jest.mock('@salesforce/retail-react-app/app/components/otp-auth', () => {
// eslint-disable-next-line react/prop-types
const MockOtpAuth = function ({isOpen, handleOtpVerification, isGuestRegistration}) {
const MockOtpAuth = function ({isOpen, handleOtpVerification, onClose, isGuestRegistration}) {
return isOpen ? (
<>
<div data-testid={isGuestRegistration ? 'otp-guest' : 'otp-returning'} />
<button onClick={() => handleOtpVerification('otp-123')} data-testid="otp-verify">
Verify OTP
</button>
<button onClick={onClose} data-testid="otp-close">
Close
</button>
</>
) : null
}
Expand Down Expand Up @@ -93,7 +103,7 @@ const setup = (overrides = {}) => {
}

const utils = render(
<IntlProvider locale="en-GB" messages={{}}>
<IntlProvider locale="en-GB" messages={TEST_MESSAGES}>
<UserRegistration {...props} />
</IntlProvider>
)
Expand Down Expand Up @@ -203,7 +213,7 @@ describe('UserRegistration', () => {
expect(screen.getByRole('checkbox', {name: /Create an account/i})).toBeInTheDocument()
})

test('prevents duplicate OTP sends', async () => {
test('blocks duplicate OTP sends until reset', async () => {
const user = userEvent.setup()
const {authorizePasswordlessLogin} = setup()
const checkbox = screen.getByRole('checkbox', {name: /Create an account/i})
Expand All @@ -212,14 +222,105 @@ describe('UserRegistration', () => {
await waitFor(() => {
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(1)
})
// Click to disable
await user.click(checkbox)
// Click to enable again
// Click to enable again without unchecking/closing — should not send again
await user.click(checkbox)
// Should still only have been called once due to otpSentRef
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(1)
})

test('re-sends OTP after modal close and retry', async () => {
const user = userEvent.setup()
// Arrange mocks without rendering via setup()
const defaultBasket = {
basketId: 'basket-123',
customerInfo: {email: 'test@example.com'},
productItems: [{productId: 'sku-1', quantity: 1}],
shipments: [{shippingAddress: {address1: '123 Main'}, shippingMethod: {id: 'Ground'}}]
}
useCurrentBasket.mockReturnValue({data: defaultBasket})
useCustomerType.mockReturnValue({isGuest: true})
const authorizePasswordlessLogin =
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless]
authorizePasswordlessLogin.mutateAsync.mockResolvedValue({})
// Wrapper to control the enableUserRegistration prop to simulate real toggling
const Stateful = () => {
const [enabled, setEnabled] = React.useState(false)
return (
<IntlProvider locale="en-GB" messages={TEST_MESSAGES}>
<UserRegistration
enableUserRegistration={enabled}
setEnableUserRegistration={(val) => setEnabled(val)}
isGuestCheckout={false}
isDisabled={false}
onSavePreferenceChange={jest.fn()}
onRegistered={jest.fn()}
/>
</IntlProvider>
)
}
render(<Stateful />)
const checkbox = screen.getByRole('checkbox', {name: /Create an account/i})
// First enable triggers OTP send and opens modal
await user.click(checkbox) // enable -> true
await waitFor(() => {
expect(screen.getByTestId('otp-guest')).toBeInTheDocument()
})
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(1)
// Close the modal (this should reset the guard)
await user.click(screen.getByTestId('otp-close'))
// Toggle off then on to re-enable
await user.click(checkbox) // disable -> false
await user.click(checkbox) // enable -> true
// Should send OTP again after close + re-enable
await waitFor(() => {
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(2)
})
})

test('re-sends OTP after uncheck and re-check', async () => {
const user = userEvent.setup()
// Arrange mocks without rendering via setup()
const defaultBasket = {
basketId: 'basket-123',
customerInfo: {email: 'test@example.com'},
productItems: [{productId: 'sku-1', quantity: 1}],
shipments: [{shippingAddress: {address1: '123 Main'}, shippingMethod: {id: 'Ground'}}]
}
useCurrentBasket.mockReturnValue({data: defaultBasket})
useCustomerType.mockReturnValue({isGuest: true})
const authorizePasswordlessLogin =
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless]
authorizePasswordlessLogin.mutateAsync.mockResolvedValue({})
// Wrapper to control the enableUserRegistration prop to simulate real toggling
const Stateful = () => {
const [enabled, setEnabled] = React.useState(false)
return (
<IntlProvider locale="en-GB" messages={TEST_MESSAGES}>
<UserRegistration
enableUserRegistration={enabled}
setEnableUserRegistration={(val) => setEnabled(val)}
isGuestCheckout={false}
isDisabled={false}
onSavePreferenceChange={jest.fn()}
onRegistered={jest.fn()}
/>
</IntlProvider>
)
}
render(<Stateful />)
const checkbox = screen.getByRole('checkbox', {name: /Create an account/i})
// Enable -> first send
await user.click(checkbox) // enable -> true
await waitFor(() => {
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(1)
})
// Uncheck
await user.click(checkbox) // disable -> false
// Re-check -> should send again due to guard reset on uncheck
await user.click(checkbox) // enable -> true
await waitFor(() => {
expect(authorizePasswordlessLogin.mutateAsync).toHaveBeenCalledTimes(2)
})
})
test('OTP resend functionality works', async () => {
const user = userEvent.setup()
const {authorizePasswordlessLogin} = setup()
Expand Down
Loading