registry
@automindzsheets 01–77shadcn 4.x
SHEET 58

TOTP Setup

block / auth

The whole turn-on-two-factor flow in one component — scan the QR or copy the key, confirm with the first code, save the recovery codes shown once — with every state owned, one native input instead of six boxes, and the secret coming only from your server.

type
block
files
2
registry deps
button, input, label, checkbox, skeleton, @automindz/qr-code
npm deps
lucide-react

installs

  • lib/automindz/totp.ts
  • components/automindz/totp-setup.tsx
  • Secret comes from the server — no default anywhere in the item
  • Awaited onVerify: pending, wrong-code and failed-request are three states
  • Recovery codes shown once; a checkbox gates Done

Pass what the server generated and two callbacks: <TotpSetup enrollment={{ secret, issuer, account }} onVerify={(code) => verify(code)} onDone={() => router.push("/settings/security")} />. **There is no default secret in lib/automindz/totp.ts, on purpose** — a constant would be the same second factor for every account — so the component shows a skeleton the size of the real layout until enrollment arrives, and a new secret resets the form because a new secret is a new enrolment. onVerify is **awaited** and returns { ok: true, recoveryCodes? } or { ok: false, reason? }: while it runs the whole <fieldset> is disabled (not just the button, so a second code cannot be typed over the one in flight), a wrong code is reported in place with the reason and the field cleared and refocused, and a *thrown* error is a different state — the code stays in the field and a Retry resends it. Return recoveryCodes only when the server generated them in the same call; when it does they are shown **once**, with Copy, a .txt download and a checkbox that gates onDone — the one confirmation this item insists on, because a user who leaves without saving them is locked out the day they lose the phone. Omit onDone and no button renders. error + onRetry is the enrolment failing to generate, said in place; unavailableReason shows the setting blocked and explained ("already enabled", "managed by your SSO provider") rather than hidden. It is **one native <input inputMode="numeric" autoComplete="one-time-code" maxLength={digits}>, not six boxes**, so paste, SMS and iOS autofill, backspace and screen readers all come from the platform — which is why there is no input-otp dependency. totpUri() builds the otpauth:// URI the QR carries: label parts are encoded separately (a colon or a space in the issuer cannot break the label), the secret is normalised (upper-case, no spaces, no =), and algorithm, digits and period are **left off unless set**, because several authenticators ignore non-defaults and a server verifying SHA256 or 8 digits against them rejects every code while the setup screen looks fine — set them only when tested end to end. The QR sits in a bg-background frame with margin={0} so it has a light ground on a dark card; the manual key beside it is real text in a <details>, grouped in fours, so a user with no camera has an equal path. The only motion is the spinner, with motion-reduce:animate-none.

Variations

one state per section

01

Scan, confirm, save the codes

Type 123456 to succeed — the fake server returns eight recovery codes, shown once, with Copy, a .txt download and the checkbox that gates Done. Any other six digits is a wrong code: reported in place with the reason, the field cleared and refocused. Type 999999 and the request itself fails, which is a different state — the code stays in the field and a Retry resends it. While a code is in flight the whole fieldset is disabled, not just the button. The manual key under "Can't scan?" is the same secret the QR carries, grouped in fours. The only motion here is the spinner, and it carries motion-reduce:animate-none.

fluid
Two-factor authentication
Add a second step to sign-in with an authenticator app.
  1. Scan this code with your authenticator app

    Any app that generates time-based codes works — Google Authenticator, 1Password, Authy, Microsoft Authenticator.

    Can’t scan? Enter the key manually
    GEZD GNBV GY3T QOJQ GEZD GNBV GY3T QOJQ

    Time-based, 6 digits, a new code every 30 seconds.

  2. Enter the 6-digit code the app shows

    Codes change every 30 seconds. If yours just changed, wait for the next one.

02

Success without recovery codes

onVerify returned { ok: true } with no recoveryCodes, so the success state is just the confirmation and Done — nothing to save, nothing to gate. Return codes only when the server generated them in the same call.

fluid
Two-factor authentication
Add a second step to sign-in with an authenticator app.
  1. Scan this code with your authenticator app

    Any app that generates time-based codes works — Google Authenticator, 1Password, Authy, Microsoft Authenticator.

    Can’t scan? Enter the key manually
    GEZD GNBV GY3T QOJQ GEZD GNBV GY3T QOJQ

    Time-based, 6 digits, a new code every 30 seconds.

  2. Enter the 6-digit code the app shows

    Codes change every 30 seconds. If yours just changed, wait for the next one.

03

Waiting for the server's secret

enrollment is undefined while the server generates it, and the skeleton is the shape of the real layout — two step headings, the QR square, the key line, the input row — so nothing moves when the enrolment lands. There is no default secret anywhere in this item: a constant would be the same second factor for every account.

fluid
Two-factor authentication
Add a second step to sign-in with an authenticator app.
04

The enrolment could not be generated

error renders in place as a role="alert" with the reason, and onRetry gives it a button that stays busy while the retry runs — here it takes 1.2s and then the real form lands.

fluid
Two-factor authentication
Add a second step to sign-in with an authenticator app.
05

Blocked, and it says why

unavailableReason is different from hiding the panel: the setting is still on the page, so nobody asks where two-factor went, and the copy says who to talk to. Use it for "already enabled", "disabled by policy" or "not on this plan".

fluid
Two-factor authentication
Add a second step to sign-in with an authenticator app.

Two-factor setup is unavailable

Two-factor authentication is managed by your workspace's single sign-on provider. Ask a workspace admin to change it there.