"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>
);
}