Skip to content

Commit 3df27b5

Browse files
committed
update component per UX alignment
Signed-off-by: d.phan <d.phan@salesforce.com>
1 parent be1f1de commit 3df27b5

File tree

7 files changed

+158
-74
lines changed

7 files changed

+158
-74
lines changed

packages/template-retail-react-app/app/components/otp-auth/index.jsx

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ const OtpAuth = ({
138138
}
139139

140140
const handleResend = async () => {
141+
// Only send OTP when cooldown is complete; button is always visible/enabled
142+
if (resendTimer > 0) return
143+
141144
setResendTimer(resendCooldownDuration)
142145
try {
143146
await track('/otp-resend', {
@@ -182,8 +185,6 @@ const OtpAuth = ({
182185
}
183186
}
184187

185-
const isResendDisabled = resendTimer > 0 || isVerifying
186-
187188
return (
188189
<Modal isOpen={isOpen} onClose={onClose} isCentered size="lg" closeOnOverlayClick={false}>
189190
<ModalOverlay />
@@ -240,42 +241,34 @@ const OtpAuth = ({
240241
width={12}
241242
height={14}
242243
borderRadius="md"
243-
borderColor="gray.300"
244+
borderColor={error ? 'red.500' : 'gray.300'}
244245
borderWidth={2}
245246
disabled={isVerifying}
246247
_focus={{
247-
borderColor: 'blue.500',
248-
boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)'
248+
borderColor: error ? 'red.500' : 'blue.500',
249+
boxShadow: error
250+
? '0 0 0 1px var(--chakra-colors-red-500)'
251+
: '0 0 0 1px var(--chakra-colors-blue-500)'
249252
}}
250253
_hover={{
251-
borderColor: 'gray.400'
254+
borderColor: error ? 'red.500' : 'gray.400'
252255
}}
253256
/>
254257
))}
255258
</SimpleGrid>
256259

257-
{/* Loading indicator during verification */}
258-
{isVerifying && (
259-
<Text fontSize="sm" color="blue.500">
260-
<FormattedMessage
261-
defaultMessage="Verifying code..."
262-
id="otp.message.verifying"
263-
/>
264-
</Text>
265-
)}
266-
267260
{/* Error message */}
268261
{error && (
269262
<Text fontSize="sm" color="red.500" textAlign="center">
270263
{error}
271264
</Text>
272265
)}
273266

274-
{/* Resend cooldown message */}
267+
{/* Countdown message */}
275268
{resendTimer > 0 && (
276269
<Text fontSize="sm" color="gray.600" textAlign="center">
277270
<FormattedMessage
278-
defaultMessage="You can request a new code in {timer} seconds."
271+
defaultMessage="You can request a new code in {timer} {timer, plural, one {second} other {seconds}}."
279272
id="otp.message.resend_cooldown"
280273
values={{timer: resendTimer}}
281274
/>
@@ -289,7 +282,6 @@ const OtpAuth = ({
289282
variant="solid"
290283
size="lg"
291284
minWidth={40}
292-
isDisabled={isVerifying}
293285
bg="gray.50"
294286
color="gray.800"
295287
fontWeight="bold"
@@ -318,13 +310,10 @@ const OtpAuth = ({
318310
onClick={handleResend}
319311
variant="solid"
320312
size="lg"
321-
colorScheme={isResendDisabled ? 'gray' : 'blue'}
322-
bg={isResendDisabled ? 'gray.300' : 'blue.500'}
313+
colorScheme="blue"
314+
bg="blue.500"
323315
minWidth={40}
324-
isDisabled={isResendDisabled}
325-
_hover={isResendDisabled ? {} : {bg: 'blue.600'}}
326-
_disabled={{bg: 'gray.300', color: 'gray.600'}}
327-
aria-disabled={isResendDisabled}
316+
_hover={{bg: 'blue.600'}}
328317
>
329318
<FormattedMessage
330319
defaultMessage="Resend Code"

packages/template-retail-react-app/app/components/otp-auth/index.test.js

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('OtpAuth', () => {
9595

9696
describe('Component Rendering', () => {
9797
test('renders OTP form with all elements', () => {
98-
renderWithProviders(<WrapperComponent resendCooldownDuration={0} />)
98+
renderWithProviders(<WrapperComponent />)
9999

100100
expect(screen.getByText("Confirm it's you")).toBeInTheDocument()
101101
expect(
@@ -120,7 +120,7 @@ describe('OtpAuth', () => {
120120
})
121121

122122
test('renders buttons with correct styling', () => {
123-
renderWithProviders(<WrapperComponent resendCooldownDuration={0} />)
123+
renderWithProviders(<WrapperComponent />)
124124

125125
const guestButton = screen.getByRole('button', {name: /Checkout as a Guest/i})
126126
const resendButton = screen.getByRole('button', {name: /Resend Code/i})
@@ -415,7 +415,7 @@ describe('OtpAuth', () => {
415415
expect(mockHandleSendEmailOtp).toHaveBeenCalledWith('test@example.com', true)
416416
})
417417

418-
test('resend button is disabled during initial cooldown when modal opens (default 30s)', () => {
418+
test('countdown message is shown on initial open and both buttons are always visible', () => {
419419
renderWithProviders(
420420
<OtpAuth
421421
isOpen={true}
@@ -426,14 +426,13 @@ describe('OtpAuth', () => {
426426
/>
427427
)
428428

429-
expect(
430-
screen.getByText(/You can request a new code in 30 seconds/i)
431-
).toBeInTheDocument()
432-
const resendButton = screen.getByRole('button', {name: /Resend Code/i})
433-
expect(resendButton).toBeDisabled()
429+
// FormattedMessage may render "30 second(s)." (ICU plural)
430+
expect(screen.getByText(/You can request a new code in 30 second/i)).toBeInTheDocument()
431+
expect(screen.getByRole('button', {name: /Resend Code/i})).toBeInTheDocument()
432+
expect(screen.getByRole('button', {name: /Checkout as a Guest/i})).toBeInTheDocument()
434433
})
435434

436-
test('resend cooldown uses custom prop when provided', () => {
435+
test('countdown message uses custom cooldown duration when provided', () => {
437436
renderWithProviders(
438437
<OtpAuth
439438
isOpen={true}
@@ -445,12 +444,12 @@ describe('OtpAuth', () => {
445444
/>
446445
)
447446

448-
expect(
449-
screen.getByText(/You can request a new code in 15 seconds/i)
450-
).toBeInTheDocument()
447+
// FormattedMessage may render "15 second(s)." (ICU plural)
448+
expect(screen.getByText(/You can request a new code in 15 second/i)).toBeInTheDocument()
449+
expect(screen.getByRole('button', {name: /Resend Code/i})).toBeInTheDocument()
451450
})
452451

453-
test('resend button is disabled during countdown after clicking Resend', async () => {
452+
test('clicking Resend during cooldown does not send OTP', async () => {
454453
const user = userEvent.setup()
455454
renderWithProviders(
456455
<OtpAuth
@@ -459,25 +458,69 @@ describe('OtpAuth', () => {
459458
form={mockForm}
460459
handleOtpVerification={mockHandleOtpVerification}
461460
handleSendEmailOtp={mockHandleSendEmailOtp}
462-
resendCooldownDuration={5}
463461
/>
464462
)
465463

466-
// Wait for initial 5s cooldown to expire so Resend is clickable
464+
// Resend button is always visible; during initial 30s cooldown it should not send
465+
const resendButton = screen.getByRole('button', {name: /Resend Code/i})
466+
await user.click(resendButton)
467+
await user.click(resendButton)
468+
469+
expect(mockHandleSendEmailOtp).not.toHaveBeenCalled()
470+
})
471+
472+
test('clicking Resend when cooldown is complete sends OTP and shows countdown', async () => {
473+
const user = userEvent.setup()
474+
renderWithProviders(
475+
<OtpAuth
476+
isOpen={true}
477+
onClose={mockOnClose}
478+
form={mockForm}
479+
handleOtpVerification={mockHandleOtpVerification}
480+
handleSendEmailOtp={mockHandleSendEmailOtp}
481+
resendCooldownDuration={0}
482+
/>
483+
)
484+
485+
const resendButton = screen.getByRole('button', {name: /Resend Code/i})
486+
await user.click(resendButton)
487+
488+
expect(mockHandleSendEmailOtp).toHaveBeenCalledWith('test@example.com', true)
489+
})
490+
491+
test('clicking Resend again during cooldown after first send does not send again', async () => {
492+
const user = userEvent.setup()
493+
renderWithProviders(
494+
<OtpAuth
495+
isOpen={true}
496+
onClose={mockOnClose}
497+
form={mockForm}
498+
handleOtpVerification={mockHandleOtpVerification}
499+
handleSendEmailOtp={mockHandleSendEmailOtp}
500+
resendCooldownDuration={2}
501+
/>
502+
)
503+
504+
// Wait for initial 2s cooldown to expire (text is "X second(s)." from FormattedMessage)
467505
await waitFor(
468506
() => {
469-
expect(screen.getByRole('button', {name: /Resend Code/i})).not.toBeDisabled()
507+
expect(
508+
screen.queryByText(/You can request a new code in \d+ second/i)
509+
).not.toBeInTheDocument()
470510
},
471-
{timeout: 6000}
511+
{timeout: 3000}
472512
)
473513

474514
await user.click(screen.getByRole('button', {name: /Resend Code/i}))
515+
expect(mockHandleSendEmailOtp).toHaveBeenCalledTimes(1)
475516

517+
// Countdown is showing; click Resend again - should not send
476518
expect(
477-
await screen.findByText(/You can request a new code in 5 seconds/i)
519+
screen.getByText(/You can request a new code in \d+ second/i)
478520
).toBeInTheDocument()
479-
const resendButton = screen.getByRole('button', {name: /Resend Code/i})
480-
expect(resendButton).toBeDisabled()
521+
await user.click(screen.getByRole('button', {name: /Resend Code/i}))
522+
523+
expect(mockHandleSendEmailOtp).toHaveBeenCalledTimes(1)
481524
})
482525
})
483526

@@ -521,7 +564,7 @@ describe('OtpAuth', () => {
521564
})
522565

523566
test('buttons have accessible text', () => {
524-
renderWithProviders(<WrapperComponent resendCooldownDuration={0} />)
567+
renderWithProviders(<WrapperComponent />)
525568

526569
expect(screen.getByRole('button', {name: /Checkout as a Guest/i})).toBeInTheDocument()
527570
expect(screen.getByRole('button', {name: /Resend Code/i})).toBeInTheDocument()

packages/template-retail-react-app/app/static/translations/compiled/en-GB.json

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3116,13 +3116,35 @@
31163116
},
31173117
{
31183118
"type": 0,
3119-
"value": " seconds."
3120-
}
3121-
],
3122-
"otp.message.verifying": [
3119+
"value": " "
3120+
},
3121+
{
3122+
"offset": 0,
3123+
"options": {
3124+
"one": {
3125+
"value": [
3126+
{
3127+
"type": 0,
3128+
"value": "second"
3129+
}
3130+
]
3131+
},
3132+
"other": {
3133+
"value": [
3134+
{
3135+
"type": 0,
3136+
"value": "seconds"
3137+
}
3138+
]
3139+
}
3140+
},
3141+
"pluralType": "cardinal",
3142+
"type": 6,
3143+
"value": "timer"
3144+
},
31233145
{
31243146
"type": 0,
3125-
"value": "Verifying code..."
3147+
"value": "."
31263148
}
31273149
],
31283150
"otp.title.confirm_its_you": [

packages/template-retail-react-app/app/static/translations/compiled/en-US.json

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3116,13 +3116,35 @@
31163116
},
31173117
{
31183118
"type": 0,
3119-
"value": " seconds."
3120-
}
3121-
],
3122-
"otp.message.verifying": [
3119+
"value": " "
3120+
},
3121+
{
3122+
"offset": 0,
3123+
"options": {
3124+
"one": {
3125+
"value": [
3126+
{
3127+
"type": 0,
3128+
"value": "second"
3129+
}
3130+
]
3131+
},
3132+
"other": {
3133+
"value": [
3134+
{
3135+
"type": 0,
3136+
"value": "seconds"
3137+
}
3138+
]
3139+
}
3140+
},
3141+
"pluralType": "cardinal",
3142+
"type": 6,
3143+
"value": "timer"
3144+
},
31233145
{
31243146
"type": 0,
3125-
"value": "Verifying code..."
3147+
"value": "."
31263148
}
31273149
],
31283150
"otp.title.confirm_its_you": [

packages/template-retail-react-app/app/static/translations/compiled/en-XA.json

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6576,21 +6576,35 @@
65766576
},
65776577
{
65786578
"type": 0,
6579-
"value": " şḗḗƈǿǿƞḓş."
6579+
"value": " "
65806580
},
65816581
{
6582-
"type": 0,
6583-
"value": "]"
6584-
}
6585-
],
6586-
"otp.message.verifying": [
6587-
{
6588-
"type": 0,
6589-
"value": "["
6582+
"offset": 0,
6583+
"options": {
6584+
"one": {
6585+
"value": [
6586+
{
6587+
"type": 0,
6588+
"value": "şḗḗƈǿǿƞḓ"
6589+
}
6590+
]
6591+
},
6592+
"other": {
6593+
"value": [
6594+
{
6595+
"type": 0,
6596+
"value": "şḗḗƈǿǿƞḓş"
6597+
}
6598+
]
6599+
}
6600+
},
6601+
"pluralType": "cardinal",
6602+
"type": 6,
6603+
"value": "timer"
65906604
},
65916605
{
65926606
"type": 0,
6593-
"value": "Ṽḗḗřīƒẏīƞɠ ƈǿǿḓḗḗ..."
6607+
"value": "."
65946608
},
65956609
{
65966610
"type": 0,

packages/template-retail-react-app/translations/en-GB.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,10 +1304,7 @@
13041304
"defaultMessage": "To log in to your account, enter the code sent to your email."
13051305
},
13061306
"otp.message.resend_cooldown": {
1307-
"defaultMessage": "You can request a new code in {timer} seconds."
1308-
},
1309-
"otp.message.verifying": {
1310-
"defaultMessage": "Verifying code..."
1307+
"defaultMessage": "You can request a new code in {timer} {timer, plural, one {second} other {seconds}}."
13111308
},
13121309
"otp.title.confirm_its_you": {
13131310
"defaultMessage": "Confirm it's you"

packages/template-retail-react-app/translations/en-US.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,10 +1304,7 @@
13041304
"defaultMessage": "To log in to your account, enter the code sent to your email."
13051305
},
13061306
"otp.message.resend_cooldown": {
1307-
"defaultMessage": "You can request a new code in {timer} seconds."
1308-
},
1309-
"otp.message.verifying": {
1310-
"defaultMessage": "Verifying code..."
1307+
"defaultMessage": "You can request a new code in {timer} {timer, plural, one {second} other {seconds}}."
13111308
},
13121309
"otp.title.confirm_its_you": {
13131310
"defaultMessage": "Confirm it's you"

0 commit comments

Comments
 (0)