# Profile and contact details

Edit the current account's name and picture, with save and failure states.

Import: @slices.sh/auth/headless

Public parts: 51

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/auth@^0.1.10
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/auth.md

## Anatomy

### Composition

```text
User.Root
  User.SignedIn
    Settings.Frame
      Layout.Wide
        User.Profile.Form
          Settings.Back
          User.Profile.ImageUpload
            User.Image
            User.Initials
            User.Profile.Picture
            User.Profile.RemoveImage
          User.Profile.Label
          User.Name
          User.Profile.Name
          User.Profile.Save
          User.Email
          User.Phone
          User.Profile.Saved
          User.Profile.Error
      Layout.Narrow
        User.Profile.Form
          Settings.Back
          User.Profile.ImageUpload
            User.Image
            User.Initials
            User.Profile.Picture
            User.Profile.RemoveImage
          User.Profile.Label
          User.Name
          User.Profile.Name
          User.Profile.Save
          User.Email
          User.Phone
          User.Profile.Saved
          User.Profile.Error
    User.ChangeEmail.Root
      User.ChangeEmail.Editing
        User.ChangeEmail.Form
          User.ChangeEmail.Edit
          User.ChangeEmail.Label
          User.ChangeEmail.Input
          User.ChangeEmail.Submit
      User.ChangeEmail.Pending
        User.ChangeEmail.Destination
        User.ChangeEmail.Code
        User.ChangeEmail.Verify
        User.ChangeEmail.Error
      User.ChangeEmail.Changed
    User.ChangePhone.Root
      User.ChangePhone.Editing
        User.ChangePhone.Form
          User.ChangePhone.Edit
          User.ChangePhone.PhoneRoot
            User.ChangePhone.Label
            User.ChangePhone.Input
          User.ChangePhone.Submit
      User.ChangePhone.Pending
        User.ChangePhone.Destination
        User.ChangePhone.Code
        User.ChangePhone.Verify
        User.ChangePhone.Error
      User.ChangePhone.Changed
  User.Loading
  User.ReadError
    User.Retry
  User.SessionExpired
```

## Minimal composition

```tsx
"use client"

import "@/services"
import { User } from "@slices.sh/auth/headless"

export function Example() {
  return (
    <User.Root>
      <User.Profile.Form>
        <User.Profile.Label field="name">Name</User.Profile.Label>
        <User.Profile.Name />
        <User.Profile.Save>Save profile</User.Profile.Save>
        <User.Profile.Saved>Saved.</User.Profile.Saved>
        <User.Profile.Error />
      </User.Profile.Form>
    </User.Root>
  )
}

```

Requires an authenticated account. Organization controls also require organizations and the appropriate role.

## Change email

```tsx
"use client"

import "@/services"
import { User } from "@slices.sh/auth/headless"

export function Example() {
  return (
    <User.Root>
      <User.ChangeEmail.Root>
        <User.ChangeEmail.Edit>Change email</User.ChangeEmail.Edit>
        <User.ChangeEmail.Editing>
          <User.ChangeEmail.Form>
            <User.ChangeEmail.Label>New address</User.ChangeEmail.Label>
            <User.ChangeEmail.Input />
            <User.ChangeEmail.Submit>Send code</User.ChangeEmail.Submit>
          </User.ChangeEmail.Form>
        </User.ChangeEmail.Editing>
        <User.ChangeEmail.Pending>
          <User.ChangeEmail.Code />
          <User.ChangeEmail.Verify>Verify</User.ChangeEmail.Verify>
        </User.ChangeEmail.Pending>
        <User.ChangeEmail.Changed>Updated.</User.ChangeEmail.Changed>
        <User.ChangeEmail.Error />
      </User.ChangeEmail.Root>
    </User.Root>
  )
}

```

Requires an authenticated account. Organization controls also require organizations and the appropriate role.

## Change phone

```tsx
"use client"

import "@/services"
import { User } from "@slices.sh/auth/headless"

export function Example() {
  return (
    <User.Root>
      <User.ChangePhone.Root>
        <User.ChangePhone.Edit>Change phone</User.ChangePhone.Edit>
        <User.ChangePhone.Editing>
          <User.ChangePhone.Form>
            <User.ChangePhone.Label>New address</User.ChangePhone.Label>
            <User.ChangePhone.Input />
            <User.ChangePhone.Submit>Send code</User.ChangePhone.Submit>
          </User.ChangePhone.Form>
        </User.ChangePhone.Editing>
        <User.ChangePhone.Pending>
          <User.ChangePhone.Code />
          <User.ChangePhone.Verify>Verify</User.ChangePhone.Verify>
        </User.ChangePhone.Pending>
        <User.ChangePhone.Changed>Updated.</User.ChangePhone.Changed>
        <User.ChangePhone.Error />
      </User.ChangePhone.Root>
    </User.Root>
  )
}

```

Requires an authenticated account. Organization controls also require organizations and the appropriate role.

## Responsive layout

```tsx
"use client"

import { Layout } from "@slices.sh/auth/headless"

export function Example() {
  return (
    <>
      <Layout.Narrow>
        <p>Compact account menu</p>
      </Layout.Narrow>
      <Layout.Wide>
        <p>Full account navigation</p>
      </Layout.Wide>
    </>
  )
}

```

These gates follow the viewport width. They require no authentication service.

## Complete API reference

### User.Root

scope

Says which auth the User parts below read, and whether they may do without it. The parts need
no root: left out, they read the nearest auth root's config, or "default", and render nothing
without auth. `<User.Root enabled="optional">` is for the one who wants that said in the code.

Import: @slices.sh/auth/headless

```tsx
RootProps & AuthRootProps
```

Alternate import: User.Root from @slices.sh/auth/headless/user

Alternate import: User.Root from @slices.sh/auth/headless/passkeys

Alternate import: User.Root from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/root.tsx

### User.SignedIn

gate

SignedIn and SignedOut open once the session has answered, so nobody flashes signed out. Loading covers the first load only. SessionExpired opens when the session on screen has run out while its name and picture are still shown. ReadError is <p role="alert"> with { message } when the session could not be read, and Retry reads it again.

Import: @slices.sh/auth/headless

```tsx
GateProps
```

Alternate import: User.SignedIn from @slices.sh/auth/headless/user

Alternate import: User.SignedIn from @slices.sh/auth/headless/passkeys

Alternate import: User.SignedIn from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/signed-in.tsx

### User.SessionExpired

gate

The session on screen has run out.

Shown while the avatar and the name are still there, because they are: the read that
loaded them succeeded, and what changed is the clock. Everything offered from here is
going to come back `SIGNED_OUT`, so it is worth saying so before somebody tries.

Import: @slices.sh/auth/headless

```tsx
GateProps
```

Alternate import: User.SessionExpired from @slices.sh/auth/headless/user

Alternate import: User.SessionExpired from @slices.sh/auth/headless/passkeys

Alternate import: User.SessionExpired from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/session-expired.tsx

### User.Loading

gate

SignedIn and SignedOut open once the session has answered, so nobody flashes signed out. Loading covers the first load only. SessionExpired opens when the session on screen has run out while its name and picture are still shown. ReadError is <p role="alert"> with { message } when the session could not be read, and Retry reads it again.

Import: @slices.sh/auth/headless

```tsx
GateProps
```

Alternate import: User.Loading from @slices.sh/auth/headless/user

Alternate import: User.Loading from @slices.sh/auth/headless/passkeys

Alternate import: User.Loading from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/loading.tsx

### User.ReadError

content · Element: p

SignedIn and SignedOut open once the session has answered, so nobody flashes signed out. Loading covers the first load only. SessionExpired opens when the session on screen has run out while its name and picture are still shown. ReadError is <p role="alert"> with { message } when the session could not be read, and Retry reads it again.

Import: @slices.sh/auth/headless

```tsx
PartProps<"p", { message: string }>
```

Alternate import: User.ReadError from @slices.sh/auth/headless/user

Alternate import: User.ReadError from @slices.sh/auth/headless/passkeys

Alternate import: User.ReadError from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/read-error.tsx

### User.Retry

action · Element: button

SignedIn and SignedOut open once the session has answered, so nobody flashes signed out. Loading covers the first load only. SessionExpired opens when the session on screen has run out while its name and picture are still shown. ReadError is <p role="alert"> with { message } when the session could not be read, and Retry reads it again.

Import: @slices.sh/auth/headless

```tsx
PartProps<"button">
```

Alternate import: User.Retry from @slices.sh/auth/headless/user

Alternate import: User.Retry from @slices.sh/auth/headless/passkeys

Alternate import: User.Retry from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/retry.tsx

### User.Image

content · Element: img

Image is <img alt=""> with src locked to the stored image, else Gravatar, and nothing when neither exists or it fails to load. Initials are two letters from the name, else the address. Name and Phone render nothing when absent. Email is the address.

Import: @slices.sh/auth/headless

```tsx
PartProps<"img">
```

Alternate import: User.Image from @slices.sh/auth/headless/user

Alternate import: User.Image from @slices.sh/auth/headless/passkeys

Alternate import: User.Image from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/image.tsx

### User.Initials

content

Image is <img alt=""> with src locked to the stored image, else Gravatar, and nothing when neither exists or it fails to load. Initials are two letters from the name, else the address. Name and Phone render nothing when absent. Email is the address.

Import: @slices.sh/auth/headless

```tsx
PartProps<null>
```

Alternate import: User.Initials from @slices.sh/auth/headless/user

Alternate import: User.Initials from @slices.sh/auth/headless/passkeys

Alternate import: User.Initials from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/initials.tsx

### User.Name

content

Image is <img alt=""> with src locked to the stored image, else Gravatar, and nothing when neither exists or it fails to load. Initials are two letters from the name, else the address. Name and Phone render nothing when absent. Email is the address.

Import: @slices.sh/auth/headless

```tsx
PartProps<null>
```

Alternate import: User.Name from @slices.sh/auth/headless/user

Alternate import: User.Name from @slices.sh/auth/headless/passkeys

Alternate import: User.Name from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/name.tsx

### User.Email

content

Image is <img alt=""> with src locked to the stored image, else Gravatar, and nothing when neither exists or it fails to load. Initials are two letters from the name, else the address. Name and Phone render nothing when absent. Email is the address.

Import: @slices.sh/auth/headless

```tsx
PartProps<null>
```

Alternate import: User.Email from @slices.sh/auth/headless/user

Alternate import: User.Email from @slices.sh/auth/headless/passkeys

Alternate import: User.Email from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/email.tsx

### User.Phone

content

Image is <img alt=""> with src locked to the stored image, else Gravatar, and nothing when neither exists or it fails to load. Initials are two letters from the name, else the address. Name and Phone render nothing when absent. Email is the address.

Import: @slices.sh/auth/headless

```tsx
PartProps<null>
```

Alternate import: User.Phone from @slices.sh/auth/headless/user

Alternate import: User.Phone from @slices.sh/auth/headless/passkeys

Alternate import: User.Phone from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/user-phone.tsx

### User.Profile.Form

action · Element: form

Form holds the fields, prevents the page submit and gives { pending }; nothing when the backend changes neither field. Label takes field="name" | "image" and renders nothing when its field is hidden. Name starts at the stored name, with { invalid }.

Import: @slices.sh/auth/headless

Capability: profile: name

```tsx
PartProps<"form", { pending: boolean }>
```

Alternate import: User.Profile.Form from @slices.sh/auth/headless/user

Alternate import: User.Profile.Form from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Form from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/form.tsx

### User.Profile.Label

content · Element: label

Form holds the fields, prevents the page submit and gives { pending }; nothing when the backend changes neither field. Label takes field="name" | "image" and renders nothing when its field is hidden. Name starts at the stored name, with { invalid }.

Import: @slices.sh/auth/headless

Capability: profile: name

User.Profile.Label must be inside <User.Profile.Form>.

```tsx
PartProps<"label"> & { field: ProfileField & keyof typeof FIELDS }
```

Alternate import: User.Profile.Label from @slices.sh/auth/headless/user

Alternate import: User.Profile.Label from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Label from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/label.tsx

### User.Profile.Name

input · Element: input

Form holds the fields, prevents the page submit and gives { pending }; nothing when the backend changes neither field. Label takes field="name" | "image" and renders nothing when its field is hidden. Name starts at the stored name, with { invalid }.

Import: @slices.sh/auth/headless

Capability: profile: name

User.Profile.Name must be inside <User.Profile.Form>.

```tsx
PartProps<"input", { invalid: boolean }>
```

Alternate import: User.Profile.Name from @slices.sh/auth/headless/user

Alternate import: User.Profile.Name from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Name from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/name.tsx

### User.Profile.ImageUpload

content

`save` stages a legacy upload; `change` persists a cropped portrait independently of other draft fields.

Import: @slices.sh/auth/headless

Capability: profile: image

User.Profile.ImageUpload must be inside <User.Profile.Form>.

```tsx
PartProps<
    null,
    { save(uploaded: { permalink: string }): void; value: string; change(url: string): void }
  >
```

Alternate import: User.Profile.ImageUpload from @slices.sh/auth/headless/user

Alternate import: User.Profile.ImageUpload from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.ImageUpload from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/image-upload.tsx

### User.Profile.Picture

content · Element: img

ImageUpload is auth's half of an avatar upload: its function child gets { save }, which keeps the permalink an Avatar.Root or Upload.Root reports. Auth stores no bytes. Picture shows what a save would leave: the edit, else the stored picture, else Gravatar. RemoveImage renders nothing while there is no picture.

Import: @slices.sh/auth/headless

Capability: profile: image

User.Profile.Picture must be inside <User.Profile.Form>.

```tsx
PartProps<"img">
```

Alternate import: User.Profile.Picture from @slices.sh/auth/headless/user

Alternate import: User.Profile.Picture from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Picture from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/picture.tsx

### User.Profile.RemoveImage

action · Element: button

ImageUpload is auth's half of an avatar upload: its function child gets { save }, which keeps the permalink an Avatar.Root or Upload.Root reports. Auth stores no bytes. Picture shows what a save would leave: the edit, else the stored picture, else Gravatar. RemoveImage renders nothing while there is no picture.

Import: @slices.sh/auth/headless

Capability: profile: image

User.Profile.RemoveImage must be inside <User.Profile.Form>.

```tsx
PartProps<"button">
```

Alternate import: User.Profile.RemoveImage from @slices.sh/auth/headless/user

Alternate import: User.Profile.RemoveImage from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.RemoveImage from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/remove-image.tsx

### User.Profile.Save

action · Element: button

Save is the submit with { pending }, disabled while nothing changed. Saved opens after a save until a field changes. Error is <div role="alert"> with { code, message }.

Import: @slices.sh/auth/headless

User.Profile.Save must be inside <User.Profile.Form>.

```tsx
PartProps<"button", { pending: boolean }>
```

Alternate import: User.Profile.Save from @slices.sh/auth/headless/user

Alternate import: User.Profile.Save from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Save from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/save.tsx

### User.Profile.Saved

gate

Save is the submit with { pending }, disabled while nothing changed. Saved opens after a save until a field changes. Error is <div role="alert"> with { code, message }.

Import: @slices.sh/auth/headless

User.Profile.Saved must be inside <User.Profile.Form>.

```tsx
GateProps
```

Alternate import: User.Profile.Saved from @slices.sh/auth/headless/user

Alternate import: User.Profile.Saved from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Saved from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/saved.tsx

### User.Profile.Error

content · Element: div

Save is the submit with { pending }, disabled while nothing changed. Saved opens after a save until a field changes. Error is <div role="alert"> with { code, message }.

Import: @slices.sh/auth/headless

```tsx
PartProps<"div", { code: AuthErrorCode; message: string }>
```

Alternate import: User.Profile.Error from @slices.sh/auth/headless/user

Alternate import: User.Profile.Error from @slices.sh/auth/headless/passkeys

Alternate import: User.Profile.Error from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/profile/error.tsx

### User.ChangeEmail.Root

scope

The section, only where the backend can change an address.

Every `ChangeEmail` part already renders nothing without the capability, but the heading
and the hint around them are pattern code, not parts — so on a backend that cannot do it
an installed `email=1` drew "Email address" over an empty space. A gate around the whole
section is what `Passkeys.Root` does for the same reason.

It also holds whether the person has asked to change their address: until they do, there
is a sentence and nothing to fill in, which is what `Edit` and `Editing` divide.

Import: @slices.sh/auth/headless

```tsx
RootProps & AuthRootProps
```

Alternate import: User.ChangeEmail.Root from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Root from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Root from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/root.tsx

### User.ChangeEmail.Edit

action · Element: button

What opens the form: the address itself, as something to press.

The address was drawn as prose with a form already under it, so the one thing anybody
tries — pressing their own address — did nothing at all. It is a button now, and the form
it opens stands in its place.

Import: @slices.sh/auth/headless

User.ChangeEmail.Edit must be inside <User.ChangeEmail.Root>.

```tsx
PartProps<"button">
```

Alternate import: User.ChangeEmail.Edit from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Edit from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Edit from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/edit.tsx

### User.ChangeEmail.Editing

gate

Edit is the address as a button: pressing it opens the form and it stands aside. Editing gates the form once Edit was pressed.

Import: @slices.sh/auth/headless

User.ChangeEmail.Editing must be inside <User.ChangeEmail.Root>.

```tsx
GateProps
```

Alternate import: User.ChangeEmail.Editing from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Editing from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Editing from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/editing.tsx

### User.ChangeEmail.Form

action · Element: form

Form holds the address and the code with { pending }. Label locks htmlFor. Input is <input type="email"> with { invalid } once the backend refuses it. Submit is disabled until the address is valid and differs from the current one.

Import: @slices.sh/auth/headless

```tsx
PartProps<"form", { pending: boolean }>
```

Alternate import: User.ChangeEmail.Form from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Form from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Form from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/form.tsx

### User.ChangeEmail.Label

content · Element: label

Form holds the address and the code with { pending }. Label locks htmlFor. Input is <input type="email"> with { invalid } once the backend refuses it. Submit is disabled until the address is valid and differs from the current one.

Import: @slices.sh/auth/headless

User.ChangeEmail.Label must be inside <User.ChangeEmail.Form>.

```tsx
PartProps<"label">
```

Alternate import: User.ChangeEmail.Label from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Label from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Label from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/label.tsx

### User.ChangeEmail.Input

input · Element: input

Form holds the address and the code with { pending }. Label locks htmlFor. Input is <input type="email"> with { invalid } once the backend refuses it. Submit is disabled until the address is valid and differs from the current one.

Import: @slices.sh/auth/headless

User.ChangeEmail.Input must be inside <User.ChangeEmail.Form>.

```tsx
PartProps<"input", { invalid: boolean }>
```

Alternate import: User.ChangeEmail.Input from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Input from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Input from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/input.tsx

### User.ChangeEmail.Submit

action · Element: button

Form holds the address and the code with { pending }. Label locks htmlFor. Input is <input type="email"> with { invalid } once the backend refuses it. Submit is disabled until the address is valid and differs from the current one.

Import: @slices.sh/auth/headless

User.ChangeEmail.Submit must be inside <User.ChangeEmail.Form>.

```tsx
PartProps<"button", { pending: boolean }>
```

Alternate import: User.ChangeEmail.Submit from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Submit from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Submit from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/submit.tsx

### User.ChangeEmail.Pending

gate

While the new address has a code outstanding.

The address is not theirs until they prove it, and the only thing that can prove it is
something delivered to it — so the code goes to the new address, and this is the step
where it is entered.

Import: @slices.sh/auth/headless

User.ChangeEmail.Pending must be inside <User.ChangeEmail.Form>.

```tsx
GateProps<{ email: string }>
```

Alternate import: User.ChangeEmail.Pending from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Pending from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Pending from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/pending.tsx

### User.ChangeEmail.Destination

content

The address the code went to, which is the new one rather than the one on the account.

Import: @slices.sh/auth/headless

User.ChangeEmail.Destination must be inside <User.ChangeEmail.Form>.

```tsx
PartProps<null>
```

Alternate import: User.ChangeEmail.Destination from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Destination from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Destination from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/destination.tsx

### User.ChangeEmail.Code

input · Element: input

Pending opens with { email } while a code is outstanding. Destination is the new address. Code is a numeric one-time-code input that checks itself on the last digit, read-only while it does. Verify does the same for a pasted code.

Import: @slices.sh/auth/headless

User.ChangeEmail.Code must be inside <User.ChangeEmail.Form>.

```tsx
PartProps<"input", { invalid: boolean }>
```

Alternate import: User.ChangeEmail.Code from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Code from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Code from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/code.tsx

### User.ChangeEmail.Verify

action · Element: button

Spending the code, for anyone who does not reach its last digit by typing.

The field checks itself on that digit, so this is the same deed rather than another one —
for a pasted code, for a code whose last digit was corrected, and for anybody who wants a
button to press.

Import: @slices.sh/auth/headless

User.ChangeEmail.Verify must be inside <User.ChangeEmail.Form>.

```tsx
PartProps<"button", { pending: boolean }>
```

Alternate import: User.ChangeEmail.Verify from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Verify from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Verify from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/verify.tsx

### User.ChangeEmail.Changed

gate

Changed opens with { email } once the address is the account's. Error is <div role="alert"> with { code, message } for whichever failed last.

Import: @slices.sh/auth/headless

User.ChangeEmail.Changed must be inside <User.ChangeEmail.Form>.

```tsx
GateProps<{ email: string }>
```

Alternate import: User.ChangeEmail.Changed from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Changed from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Changed from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/changed.tsx

### User.ChangeEmail.Error

content · Element: div

Changed opens with { email } once the address is the account's. Error is <div role="alert"> with { code, message } for whichever failed last.

Import: @slices.sh/auth/headless

```tsx
PartProps<"div", { code: AuthErrorCode; message: string }>
```

Alternate import: User.ChangeEmail.Error from @slices.sh/auth/headless/user

Alternate import: User.ChangeEmail.Error from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangeEmail.Error from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-email/error.tsx

### User.ChangePhone.Root

scope

The section, only where the backend can text a code to a new number.

The heading around the parts is pattern code, not a part, so on a backend that cannot do
it an installed `phone=1` would draw a title over an empty space: a gate around the whole
section is what `ChangeEmail.Root` does for the same reason. It also holds whether the
person has asked to change their number, which `Edit` and `Editing` divide.

Import: @slices.sh/auth/headless

```tsx
RootProps & AuthRootProps
```

Alternate import: User.ChangePhone.Root from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Root from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Root from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/root.tsx

### User.ChangePhone.Edit

action · Element: button

What opens the form: the number itself, as something to press. The children get the
number on the account, or `null` for an account that has none yet, which wants another
sentence than "change".

Import: @slices.sh/auth/headless

User.ChangePhone.Edit must be inside <User.ChangePhone.Root>.

```tsx
PartProps<"button", { phoneNumber: string | null }>
```

Alternate import: User.ChangePhone.Edit from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Edit from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Edit from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/edit.tsx

### User.ChangePhone.Editing

gate

Edit is the number as a button, with { phoneNumber }, which is null for an account without one, so it can say add. Editing gates the form.

Import: @slices.sh/auth/headless

User.ChangePhone.Editing must be inside <User.ChangePhone.Root>.

```tsx
GateProps
```

Alternate import: User.ChangePhone.Editing from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Editing from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Editing from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/editing.tsx

### User.ChangePhone.Form

action · Element: form

Form holds the number and the code. PhoneRoot is Phone.Root over the form's field, taking defaultCountry, allowedCountries and locale; Phone.Country parts and Phone.Blocked go inside it. Label locks htmlFor to Input, which is Phone.Input with { invalid, status }. Submit is disabled until the number is valid, allowed and new.

Import: @slices.sh/auth/headless

```tsx
PartProps<"form", { pending: boolean }>
```

Alternate import: User.ChangePhone.Form from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Form from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Form from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/form.tsx

### User.ChangePhone.PhoneRoot

scope

The new number and its country list, kept in the form's field as an E.164 number or as
nothing at all. Renders nothing itself.

The form's field keeps the id, so `ChangePhone.Label` finds the number's input without
knowing a country list is there.

Import: @slices.sh/auth/headless

User.ChangePhone.PhoneRoot must be inside <User.ChangePhone.Form>.

```tsx
PhoneRootProps

type PhoneRootProps = RootProps & {
  /** Where a number typed without a `+` is read until a country is picked. ISO 3166-1 alpha-2. */
  defaultCountry?: string
  /** The countries a number may be in, which are also the ones the list offers. Without it, every country. */
  allowedCountries?: readonly string[]
  locale?: string
}
```

Alternate import: User.ChangePhone.PhoneRoot from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.PhoneRoot from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.PhoneRoot from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/phone-root.tsx

### User.ChangePhone.Label

content · Element: label

Form holds the number and the code. PhoneRoot is Phone.Root over the form's field, taking defaultCountry, allowedCountries and locale; Phone.Country parts and Phone.Blocked go inside it. Label locks htmlFor to Input, which is Phone.Input with { invalid, status }. Submit is disabled until the number is valid, allowed and new.

Import: @slices.sh/auth/headless

User.ChangePhone.Label must be inside <User.ChangePhone.Form>.

```tsx
PartProps<"label">
```

Alternate import: User.ChangePhone.Label from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Label from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Label from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/label.tsx

### User.ChangePhone.Input

input · Element: input

The new number, typed in its country's own format. It sits inside `ChangePhone.PhoneRoot`,
which is where the country list is too, and says "invalid" for a refusal the number's own
reading cannot know about: a number that is taken, or a country nobody may text.

Import: @slices.sh/auth/headless

User.ChangePhone.Input must be inside <User.ChangePhone.Form>.

```tsx
PartProps<"input", { invalid: boolean; status: PhoneStatus }> & { invalidMessage?: string }
```

Alternate import: User.ChangePhone.Input from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Input from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Input from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/input.tsx

### User.ChangePhone.Submit

action · Element: button

Form holds the number and the code. PhoneRoot is Phone.Root over the form's field, taking defaultCountry, allowedCountries and locale; Phone.Country parts and Phone.Blocked go inside it. Label locks htmlFor to Input, which is Phone.Input with { invalid, status }. Submit is disabled until the number is valid, allowed and new.

Import: @slices.sh/auth/headless

User.ChangePhone.Submit must be inside <User.ChangePhone.Form>.

```tsx
PartProps<"button", { pending: boolean }>
```

Alternate import: User.ChangePhone.Submit from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Submit from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Submit from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/submit.tsx

### User.ChangePhone.Pending

gate

While the new number has a code outstanding.

The number is not theirs until they prove it, and the only thing that can prove it is
something delivered to it: the code goes to the new number, and this is the step where it
is entered.

Import: @slices.sh/auth/headless

User.ChangePhone.Pending must be inside <User.ChangePhone.Form>.

```tsx
GateProps<{ phoneNumber: string }>
```

Alternate import: User.ChangePhone.Pending from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Pending from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Pending from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/pending.tsx

### User.ChangePhone.Destination

content

The number the code went to, which is the new one rather than the one on the account.

Import: @slices.sh/auth/headless

User.ChangePhone.Destination must be inside <User.ChangePhone.Form>.

```tsx
PartProps<null>
```

Alternate import: User.ChangePhone.Destination from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Destination from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Destination from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/destination.tsx

### User.ChangePhone.Code

input · Element: input

Pending opens with { phoneNumber } while a code is outstanding. Destination, Code and Verify behave as in ChangeEmail.

Import: @slices.sh/auth/headless

User.ChangePhone.Code must be inside <User.ChangePhone.Form>.

```tsx
PartProps<"input", { invalid: boolean }>
```

Alternate import: User.ChangePhone.Code from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Code from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Code from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/code.tsx

### User.ChangePhone.Verify

action · Element: button

Spending the code, for anyone who does not reach its last digit by typing: a pasted code,
a code whose last digit was corrected, and anybody who wants a button to press.

Import: @slices.sh/auth/headless

User.ChangePhone.Verify must be inside <User.ChangePhone.Form>.

```tsx
PartProps<"button", { pending: boolean }>
```

Alternate import: User.ChangePhone.Verify from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Verify from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Verify from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/verify.tsx

### User.ChangePhone.Changed

gate

Changed opens with { phoneNumber }. Error reports whichever failed last.

Import: @slices.sh/auth/headless

User.ChangePhone.Changed must be inside <User.ChangePhone.Form>.

```tsx
GateProps<{ phoneNumber: string }>
```

Alternate import: User.ChangePhone.Changed from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Changed from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Changed from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/changed.tsx

### User.ChangePhone.Error

content · Element: div

Changed opens with { phoneNumber }. Error reports whichever failed last.

Import: @slices.sh/auth/headless

```tsx
PartProps<"div", { code: AuthErrorCode; message: string }>
```

Alternate import: User.ChangePhone.Error from @slices.sh/auth/headless/user

Alternate import: User.ChangePhone.Error from @slices.sh/auth/headless/passkeys

Alternate import: User.ChangePhone.Error from @slices.sh/auth/headless/two-factor

Source: @slices.sh/auth/src/headless/user/change-phone/error.tsx

### Settings.Frame

content

Frame maps onto render={<Tabs />} and sets data-view="list" | "section" for a narrow panel. Back returns from the open section to the list; draw it only where the panel is too narrow for both.

Import: @slices.sh/auth/headless

```tsx
SettingsFrameProps

export type SettingsFrameProps = PartProps<null> &
  SettingsNavigationOptions & {
    sections: readonly string[]
    orientation?: "horizontal" | "vertical"
    ref?: Ref<HTMLElement>
  }
```

Alternate import: Settings.Frame from @slices.sh/auth/headless/settings

Source: @slices.sh/auth/src/headless/settings/frame.tsx

### Settings.Back

action · Element: button

Back to the list of sections, on a panel too narrow to show it beside the open one.

It is drawn at every width: whether there is room for both is the layout's to say, from
the panel's own width, so the button's styles hide it where the list is already in view.

Import: @slices.sh/auth/headless

Settings.Back must be inside <Settings.Frame>.

```tsx
PartProps<"button">
```

Alternate import: Settings.Back from @slices.sh/auth/headless/settings

Source: @slices.sh/auth/src/headless/settings/back.tsx

### Layout.Narrow

gate · Generic

Narrow renders on a phone-shaped window, Wide otherwise. Exactly one renders.

Import: @slices.sh/auth/headless

```tsx
GateProps
```

Alternate import: Layout.Narrow from @slices.sh/auth/headless/layout

Source: @slices.sh/auth/src/headless/layout/narrow.tsx

### Layout.Wide

gate · Generic

Narrow renders on a phone-shaped window, Wide otherwise. Exactly one renders.

Import: @slices.sh/auth/headless

```tsx
GateProps
```

Alternate import: Layout.Wide from @slices.sh/auth/headless/layout

Source: @slices.sh/auth/src/headless/layout/wide.tsx
