Vectrum
Sign In
141 lines · 5.6 KB · TypeScript
"use client";

import { useActionState, type ReactNode } from "react";
import {
  cancelTwoFactorSetupAction,
  disableTwoFactorAction,
  enableTwoFactorAction,
  regenerateRecoveryCodesAction,
} from "@/app/actions/settings";
import { DownloadIcon } from "@/components/icons";
import { CopyIconButton, TextField } from "@/components/settings-fields";
import { PlateBody, PlateFooter } from "@/components/settings-plate";
import { Alert, Button } from "@/components/ui";

function CodeField({ label = "Authentication code", description }: { label?: string; description?: ReactNode }) {
  return (
    <TextField
      label={label}
      description={description}
      name="code"
      required
      autoComplete="one-time-code"
      autoCapitalize="none"
      spellCheck={false}
      maxLength={20}
      className="max-w-xs"
      mono
    />
  );
}

/** Recovery codes, shown once: copy, download, then Done reloads the page. */
function RecoveryCodes({ codes, notice }: { codes: string[]; notice: string }) {
  const text = codes.join("\n");
  const download = () => {
    const url = URL.createObjectURL(new Blob([`${text}\n`], { type: "text/plain" }));
    const a = Object.assign(document.createElement("a"), { href: url, download: "recovery-codes.txt" });
    a.click();
    URL.revokeObjectURL(url);
  };
  return (
    <>
      <PlateBody className="max-w-2xl">
        <Alert kind="success">{notice}</Alert>
        <p className="type-body text-ink2">
          Save these recovery codes somewhere safe, like a password manager. If you lose your device, each one signs you in once.
          You won’t be able to see them again.
        </p>
        <div className="flex items-start gap-2 rounded-row recipe-well-field py-3 pr-1.5 pl-4">
          <ul className="grid flex-1 grid-cols-2 gap-x-8 gap-y-1.5 font-mono text-[13px] leading-5 text-ink">
            {codes.map((code) => (
              <li key={code}>{code}</li>
            ))}
          </ul>
          <CopyIconButton text={text} label="Copy codes" toast="Copied recovery codes" />
        </div>
      </PlateBody>
      <PlateFooter>
        <Button type="button" icon={<DownloadIcon />} onClick={download}>
          Download
        </Button>
        <Button type="button" variant="primary" onClick={() => window.location.reload()}>
          Done
        </Button>
      </PlateFooter>
    </>
  );
}

export function EnableTwoFactorForm({ qr, secret }: { qr: ReactNode; secret: string }) {
  const [state, action, pending] = useActionState(enableTwoFactorAction, undefined);
  if (state?.codes) return <RecoveryCodes codes={state.codes} notice="Two-factor authentication is on." />;
  return (
    <form action={action}>
      <PlateBody className="max-w-2xl gap-6">
        <div className="flex flex-col gap-4 sm:flex-row sm:items-start">
          <div className="shrink-0 self-start rounded-row bg-white p-1.5">{qr}</div>
          <div className="flex min-w-0 flex-col gap-3">
            <div className="flex flex-col gap-1">
              <p className="type-title text-ink">1. Scan the QR code</p>
              <p className="type-body text-ink2">Use an authenticator app like 1Password, Google Authenticator, Microsoft Authenticator, Authy or Bitwarden.</p>
            </div>
            <div className="flex flex-col gap-1.5">
              <p className="type-body text-ink2">Can’t scan it? Enter this setup key instead:</p>
              <div className="flex items-center gap-2 rounded-row recipe-well-field py-1.5 pr-1.5 pl-3.5">
                <code className="flex-1 font-mono text-[12.5px] leading-5 break-all text-ink">{secret}</code>
                <CopyIconButton text={secret.replace(/\s/g, "")} label="Copy setup key" toast="Copied setup key" />
              </div>
            </div>
          </div>
        </div>
        <div className="flex flex-col gap-3">
          <p className="type-title text-ink">2. Enter the 6-digit code from the app</p>
          <CodeField label="Code" />
        </div>
        {state?.error && <Alert>{state.error}</Alert>}
      </PlateBody>
      <PlateFooter>
        <Button type="submit" formAction={cancelTwoFactorSetupAction} formNoValidate disabled={pending}>
          Cancel
        </Button>
        <Button variant="primary" disabled={pending}>
          {pending ? "Verifying…" : "Verify and Turn On"}
        </Button>
      </PlateFooter>
    </form>
  );
}

export function RegenerateRecoveryCodesForm() {
  const [state, action, pending] = useActionState(regenerateRecoveryCodesAction, undefined);
  if (state?.codes) return <RecoveryCodes codes={state.codes} notice="New recovery codes generated. The old ones no longer work." />;
  return (
    <form action={action}>
      <PlateBody className="max-w-2xl">
        <CodeField description="A code from your authenticator app, to generate new recovery codes. The old ones stop working." />
        {state?.error && <Alert>{state.error}</Alert>}
      </PlateBody>
      <PlateFooter>
        <Button disabled={pending}>{pending ? "Generating…" : "Generate New Codes"}</Button>
      </PlateFooter>
    </form>
  );
}

export function DisableTwoFactorForm() {
  const [state, action, pending] = useActionState(disableTwoFactorAction, undefined);
  return (
    <form action={action}>
      <PlateBody className="max-w-2xl">
        <CodeField description="A code from your authenticator app, or a recovery code. Signing in will take only your password again." />
        {state?.error && <Alert>{state.error}</Alert>}
      </PlateBody>
      <PlateFooter>
        <Button variant="danger" disabled={pending}>
          {pending ? "Turning off…" : "Turn Off Two-Factor Authentication"}
        </Button>
      </PlateFooter>
    </form>
  );
}