Skip to main content
ChallengeView is the single OTP component used across all verification flows in ailita-library — email change confirmation, TOTP entry, login OTP, and password recovery all render it. It handles auto-submission, resend cooldown, attempt tracking, and the dual-response discriminated union from a single interface. You never need to build a digit input or countdown timer yourself — mount ChallengeView with a challengeId and the correct callback, and the rest is handled internally.

Props

string
required
The ID of the challenge to verify. Obtained from whatever API call created the challenge — for example, LoginResponse.challengeId, EmailChangeResponse.challengeId, or ForgotPasswordResponse.challengeId. Pass this directly from the response that triggered the challenge flow.
(result: ChallengeResponse) => void
Called when a non-login challenge completes successfully. Receives the full ChallengeResponse with status: 'COMPLETED'. Use this for SIGNUP, RECOVERY, FRICTION, and EMAIL_CHANGE challenge types. Do not use this for LOGIN_OTP challenges — see onLoginSuccess below.
(result: LoginResponse) => void
Called when a LOGIN_OTP challenge completes and tokens have been set in the client. Receives the LoginResponse with a valid accessToken. Use this instead of onComplete when rendering ChallengeView for a login flow. By the time this callback fires, the access token and refresh token are already stored in memory — you can navigate directly to the authenticated area.
string
Optional heading override. When omitted, defaults to "Ingresa tu código 2FA" when type='TOTP', or "Verifica tu email" when type='EMAIL'.
string
Optional subtitle override. When omitted, defaults to authenticator-app instructions for TOTP, or “enter the 6-digit code sent to your email” for EMAIL.
'EMAIL' | 'TOTP'
Controls UI mode. Default: 'EMAIL'. When 'TOTP': the resend/regenerate button is hidden — TOTP codes rotate on a time-based schedule and cannot be re-requested. When 'EMAIL': a countdown timer and “Resend code” button appear once the cooldown expires and canRegenerate is true.

Dual-response contract

ChallengeView handles two distinct challenge categories, and each calls a different callback. Understanding which to implement is critical — using the wrong callback for a given challenge type will result in a broken integration.
Never use onComplete for a LOGIN_OTP challenge. The onComplete callback fires only for non-login challenges — the ChallengeResponse it receives does not contain tokens. For LOGIN_OTP, use onLoginSuccess, which fires after tokens are set in the client. If you accidentally wire onComplete for a login challenge, the callback will never fire and the user will appear stuck on the OTP screen.
The type guard that distinguishes the two response types (shown below for reference — you do not call this directly):
ChallengeView calls both callbacks through the complete() function in useChallenge. The hook resolves completeChallenge(challengeId, value) from the API, then checks 'accessToken' in data to determine which callback to invoke. You never interact with the type guard directly — just implement the right callback for your use case.

Expiry and resend handling

Challenges have a TTL (expiresAt from ChallengeResponse). ChallengeView surfaces two expiry-related behaviors depending on the challenge type.

Resend countdown (EMAIL type only)

After the challenge is created, a cooldown period prevents immediate resend (canRegenerateAt from ChallengeResponse). ChallengeView tracks this with a countdown timer sourced from useChallenge.secondsUntilRegenerate. The “Resend code” button is hidden during the countdown and appears only when the timer hits zero and canRegenerate is true.
Resend can only happen once per challenge — canRegenerate is set to false after the first regenerate call and the backend will reject further regenerate requests. If the user lets the challenge expire without completing it and has already used their one resend, they must restart the flow entirely (for example, go back to LoginForm or EmailChangeForm to generate a new challenge). Your error handling should account for this dead end.

Attempt limit

The component displays “Intento N de M” when attempts > 0. When attempts reaches maxAttempts, the backend marks the challenge as FAILED and subsequent submit calls will error. The component does not auto-redirect — your onComplete or onLoginSuccess handler is responsible for handling the FAILED state and offering the user a way to restart the flow.

TOTP (no regenerate)

When type='TOTP', the resend/regenerate section is entirely hidden. TOTP codes rotate on a time-based schedule — regenerating does not apply and will error if called. If the user’s authenticator app is generating wrong codes, the issue is clock drift: direct the user to sync their device clock in system settings. Do not render ChallengeView with type='EMAIL' for a TOTP challenge — the “Resend code” button will appear and fail on click since regenerate is not valid for authenticator-app challenges.

Usage examples

Login flow (LOGIN_OTP)

Email change confirmation (non-login)

2FA verification (TOTP)

ChallengeView auto-submits when all 6 digits are filled — the user does not need to press the Verify button. The button remains visible as a fallback for cases where focus is lost or auto-submit does not trigger. Paste support is built in: pasting a 6-digit string into any digit input fills all slots and triggers auto-submit automatically.

ChallengeResponse type

LoginResponse type

For LOGIN_OTP challenges, the onLoginSuccess callback receives a LoginResponse. By the time the callback fires, accessToken and refreshToken have already been stored in the client — the response is passed to your callback for inspection only (e.g., if you need to redirect or update local state).

Error handling

When a user submits an incorrect code, useChallenge catches the error, increments attempts, and displays the backend error message inline. ChallengeView resets the digit inputs automatically so the user can retry without clearing manually. Your onComplete and onLoginSuccess callbacks are only invoked on success — you do not need to handle errors inside them. If you need to respond to exhausted attempts or an expired challenge, check result.status in onComplete:
For LOGIN_OTP challenges, a FAILED status means the login attempt is rejected. Redirect the user back to LoginForm to start a new login session.

Next steps

Auth Flows

Login state machine and ChallengeView integration from the LoginForm perspective

Profile Module

EmailChangeForm and TOTPSetup — components that use ChallengeView internally