Components
ElvixEmailCodeSignIn

ElvixEmailCodeSignIn

A compact sign-in that starts at the email code, for an address you already have (a wallet payment, an invite). It sends the code once when it mounts and draws only where the code went, the six boxes, Send again and Use another email, inside your own card; onSignedIn hands you the session.

Your ticket is ready

Sign in to keep it in your account.

Sending a code to [email protected]…

Any six digits sign in; 000000 shows the wrong-code message.

components/ticket-ready.tsx
"use client";
import { ElvixEmailCodeSignIn } from "@elvix.is/sdk/react";
// Inside <ElvixProvider clientId="your-app">, in your own card, right after
// the step that told you the address (a wallet payment, an invite).
export function TicketReady({ email, onSignedIn }: Props) {
return (
<ElvixEmailCodeSignIn
email={email}
title="Your ticket is ready" // optional, like the line below
description="Sign in to keep it in your account."
deferOnboarding // the ticket first; username and passkey next time
onSignedIn={(session) => onSignedIn(session.token)} // verify it on your backend
signedIn={<p>Your ticket is in your account.</p>} // optional
/>
);
}
type Props = { email: string; onSignedIn: (token?: string) => void };

When to use it

Your app already knows the address: a person paid with Apple Pay or Google Pay while signed out and the wallet handed it over, or they followed an invite. Right after that step you want them signed in with one move, the code in their inbox, in a block short enough to sit under other content. <ElvixSignInForm> is the whole front door (every method, the header, the footer); this is the one step that is left.

Sign-in is sign-up, as everywhere in elvix: an address nobody signed in with yet creates the account.

Props

PropTypeRequiredDefaultDescription
emailstringrequirednoneWhere the code goes. Sent once when the block mounts. A new value starts over with its own code. An address that is not one opens on the email field instead.
onSignedIn(session: ElvixSignInResultOk) => voidoptionalnoneThe person is signed in: { ok: true, method: "email_otp", token, redirect, deferred? }. The provider has already refreshed, so useElvixSession() is authenticated. Show the next thing here. Fires once.
signedInReactNodeoptionalnothingWhat the block shows once signed in, in place of everything else (its title and line too).
titleReactNodeoptionalhiddenA heading above the block. A string renders as an <h3>; any element renders as given. Left out, nothing is drawn and no space is kept.
descriptionReactNodeoptionalhiddenA line under the title, the same way (a string renders as a <p>).
deferOnboardingbooleanoptionalfalseSign in at once when the app would offer a username or a passkey; session.deferred names the step put off ("username" or "passkey"), offered again at the next sign-in. Restoring a membership is never put off.
autoFocusbooleanoptionaltrueFocus the code boxes (or the email field) when they appear, so a phone shows its keyboard and the code suggestion.
onResult(r: ElvixSignInResult) => voidoptionalnoneAs on <ElvixSignInForm>: the success above, and every failure with its code, { ok: false, error, message }.
onAuthenticated(r: { ok: true; redirect: string; token?: string }) => voidoptionalnoneThe form's short success shape. Prefer onSignedIn.
clientIdstringoptional<ElvixProvider clientId>Your app's public clientId.
mode"interactive" | "preview"optional"interactive""preview" draws the block with email and demo state, and asks elvix nothing: no request, no email.
previewStep"code" | "email" | "username" | "passkey" | "recover"optional"code"Preview only: which pane to draw.
theme"light" | "dark" | "auto"optionalprovider, then ConsolePins the palette; "auto" inherits the page.
classNamestringoptionalnoneOn the block's root. It has no frame, padding or background of its own.
classNames{ root?, header?, title?, description?, line?, code?, field?, button?, links?, link?, error? }optionalnoneMore classes on each part: spacing, type, colour. The fields' fill, outline and radius are the variables below.

What it does

  • One code per address. The code goes out when the block mounts, and never again for a remount, React StrictMode's double effect or a reload while the code is still good (it is kept in sessionStorage for nine of its ten minutes). Send again is the only way to a second one, after the same cooldown as the form.
  • Use another email turns the block into an email field and Continue, in the same frame; the code goes to that address. Back returns to the code already sent. A wallet can hand over an address the person does not read, so this way out is always there.
  • The code verifies at the sixth digit, typed, pasted or filled in by the phone's one-time-code suggestion. The boxes are one labelled group, described by the line that says where the code went, and that line and every error are announced.
  • Onboarding happens in the block, with the sign-in form's own panes: pick a username, add a passkey, restore a membership. deferOnboarding signs in at once instead of offering the first two.
  • It never navigates. onSignedIn is where you do what comes next; the block then shows signedIn, or nothing.
  • DPoP works as in the form: with dpopEnabled on, the session binds to this browser's key the moment it is created.

Errors

Every failure is shown in the block with its cause and what to do, and reported to onResult with its code.

errorShown in the blockWhat the person does
invalid_codeThat code didn't work. Try again.Types it again.
expiredThat code expired. Send a new one.Send again.
too_many_attemptsToo many wrong codes. Send a new one.Send again.
already_usedThat code was already used. Send a new one.Send again.
too_recentWait Ns before requesting another code.Send again counts down.
too_manyToo many codes sent to this email. Try again in N.Waits, or uses another email.
network_errorNetwork hiccup. Try again.Send again, or types the code again.
user_banned, user_paused, gate_*The same words as the sign-in form.Contacts the app.

Preview mode

To show the flow to someone who is not signing in (an organizer previewing what a buyer sees), draw it in preview mode. It hits no endpoint and sends no email.

preview.tsx
// What the buyer will see, drawn for someone who is not buying: no request,
// no email, the given address and a filled-in code.
<ElvixEmailCodeSignIn mode="preview" email="[email protected]" />
// Any pane: "code" (default), "email", "username", "passkey", "recover".
<ElvixEmailCodeSignIn mode="preview" previewStep="username" email="[email protected]" />

Look

No card, logo, method list or footer: your card is the frame, with its own padding; the block is one column with one gap, so a title or line left out leaves no space behind. Colours follow the provider and the Console like the form. The fields are a tint of the text colour with no outline, and a thin brand border when focused. Every part of that look is a variable, set on any ancestor; the fields are plain CSS, so no stylesheet order can override them. Words come from the provider's messages (signin.codeSentSubtitle, signin.sendAgain, signin.useAnotherEmail and the rest), like every SDK string.

app.css
/* On any ancestor of the block; every one is optional. */
.ticket-card {
--elvix-field: var(--muted); /* fill */
--elvix-field-hover: var(--accent); /* fill under the pointer */
--elvix-field-border: transparent; /* outline at rest */
--elvix-field-focus: var(--ring); /* outline when focused */
--elvix-field-focus-ring: none; /* a ring around a focused field */
--elvix-field-radius: 12px;
--elvix-field-text: var(--foreground);
}

Mount it only for a signed-out person, and only with an address they gave you. The block signs in whoever reads that inbox: it is the email's owner who proves themselves with the code, never the person in front of your page. Requires @elvix.is/sdk 0.17 or later.

Related