# Step up dialog

A verification step before a sensitive action: the person proves it's them again, by a passkey, a fresh SSO sign-in, or a code, whichever the app provides (each shows only when its callback is given). It says what is waiting on the check, in the app's words, and that cancelling runs nothing. Nothing is chosen for the person and no method is tried on its own: the app runs the ceremony in the callback, reports a refusal through `error`, and closes the dialog when the check passes. Record the pass as `stepup.completed` in the audit log (AuditLog knows the action).

When `error` arrives after a code was submitted, focus moves to the code field, which rings once.

- Registry name: `step-up-dialog` (`@candor/step-up-dialog`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/step-up-dialog

## Install

```bash
npx candor-ui add step-up-dialog
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/step-up-dialog`.
It brings in: button, dialog, input, locale, locked, motion, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { StepUpDialog } from "@/components/candor/step-up-dialog"
import { Button } from "@/components/candor/ui/button"

export default function Example() {
  const [open, setOpen] = React.useState(false)
  const [busy, setBusy] = React.useState(false)
  const [error, setError] = React.useState<string>()
  const [passed, setPassed] = React.useState(false)
  const wait = () => new Promise((r) => setTimeout(r, 600))
  // The app runs each check, says why one failed, and closes the dialog when one passes (and records stepup.completed).
  const pass = () => {
    setOpen(false)
    setPassed(true)
  }
  return (
    <div className="flex flex-col items-start gap-2 text-sm">
      <Button variant="destructive" onClick={() => (setError(undefined), setOpen(true))}>
        Delete the production environment
      </Button>
      {passed && <p className="text-muted-foreground">Checked. The demo would delete it now.</p>}
      <StepUpDialog
        open={open}
        onOpenChange={setOpen}
        action="delete the production environment"
        account="sam@keel.dev"
        grantFor="10 minutes"
        codeHint="Enter the 6-digit code from your authenticator app. Try 123456."
        busy={busy}
        error={error}
        onPasskey={async () => {
          setBusy(true)
          await wait()
          setBusy(false)
          setError("This device has no passkey for sam@keel.dev. Use a code instead.")
        }}
        onVerifyCode={async (code) => {
          setBusy(true)
          await wait()
          setBusy(false)
          if (code === "123456") pass()
          else setError("That code doesn’t match. Codes change every 30 seconds.")
        }}
      />
    </div>
  )
}
```

## <StepUpDialog>

Props:

- `open: boolean`: Whether the dialog is open. The app opens it when a step needs a fresh check.
- `onOpenChange: (open: boolean) => void`: Called with `false` when the person cancels or closes it; the step it guards does not run.
- `action?: string | undefined`: The step waiting on the check, as a verb phrase in the app's words: "delete the production environment".
- `account?: string | undefined`: Who is being checked, from the app: an email or a name.
- `onPasskey?: (() => void) | undefined`: Shows "Use a passkey"; called when it's pressed. Run the WebAuthn ceremony here.
- `onSso?: (() => void) | undefined`: Shows "Sign in again with company SSO"; called when it's pressed.
- `onVerifyCode?: ((code: string) => void | Promise<void>) | undefined`: Shows the code field; called with the code typed once it has `codeLength` digits and Verify is pressed.
- `onSendCode?: (() => void) | undefined`: Shows "Send a new code" under the code field, for a code the app sends by email or text.
- `codeHint?: string | undefined`: Where the code comes from, in the app's words: "Enter the code from your authenticator app." Replaces the general line.
- `codeLength?: number | undefined` (default `6`): How many digits the code has. 6 unless the app says otherwise.
- `grantFor?: string | undefined`: How long the check lasts once passed, in the app's words ("10 minutes"). Left out, nothing is said about it.
- `busy?: boolean | undefined` (default `false`): A check is in flight: the methods can't be pressed again and the pressed one shows a spinner.
- `error?: string | undefined`: Why the last check failed, from the app. Shown under the methods.
- `className?: string | undefined`

## Strings (`stepUpDialog`)

The fixed words, English by default. Override any of them under `stepUpDialog` in `<LocaleProvider strings={{ stepUpDialog: { … } }}>`; the table is exported as `stepUpDialogStrings`.

- `title`: Confirm it's you
- `description`: Before you {action}, check it's you.
- `descriptionGeneric`: Before this step runs, check it's you.
- `account`: Signed in as {account}
- `passkey`: Use a passkey
- `sso`: Sign in again with company SSO
- `or`: or
- `code`: Verification code
- `codeHint`: Enter the {length}-digit code.
- `verify`: Verify
- `lockedChecking`: Checking it’s you
- `verifyNeedsCode`: Enter all {length} digits of the code first.
- `newCode`: Send a new code
- `cancel`: Cancel
- `cancelNote`: Cancelling runs nothing: the step waits until it's checked.
- `grant`: You won't be asked again for {grantFor}.

## Motion

- **busy** (live): While the app checks a code, a passkey or a sign-in, a spinner turns in the button that was pressed. It stops when the app says it is done, and under reduced motion it is still.
- **message** (arrive, fade): A refusal fades in under the methods when the app reports one, and again when its text changes. One already showing when the dialog opens is still.

Set with `--cd-motion-step-up-dialog-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
