From a6a4618cadc0ea9bb2c099c11de1468aaed3456c Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 10 Jul 2026 16:52:58 +0530 Subject: [PATCH 1/3] feat: show a distinct, non-dismissible state for TOTP lockout verify_otp's per-account lockout (5 failed attempts/15min, backend PR authorizerdev/authorizer#670) returned the same generic dismissible error as a plain wrong code, so the form stayed submittable and users had no signal to stop retrying. Key off the new extensions.code (TOO_MANY_REQUESTS, requires @authorizerdev/authorizer-js with extensions.code support - authorizerdev/authorizer-js#39) to disable the OTP input and submit button and keep the message on screen instead of letting it be dismissed. No countdown: the backend doesn't expose remaining lockout duration, so this shows a static message rather than a timer that could be wrong. --- src/components/AuthorizerVerifyOtp.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/AuthorizerVerifyOtp.tsx b/src/components/AuthorizerVerifyOtp.tsx index f2badc5..83c583b 100644 --- a/src/components/AuthorizerVerifyOtp.tsx +++ b/src/components/AuthorizerVerifyOtp.tsx @@ -23,6 +23,7 @@ export const AuthorizerVerifyOtp: FC<{ const [successMessage, setSuccessMessage] = useState(``); const [loading, setLoading] = useState(false); const [sendingOtp, setSendingOtp] = useState(false); + const [isLockedOut, setIsLockedOut] = useState(false); const [formData, setFormData] = useState({ otp: null, }); @@ -56,6 +57,9 @@ export const AuthorizerVerifyOtp: FC<{ data.is_totp = !!is_totp; const { data: res, errors } = await authorizerRef.verifyOtp(data); if (errors && errors.length) { + if (errors[0]?.code === 'TOO_MANY_REQUESTS') { + setIsLockedOut(true); + } setError(errors[0]?.message || ``); return; } @@ -134,7 +138,11 @@ export const AuthorizerVerifyOtp: FC<{ /> )} {error && ( - + )}

Please enter the OTP sent to your email or phone number or authenticator @@ -155,6 +163,7 @@ export const AuthorizerVerifyOtp: FC<{ type="password" value={formData.otp || ''} onChange={(e) => onInputChange('otp', e.target.value)} + disabled={isLockedOut} /> {errorData.otp && (

{errorData.otp}
@@ -172,7 +181,7 @@ export const AuthorizerVerifyOtp: FC<{
{loading ? `Processing ...` : `Submit`} From 7153189e5b846978d8bb20449da7ce2d7af72a32 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 10 Jul 2026 17:08:12 +0530 Subject: [PATCH 2/3] fix: TOTP-specific OTP screen issues in AuthorizerVerifyOtp - Hide "Resend OTP" when is_totp is true: TOTP codes are generated locally by an authenticator app on a 30s rolling window, never sent by the server, so offering to resend was misleading and the backend call for it doesn't apply to this flow. - Add autocomplete="one-time-code" to the OTP input so browsers/OS can offer native autofill for authenticator/SMS codes. --- src/components/AuthorizerVerifyOtp.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/AuthorizerVerifyOtp.tsx b/src/components/AuthorizerVerifyOtp.tsx index 83c583b..aa10880 100644 --- a/src/components/AuthorizerVerifyOtp.tsx +++ b/src/components/AuthorizerVerifyOtp.tsx @@ -161,6 +161,7 @@ export const AuthorizerVerifyOtp: FC<{ }`} placeholder="e.g.- AB123C" type="password" + autoComplete="one-time-code" value={formData.otp || ''} onChange={(e) => onInputChange('otp', e.target.value)} disabled={isLockedOut} @@ -189,13 +190,14 @@ export const AuthorizerVerifyOtp: FC<{ {setView && ( - {sendingOtp ? ( -
Sending ...
- ) : ( - - Resend OTP - - )} + {!is_totp && + (sendingOtp ? ( +
Sending ...
+ ) : ( + + Resend OTP + + ))} {config.is_sign_up_enabled && (
Don't have an account?{' '} From 57be03eac902342c046623259f797d2cb9905c30 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 11 Jul 2026 11:59:55 +0530 Subject: [PATCH 3/3] fix: fallback message when the lockout error has no text If the server ever sends TOO_MANY_REQUESTS with an empty message, the form previously went completely silent: input and Submit both disabled, no explanation rendered at all (Message returns null for blank text). Added a fixed fallback string so the user always has some explanation for why the form just went dead, regardless of what the server sent. --- src/components/AuthorizerVerifyOtp.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/AuthorizerVerifyOtp.tsx b/src/components/AuthorizerVerifyOtp.tsx index aa10880..e673c71 100644 --- a/src/components/AuthorizerVerifyOtp.tsx +++ b/src/components/AuthorizerVerifyOtp.tsx @@ -59,6 +59,16 @@ export const AuthorizerVerifyOtp: FC<{ if (errors && errors.length) { if (errors[0]?.code === 'TOO_MANY_REQUESTS') { setIsLockedOut(true); + // Fall back to a fixed message if the server ever sends an empty + // one: the form is about to go fully disabled (input + submit), + // so this is the user's only explanation for why - an empty + // string here would mean no message renders at all (Message + // returns null for blank text) and the form goes silently dead. + setError( + errors[0]?.message || + `Too many attempts. Please wait a while before trying again.`, + ); + return; } setError(errors[0]?.message || ``); return;