Skip to content
File

Blob: src/client/components/webhooks/secret-reveal-dialog.tsx

typescript114 lines
1import { useEffect, useState } from "react";
2import { Check, Copy, ExternalLink, Key } from "lucide-react";
3import { Button, Dialog } from "@/client/components/ui";
4import { useToast } from "@/client/toast";
5import type { WebhookProvider } from "@/contracts";
6import { webhookProviderCatalog } from "@/lib/webhooks";
7 
8interface SecretRevealDialogProps {
9 open: boolean;
10 onClose: () => void;
11 secret: string;
12 provider: WebhookProvider;
13 webhookUrl: string;
14}
15 
16export const SecretRevealDialog = ({ open, onClose, secret, provider, webhookUrl }: SecretRevealDialogProps) => {
17 const { pushToast } = useToast();
18 const [secondsRemaining, setSecondsRemaining] = useState(5);
19 const [copied, setCopied] = useState(false);
20 
21 const catalog = webhookProviderCatalog[provider];
22 
23 useEffect(() => {
24 if (!open) {
25 return;
26 }
27 
28 const interval = setInterval(() => {
29 setSecondsRemaining((prev) => {
30 if (prev <= 1) {
31 clearInterval(interval);
32 return 0;
33 }
34 return prev - 1;
35 });
36 }, 1000);
37 
38 return () => clearInterval(interval);
39 }, [open]);
40 
41 const handleCopySecret = () => {
42 void navigator.clipboard.writeText(secret).then(() => {
43 setCopied(true);
44 pushToast({ tone: "info", title: "Copied", message: "Webhook secret copied to clipboard." });
45 setTimeout(() => setCopied(false), 2000);
46 });
47 };
48 
49 return (
50 <Dialog
51 open={open}
52 onClose={onClose}
53 title="Webhook Secret"
54 description={`${catalog.displayName} webhook configuration`}
55 icon={<Key className="h-5 w-5" />}
56 dismissDisabled={secondsRemaining > 0}
57 >
58 <div className="space-y-4">
59 <p className="text-sm text-amber-400">
60 This secret is shown only once. Copy it now and paste it into your {catalog.displayName} webhook settings.
61 </p>
62
63 <div className="rounded-xl border border-zinc-700/60 bg-zinc-800/80 p-3">
64 <p className="mb-2 text-xs uppercase tracking-[0.18em] text-zinc-500">Secret</p>
65 <p className="break-all font-mono text-sm text-accent-300">{secret}</p>
66 </div>
67
68 <div className="rounded-xl border border-zinc-700/60 bg-zinc-800/80 p-3">
69 <p className="mb-2 text-xs uppercase tracking-[0.18em] text-zinc-500">Webhook URL</p>
70 <p className="break-all font-mono text-sm text-accent-300">{webhookUrl}</p>
71 </div>
72
73 <div>
74 <p className="mb-2 text-xs uppercase tracking-[0.18em] text-zinc-500">Setup instructions</p>
75 <ol className="list-decimal space-y-1 pl-5 text-sm leading-6 text-zinc-400">
76 {catalog.setupInstructions.map((step, i) => (
77 <li key={i}>{step}</li>
78 ))}
79 </ol>
80 <a
81 href={catalog.docsUrl}
82 target="_blank"
83 rel="noopener noreferrer"
84 className="mt-2 inline-flex items-center gap-1 text-sm text-accent-300 transition-colors hover:text-accent-200"
85 >
86 View {catalog.displayName} docs
87 <ExternalLink className="h-3.5 w-3.5" />
88 </a>
89 </div>
90
91 <div className="flex gap-3">
92 <Button
93 variant="primary"
94 className="flex-1"
95 icon={copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
96 onClick={handleCopySecret}
97 >
98 {copied ? "Copied" : "Copy Secret"}
99 </Button>
100 {secondsRemaining > 0 ? (
101 <Button variant="secondary" disabled>
102 Wait {secondsRemaining}s...
103 </Button>
104 ) : (
105 <Button variant="secondary" onClick={onClose}>
106 Done
107 </Button>
108 )}
109 </div>
110 </div>
111 </Dialog>
112 );
113};