Skip to content

Custom flows (headless)

The drop-in <SignUp> and <ForgotPassword> cards handle the full flow for you. When you need your own markup and copy, the credential & flow hooks let you drive the exact same flows. The network and parsing still run on the Torii runtime, so you don’t reimplement (or pin) the security-critical logic: you only build the UI.

Everything here requires a <ToriiProvider> ancestor.

Sign-up is two steps: start() validates and emails a 6-digit code, then verifyCode() confirms it and signs the user in. The session is applied automatically on success.

import { useSignUp } from '@torii-js/torii-react';
function SignUp() {
const { state, start, verifyCode, resend } = useSignUp();
if (state.status === 'needs-code' || state.status === 'code-invalid') {
return (
<CodeForm
invalid={state.status === 'code-invalid'}
onSubmit={(code) => verifyCode(code)}
onResend={resend}
/>
);
}
if (state.status === 'code-expired') return <Expired onRestart={() => location.reload()} />;
return (
<DetailsForm
busy={state.status === 'submitting'}
error={state.status === 'error' ? state.error : null}
onSubmit={(email, password) => start({ email, password, legalConsentAccepted: true })}
/>
);
}

If the environment requires legal consent, useSignUp() surfaces what you need:

const { start, legalSettings } = useSignUp();
// Render your consent checkbox from legalSettings, then:
await start({ email, password, legalConsentAccepted: true });

If you skip required consent, start() resolves to { status: 'error' } with the reason in error.code.

import { usePasswordReset } from '@torii-js/torii-react';
function ResetPassword() {
const { state, request, confirm } = usePasswordReset();
if (state.status === 'idle' || state.status === 'requesting') {
return <EmailForm busy={state.status === 'requesting'} onSubmit={(email) => request(email)} />;
}
if (state.status === 'done') return <Done />;
// 'code-sent' | 'resetting' | 'code-invalid' | 'error'
return (
<NewPasswordForm
invalid={state.status === 'code-invalid'}
onSubmit={(code, newPassword) => confirm({ code, newPassword })}
/>
);
}

request() always resolves to code-sent on success regardless of whether the email exists, so your UI can’t be used to enumerate accounts.

Available when the environment has Passwordless email code enabled (see authentication configuration). There is no dedicated hook: the two calls sit directly on useAuth(), because each is a single request with no intermediate state to manage.

import { useAuth } from '@torii-js/torii-react';
function EmailCodeSignIn() {
const { startEmailCodeSignIn, verifyEmailCodeSignIn, signIn } = useAuth();
const [sent, setSent] = useState(false);
async function request(email: string) {
const outcome = await startEmailCodeSignIn({ email });
// 'sent' | 'rate-limited' | 'disabled' | 'error' | 'network-error'
if (outcome.kind === 'sent') setSent(true);
}
async function submit(email: string, code: string) {
const outcome = await verifyEmailCodeSignIn({ email, code });
if (outcome.kind === 'success') signIn(outcome.tokens);
if (outcome.kind === 'mfa-required') {
// An emailed code is a FIRST factor. Complete the challenge exactly as
// you would after a password sign-in — see the MFA guide.
}
}
}

Two contract details your UI has to respect:

  • sent says nothing about whether the account exists. The server answers identically for a known and an unknown address so the endpoint cannot be used to enumerate your users, so there is no outcome to branch on. Show “if an account exists, we sent a code”.
  • Handle mfa-required. Skipping that arm means an enrolled user’s sign-in silently stops after they enter a correct code.

Every rejection at the verify step — wrong code, expired, already used, too many attempts — comes back as the single invalid-code outcome. That is deliberate: the server does not distinguish them on the wire either.

By default these hooks call useAuth().signIn(tokens) for you. To control it (for example, “register but don’t log in yet”), opt out and apply the tokens when you choose:

const { state, verifyCode } = useSignUp({ autoSignIn: false });
const { signIn } = useAuth();
const next = await verifyCode(code);
if (next.status === 'success') signIn(next.tokens);