File
Blob: src/client/components/webhooks/secret-reveal-dialog.tsx
| 1 | import { useEffect, useState } from "react"; |
| 2 | import { Check, Copy, ExternalLink, Key } from "lucide-react"; |
| 3 | import { Button, Dialog } from "@/client/components/ui"; |
| 4 | import { useToast } from "@/client/toast"; |
| 5 | import type { WebhookProvider } from "@/contracts"; |
| 6 | import { webhookProviderCatalog } from "@/lib/webhooks"; |
| 7 | |
| 8 | interface SecretRevealDialogProps { |
| 9 | open: boolean; |
| 10 | onClose: () => void; |
| 11 | secret: string; |
| 12 | provider: WebhookProvider; |
| 13 | webhookUrl: string; |
| 14 | } |
| 15 | |
| 16 | export 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 | }; |