# ElvixOnboarding

> What happens after a redirect sign-in. Mount once; it opens itself on the step the server asked for (add a passkey, pick a username, restore a membership), finishes it in place and closes.

<!-- components/providers.tsx -->
```tsx
"use client";

import { ElvixOnboarding, ElvixProvider } from "@elvix.is/sdk/react";
import { useRouter } from "next/navigation";

export function Providers({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  return (
    <ElvixProvider clientId={process.env.NEXT_PUBLIC_ELVIX_CLIENT_ID!}>
      {/* Mount once, high in the tree. Renders nothing until a redirect
          sign-in brings back a step, then opens itself on that step. */}
      <ElvixOnboarding
        steps={["passkey"]}
        onResult={(r) => {
          if (r.ok) router.refresh();
        }}
      />
      {children}
    </ElvixProvider>
  );
}
```

## Why it exists

Google, GitHub and the hosted passkey page send the person away and bring them back to your page. That is a full page load, so the in-frame sign-in is gone. When the server asked for one more step (pick a username, add a passkey, restore a membership), there is nothing on the page to show it. Email-code, username and passkey sign-ins stay on your page and show those steps in the same frame, which is why a person could be offered a passkey after a username sign-in and never after Google.

`<ElvixOnboarding>` closes that gap. It renders nothing until such a return arrives. Then it opens as a modal or drawer on the right pane for the step, lets the person finish it, calls `onResult`, and closes.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `steps` | `("username" \| "passkey" \| "recover")[]` | optional | all three | Which steps this surface offers. `["passkey"]` offers the passkey after a redirect sign-in and nothing else. A step you leave out is not shown and the sign-in simply completes. |
| `presentation` | `"modal" \| "drawer"` | optional | `"modal"` | How it opens. |
| `drawerSide` | `"bottom" \| "left" \| "right"` | optional | `"bottom"` | Drawer anchor edge. |
| `width` | `number \| string` | optional | `420` | Card / drawer width. |
| `dismissable` | `boolean` | optional | `true` | Escape and backdrop close it. Closing means "not now": the sign-in completes without the step. |
| `navigate` | `boolean` | optional | `false` | The person is already on your page, so it does not navigate by default. Set `true` to follow `redirectAfterSignIn`. |
| `redirectAfterSignIn` | `string` | optional | none | Where to go when `navigate` is true. Checked: your origin or an origin registered for the app, else `/`. |
| `onResult` | `(r: ElvixSignInResult) => void` | optional | none | Fires once, at the true end of the step (after the provider has refreshed, so `useElvixSession()` is already authenticated). |
| `onAuthenticated` | `(r: { ok: true; redirect?: string; token?: string }) => void` | optional | none | Same moment, the legacy shape. |

## What the host sees meanwhile

- **`useElvixSession()` stays anonymous until the step is done.** The person is signed in on the elvix side but has not finished signing in. A header that flips to signed in, or a redirect that fires on authenticated, would otherwise tear the form down before the step shows.
- **Finishing refreshes the provider before `onResult` fires**, so `useElvixSession()` and `useElvixUser()` are authenticated inside your callback.
- **Closing it means not now.** The sign-in completes without the step, the host sees the user, and the step is offered again at their next sign-in.
- **A forged link does nothing.** The return is accepted only with the one-time `state` this tab started the sign-in with (see [Redirect-OAuth round-trip](/docs/install)); a link without it opens nothing and signs nobody in.

## Choosing the steps

`steps={["passkey"]}` is the common case: offer a passkey after Google or GitHub, never interrupt for anything else. A step that is not in the list is skipped and the sign-in completes. Leave `"recover"` in unless your own flow handles it: it asks whether to restore a deactivated or left membership, which is a decision rather than an upsell.

> **Note:** The passkey step appears only when the person has no working passkey for this app. A passkey made for another app does not count, and neither does an account-level one that has never signed anyone in. A person who already has one signs in and is not asked.

## A modal sign-in form already does this

`<ElvixSignInForm presentation="modal" | "drawer">` opens itself on a redirect return that carries a step (`resumeOnReturn`, default on), so if your sign-in is already that form you need nothing else. Mount `<ElvixOnboarding>` when your sign-in surface is not the form (a hosted redirect page, your own buttons) or you want the post-redirect step offered everywhere in the app.

## Building your own surface

`useElvixOnboarding()` returns `{ pending, step }`. Open any surface you like while `pending` is true: render `<ElvixSignInForm>` controlled with `resumeOnReturn={false}`, as below. The form finishes the step in place.

<!-- components/my-sign-in-modal.tsx -->
```tsx
"use client";

import { ElvixSignInForm, useElvixOnboarding } from "@elvix.is/sdk/react";

// Your own surface: open it whenever a step is waiting.
export function MySignInModal() {
  const { pending, step } = useElvixOnboarding(); // step: "username" | "passkey" | "recover" | null
  return (
    <ElvixSignInForm
      presentation="modal"
      open={pending}
      resumeOnReturn={false}
      onResult={(r) => r.ok && location.reload()}
    />
  );
}
```

> **Warning:** Do not navigate from a handler that fires when the session turns authenticated while `pending` is true: it cannot, by design, but a redirect on the `elvix:return-token` event can. Let the form finish; `onResult` is the true end. Requires `@elvix.is/sdk` 0.14.3 or later, and an elvix that echoes the `state` (live since 2026-10-12).


## Related

- [ElvixSignInForm](/docs/components/elvix-sign-in-form.md): The full branded sign-in surface (card, header, brand wash, every Console-enabled method, Secured-by-elvix badge). Exported as both `ElvixSignInForm` and the short alias `ElvixSignIn` — same component.
- [ElvixProvider](/docs/components/elvix-provider.md): Top-level context. Fetches the brand chord, factor config, and copy from elvix; pipes session state and the event channel to every nested <Elvix*> component.
- [ElvixAppPasskeys](/docs/components/elvix-app-passkeys.md): Per-app passkey manager. Lists and lets the user add or remove WebAuthn passkeys scoped to ONE application — the credential can only sign them in to this app. Account-level passkeys (added on `/account/security`) work everywhere and are NOT shown here.
