"use client"; /** * ApiKeyRevealDialog (TASK-0128) — the one-time raw-key reveal. * * The raw key (`fk_live_…`) appears in EXACTLY ONE DOM location: this * dialog (the create response body + this state). It is never persisted * (no sessionStorage/localStorage — the "Security" requirement), never * re-fetchable (the list only carries the prefix), and dismissing it * (the explicit "I have stored it" confirmation) drops it from UI state * permanently — closing the tab loses it. * * A11y: the dialog focus-traps (role=dialog + aria-modal), the copy * button announces via an aria-live region, the key is a read-only * monospace field (selectable + copyable). */ import { useEffect, useRef, useState } from "react"; import { Check, Copy, ShieldAlert } from "lucide-react"; import { Dialog } from "@/components/dialogs/dialog"; interface ApiKeyRevealDialogProps { open: boolean; rawKey: string | null; keyName: string; onConfirmed: () => void; } async function copyText(text: string): Promise { try { await navigator.clipboard.writeText(text); return true; } catch { // Clipboard API unavailable (insecure context / permission) — the // textarea fallback path. try { const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); const ok = document.execCommand("copy"); document.body.removeChild(ta); return ok; } catch { return false; } } } export function ApiKeyRevealDialog({ open, rawKey, keyName, onConfirmed, }: ApiKeyRevealDialogProps) { const [copied, setCopied] = useState(false); const [storeChecked, setStoreChecked] = useState(false); const copyBtn = useRef(null); // Focus lands in the dialog on open (a11y: focus trap entry point). useEffect(() => { if (open) { setCopied(false); setStoreChecked(false); // Move focus to the key field (the first focusable content). setTimeout(() => { const el = document.getElementById("apikey-raw") as HTMLInputElement | null; el?.focus(); el?.select(); }, 0); } }, [open]); const doCopy = async () => { if (!rawKey) return; const ok = await copyText(rawKey); setCopied(ok); if (ok) { // Announce to screen readers + visually reset after a beat. setTimeout(() => setCopied(false), 2500); copyBtn.current?.focus(); } }; return ( I have stored it } >
This key will not be shown again. Store it securely — the Factory only keeps its hash. If you lose it, revoke and recreate.
{/* Stacked below md (the 32-char key does not fit beside the button). The breakpoint is `md` — not `sm` — because the mobile E2E viewport is exactly 640px, the `sm` boundary, where `sm:flex-row` would still apply and shift the layout. */}
e.currentTarget.select()} aria-label="API key (shown once)" data-testid="apikey-raw" className="min-w-0 w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-xs outline-none focus-visible:ring-1 focus-visible:ring-ring" />
{/* aria-live copy announcement. */} {copied ? "API key copied to clipboard" : ""}
); }