@@ -37,7 +37,8 @@ const OtpAuth = ({
3737 handleOtpVerification,
3838 onCheckoutAsGuest,
3939 isGuestRegistration = false ,
40- hideCheckoutAsGuestButton = false
40+ hideCheckoutAsGuestButton = false ,
41+ resendCooldownDuration = 30
4142} ) => {
4243 const { tokenLength} = getConfig ( ) . app . login
4344 const parsedLength = Number ( tokenLength )
@@ -99,6 +100,8 @@ const OtpAuth = ({
99100 otpInputs . clear ( )
100101 setError ( '' )
101102 form . setValue ( 'otp' , '' )
103+ // Start resend cooldown when modal opens
104+ setResendTimer ( resendCooldownDuration )
102105
103106 // Track OTP modal view activity
104107 track ( '/otp-authentication' , {
@@ -108,10 +111,10 @@ const OtpAuth = ({
108111
109112 setTimeout ( ( ) => otpInputs . inputRefs . current [ 0 ] ?. focus ( ) , 100 )
110113 }
111- } , [ isOpen ] )
114+ } , [ isOpen , resendCooldownDuration ] )
112115
113116 const handleVerify = async ( code = otpInputs . values . join ( '' ) ) => {
114- if ( code . length !== OTP_LENGTH ) return
117+ if ( isVerifying || code . length !== OTP_LENGTH ) return
115118
116119 setIsVerifying ( true )
117120 setError ( '' )
@@ -147,14 +150,17 @@ const OtpAuth = ({
147150 }
148151
149152 const handleResend = async ( ) => {
150- setResendTimer ( 5 )
153+ // No action while verifying or during cooldown; button stays visible/enabled
154+ if ( isVerifying || resendTimer > 0 ) return
155+
156+ setResendTimer ( resendCooldownDuration )
151157 try {
152158 await track ( '/otp-resend' , {
153159 activity : 'otp_code_resent' ,
154160 context : 'authentication' ,
155161 resendAttempt : true
156162 } )
157- await handleSendEmailOtp ( form . getValues ( 'email' ) )
163+ await handleSendEmailOtp ( form . getValues ( 'email' ) , true )
158164 } catch ( error ) {
159165 setResendTimer ( 0 )
160166 await track ( '/otp-resend-failed' , {
@@ -167,6 +173,8 @@ const OtpAuth = ({
167173 }
168174
169175 const handleCheckoutAsGuest = async ( ) => {
176+ if ( isVerifying ) return
177+
170178 // Track checkout as guest selection
171179 await track ( '/checkout-as-guest' , {
172180 activity : 'checkout_as_guest_selected' ,
@@ -191,8 +199,6 @@ const OtpAuth = ({
191199 }
192200 }
193201
194- const isResendDisabled = resendTimer > 0 || isVerifying
195-
196202 return (
197203 < Modal isOpen = { isOpen } onClose = { onClose } isCentered size = "lg" closeOnOverlayClick = { false } >
198204 < ModalOverlay />
@@ -213,7 +219,7 @@ const OtpAuth = ({
213219 < ModalCloseButton disabled = { isVerifying } />
214220 < ModalBody pb = { 6 } >
215221 < Stack spacing = { 12 } paddingLeft = { 4 } paddingRight = { 4 } alignItems = "center" >
216- < Text fontSize = "md" maxWidth = "300px" textAlign = "center" >
222+ < Text fontSize = "md" maxWidth = { 80 } textAlign = "center" >
217223 { isGuestRegistration ? (
218224 < FormattedMessage
219225 defaultMessage = "We sent a one-time password (OTP) to your email. To create your account and proceed to checkout, enter the {otpLength}-digit code below."
@@ -228,116 +234,111 @@ const OtpAuth = ({
228234 ) }
229235 </ Text >
230236
231- { /* OTP Input */ }
232- < SimpleGrid columns = { OTP_LENGTH } spacing = { 3 } >
233- { Array . from ( { length : OTP_LENGTH } ) . map ( ( _ , index ) => (
234- < Input
235- key = { index }
236- ref = { ( el ) => ( otpInputs . inputRefs . current [ index ] = el ) }
237- value = { otpInputs . values [ index ] }
238- onChange = { ( e ) => handleInputChange ( index , e . target . value ) }
239- onKeyDown = { ( e ) => otpInputs . handleKeyDown ( index , e ) }
240- onPaste = { otpInputs . handlePaste }
241- type = "text"
242- inputMode = "numeric"
243- maxLength = { 1 }
244- textAlign = "center"
245- fontSize = "lg"
246- fontWeight = "bold"
247- size = "lg"
248- width = "48px"
249- height = "56px"
250- borderRadius = "md"
251- borderColor = "gray.300"
252- borderWidth = "2px"
253- disabled = { isVerifying }
254- _focus = { {
255- borderColor : 'blue.500' ,
256- boxShadow : '0 0 0 1px var(--chakra-colors-blue-500)'
257- } }
258- _hover = { {
259- borderColor : 'gray.400'
260- } }
261- />
262- ) ) }
263- </ SimpleGrid >
237+ < Stack spacing = { 6 } width = "100%" alignItems = "center" >
238+ { /* OTP Input */ }
239+ < SimpleGrid columns = { OTP_LENGTH } spacing = { 3 } >
240+ { Array . from ( { length : OTP_LENGTH } ) . map ( ( _ , index ) => (
241+ < Input
242+ key = { index }
243+ ref = { ( el ) => ( otpInputs . inputRefs . current [ index ] = el ) }
244+ value = { otpInputs . values [ index ] }
245+ onChange = { ( e ) => handleInputChange ( index , e . target . value ) }
246+ onKeyDown = { ( e ) => otpInputs . handleKeyDown ( index , e ) }
247+ onPaste = { otpInputs . handlePaste }
248+ type = "text"
249+ inputMode = "numeric"
250+ maxLength = { 1 }
251+ textAlign = "center"
252+ fontSize = "lg"
253+ fontWeight = "bold"
254+ size = "lg"
255+ width = { 12 }
256+ height = { 14 }
257+ borderRadius = "md"
258+ borderColor = { error ? 'red.500' : 'gray.300' }
259+ borderWidth = { 2 }
260+ disabled = { isVerifying }
261+ _focus = { {
262+ borderColor : error ? 'red.500' : 'blue.500' ,
263+ boxShadow : error
264+ ? '0 0 0 1px var(--chakra-colors-red-500)'
265+ : '0 0 0 1px var(--chakra-colors-blue-500)'
266+ } }
267+ _hover = { {
268+ borderColor : error ? 'red.500' : 'gray.400'
269+ } }
270+ />
271+ ) ) }
272+ </ SimpleGrid >
264273
265- { /* Loading indicator during verification */ }
266- { isVerifying && (
267- < Text fontSize = "sm" color = "blue.500" >
268- < FormattedMessage
269- defaultMessage = "Verifying code..."
270- id = "otp.message.verifying"
271- />
272- </ Text >
273- ) }
274+ { /* Error message */ }
275+ { error && (
276+ < Text fontSize = "sm" color = "red.500" textAlign = "center" >
277+ { error }
278+ </ Text >
279+ ) }
274280
275- { /* Error message */ }
276- { error && (
277- < Text fontSize = "sm" color = "red.500" textAlign = "center" >
278- { error }
279- </ Text >
280- ) }
281+ { /* Countdown message */ }
282+ { resendTimer > 0 && (
283+ < Text fontSize = "sm" color = "gray.600" textAlign = "center" >
284+ < FormattedMessage
285+ defaultMessage = "You can request a new code in {timer} {timer, plural, one {second} other {seconds}}."
286+ id = "otp.message.resend_cooldown"
287+ values = { { timer : resendTimer } }
288+ />
289+ </ Text >
290+ ) }
291+
292+ { /* Buttons */ }
293+ < HStack spacing = { 4 } width = "100%" justifyContent = "flex-end" >
294+ { ! hideCheckoutAsGuestButton && (
295+ < Button
296+ onClick = { handleCheckoutAsGuest }
297+ variant = "solid"
298+ size = "lg"
299+ minWidth = { 40 }
300+ isDisabled = { isVerifying }
301+ bg = "gray.50"
302+ color = "gray.800"
303+ fontWeight = "bold"
304+ border = "none"
305+ _hover = { {
306+ bg : 'gray.100'
307+ } }
308+ _active = { {
309+ bg : 'gray.200'
310+ } }
311+ >
312+ { isGuestRegistration ? (
313+ < FormattedMessage
314+ defaultMessage = "Cancel"
315+ id = "otp.button.cancel_guest_registration"
316+ />
317+ ) : (
318+ < FormattedMessage
319+ defaultMessage = "Checkout as a Guest"
320+ id = "otp.button.checkout_as_guest"
321+ />
322+ ) }
323+ </ Button >
324+ ) }
281325
282- { /* Buttons */ }
283- < HStack spacing = { 4 } width = "100%" justifyContent = "center" >
284- { ! hideCheckoutAsGuestButton && (
285326 < Button
286- onClick = { handleCheckoutAsGuest }
327+ onClick = { handleResend }
287328 variant = "solid"
288329 size = "lg"
289- minWidth = "160px"
290- isDisabled = { isVerifying }
291- bg = "gray.50"
292- color = "gray.800"
293- fontWeight = "bold"
294- border = "none"
295- _hover = { {
296- bg : 'gray.100'
297- } }
298- _active = { {
299- bg : 'gray.200'
300- } }
330+ colorScheme = "blue"
331+ bg = "blue.500"
332+ minWidth = { 40 }
333+ _hover = { { bg : 'blue.600' } }
301334 >
302- { isGuestRegistration ? (
303- < FormattedMessage
304- defaultMessage = "Cancel"
305- id = "otp.button.cancel_guest_registration"
306- />
307- ) : (
308- < FormattedMessage
309- defaultMessage = "Checkout as a Guest"
310- id = "otp.button.checkout_as_guest"
311- />
312- ) }
313- </ Button >
314- ) }
315-
316- < Button
317- onClick = { handleResend }
318- variant = "solid"
319- size = "lg"
320- colorScheme = { isResendDisabled ? 'gray' : 'blue' }
321- bg = { isResendDisabled ? 'gray.300' : 'blue.500' }
322- minWidth = "160px"
323- isDisabled = { isResendDisabled }
324- _hover = { isResendDisabled ? { } : { bg : 'blue.600' } }
325- _disabled = { { bg : 'gray.300' , color : 'gray.600' } }
326- >
327- { resendTimer > 0 ? (
328- < FormattedMessage
329- defaultMessage = "Resend code in {timer} seconds..."
330- id = "otp.button.resend_timer"
331- values = { { timer : resendTimer } }
332- />
333- ) : (
334335 < FormattedMessage
335336 defaultMessage = "Resend Code"
336337 id = "otp.button.resend_code"
337338 />
338- ) }
339- </ Button >
340- </ HStack >
339+ </ Button >
340+ </ HStack >
341+ </ Stack >
341342 </ Stack >
342343 </ ModalBody >
343344 </ ModalContent >
@@ -353,7 +354,9 @@ OtpAuth.propTypes = {
353354 handleOtpVerification : PropTypes . func . isRequired ,
354355 onCheckoutAsGuest : PropTypes . func ,
355356 isGuestRegistration : PropTypes . bool ,
356- hideCheckoutAsGuestButton : PropTypes . bool
357+ hideCheckoutAsGuestButton : PropTypes . bool ,
358+ /** Resend cooldown (in seconds). Default 30. */
359+ resendCooldownDuration : PropTypes . number
357360}
358361
359362export default OtpAuth
0 commit comments