1"use client";
2
3
17import { useEffect, useRef, useState } from "react";
18import { Check, Copy, ShieldAlert } from "lucide-react";
19import { Dialog } from "@/components/dialogs/dialog";
20
21interface ApiKeyRevealDialogProps {
22 open: boolean;
23 rawKey: string | null;
24 keyName: string;
25 onConfirmed: () => void;
26}
27
28async function copyText(text: string): Promise<boolean> {
29 try {
30 await navigator.clipboard.writeText(text);
31 return true;
32 } catch {
33
34
35 try {
36 const ta = document.createElement("textarea");
37 ta.value = text;
38 ta.setAttribute("readonly", "");
39 ta.style.position = "fixed";
40 ta.style.opacity = "0";
41 document.body.appendChild(ta);
42 ta.select();
43 const ok = document.execCommand("copy");
44 document.body.removeChild(ta);
45 return ok;
46 } catch {
47 return false;
48 }
49 }
50}
51
52export function ApiKeyRevealDialog({
53 open,
54 rawKey,
55 keyName,
56 onConfirmed,
57}: ApiKeyRevealDialogProps) {
58 const [copied, setCopied] = useState(false);
59 const [storeChecked, setStoreChecked] = useState(false);
60 const copyBtn = useRef<HTMLButtonElement>(null);
61
62
63 useEffect(() => {
64 if (open) {
65 setCopied(false);
66 setStoreChecked(false);
67
68 setTimeout(() => {
69 const el = document.getElementById("apikey-raw") as HTMLInputElement | null;
70 el?.focus();
71 el?.select();
72 }, 0);
73 }
74 }, [open]);
75
76 const doCopy = async () => {
77 if (!rawKey) return;
78 const ok = await copyText(rawKey);
79 setCopied(ok);
80 if (ok) {
81
82 setTimeout(() => setCopied(false), 2500);
83 copyBtn.current?.focus();
84 }
85 };
86
87 return (
88 <Dialog
89 open={open}
90 title="Your API key"
91 description={`“${keyName}” was created.`}
92 onClose={onConfirmed}
93 footer={
94 <button
95 type="button"
96 disabled={!storeChecked}
97 onClick={onConfirmed}
98 className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
99 >
100 I have stored it
101 </button>
102 }
103 >
104 <div className="space-y-3">
105 <div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-2.5 text-xs text-destructive">
106 <ShieldAlert className="mt-0.5 size-4 shrink-0" aria-hidden />
107 <span>
108 <strong className="font-semibold">This key will not be shown again.</strong> Store it
109 securely — the Factory only keeps its hash. If you lose it, revoke and recreate.
110 </span>
111 </div>
112
113 <div>
114 <label
115 className="mb-1 block text-xs font-medium text-muted-foreground"
116 htmlFor="apikey-raw"
117 >
118 API key
119 </label>
120 {}
124 <div className="flex flex-col gap-1.5 md:flex-row md:items-center">
125 <input
126 id="apikey-raw"
127 readOnly
128 value={rawKey ?? ""}
129 onFocus={(e) => e.currentTarget.select()}
130 aria-label="API key (shown once)"
131 data-testid="apikey-raw"
132 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"
133 />
134 <button
135 ref={copyBtn}
136 type="button"
137 onClick={doCopy}
138 data-testid="apikey-copy"
139 aria-label="Copy API key"
140 aria-live="polite"
141 className="inline-flex shrink-0 items-center justify-center gap-1 rounded-md border border-border px-3 py-1.5 text-xs font-medium hover:bg-accent md:w-auto"
142 >
143 {copied ? (
144 <Check className="size-3.5 text-emerald-600" />
145 ) : (
146 <Copy className="size-3.5" />
147 )}
148 {copied ? "Copied" : "Copy"}
149 </button>
150 </div>
151 {}
152 <span role="status" className="sr-only">
153 {copied ? "API key copied to clipboard" : ""}
154 </span>
155 </div>
156
157 <label className="flex cursor-pointer items-start gap-2 text-sm">
158 <input
159 type="checkbox"
160 className="mt-0.5 accent-primary"
161 checked={storeChecked}
162 onChange={(e) => setStoreChecked(e.target.checked)}
163 data-testid="apikey-stored-confirm"
164 />
165 <span>I have stored this key securely.</span>
166 </label>
167 </div>
168 </Dialog>
169 );
170}
171