Skip to content
Torii docs

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.

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);