"use client"; import { useState } from "react"; import { toast } from "sonner"; import { ChevronDown, ChevronUp, RotateCcw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; const ALL_EVENTS = ["user.signed_up", "support_ticket.created", "report.filed"] as const; type AdminWebhookEventType = (typeof ALL_EVENTS)[number]; type Webhook = { id: string; url: string; events: string[]; active: boolean; createdAt: string; }; type Delivery = { id: string; event: string; statusCode: number | null; success: boolean; attempts: number; createdAt: string; }; type CreateWebhookResponse = { id: string; url: string; events: string[]; secret: string; active: boolean; createdAt: string; }; function formatDate(iso: string) { return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); } function formatDateTime(iso: string) { return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } export function AdminWebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[] }) { const [webhookList, setWebhookList] = useState(initialWebhooks); const [url, setUrl] = useState(""); const [selectedEvents, setSelectedEvents] = useState([]); const [creating, setCreating] = useState(false); const [newSecret, setNewSecret] = useState<{ id: string; secret: string } | null>(null); const [copied, setCopied] = useState(false); const [deliveries, setDeliveries] = useState>({}); const [loadingDeliveries, setLoadingDeliveries] = useState(null); const [expandedDeliveries, setExpandedDeliveries] = useState>(new Set()); const [redelivering, setRedelivering] = useState(null); function toggleEvent(event: AdminWebhookEventType) { setSelectedEvents((prev) => (prev.includes(event) ? prev.filter((e) => e !== event) : [...prev, event])); } async function handleCreate(e: React.FormEvent) { e.preventDefault(); if (!url.trim()) return; setCreating(true); try { const res = await fetch("/api/v1/admin/webhooks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: url.trim(), events: selectedEvents }), }); if (!res.ok) { const data = await res.json() as { error?: string }; throw new Error(data.error ?? "Failed to add webhook"); } const data = await res.json() as CreateWebhookResponse; setNewSecret({ id: data.id, secret: data.secret }); setWebhookList((prev) => [ { id: data.id, url: data.url, events: data.events, active: data.active, createdAt: data.createdAt }, ...prev, ]); setUrl(""); setSelectedEvents([]); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to add webhook"); } finally { setCreating(false); } } async function handleDelete(id: string) { try { const res = await fetch(`/api/v1/admin/webhooks/${id}`, { method: "DELETE" }); if (!res.ok && res.status !== 204) { const data = await res.json() as { error?: string }; throw new Error(data.error ?? "Failed to delete webhook"); } setWebhookList((prev) => prev.filter((w) => w.id !== id)); if (newSecret?.id === id) setNewSecret(null); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to delete webhook"); } } async function handleToggleActive(id: string, currentActive: boolean) { try { const res = await fetch(`/api/v1/admin/webhooks/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ active: !currentActive }), }); if (!res.ok) { const data = await res.json() as { error?: string }; throw new Error(data.error ?? "Failed to update webhook"); } setWebhookList((prev) => prev.map((w) => (w.id === id ? { ...w, active: !currentActive } : w))); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to update webhook"); } } async function handleCopySecret() { if (!newSecret) return; try { await navigator.clipboard.writeText(newSecret.secret); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast.error("Copy failed"); } } async function toggleDeliveries(webhookId: string) { const isExpanded = expandedDeliveries.has(webhookId); if (isExpanded) { setExpandedDeliveries((prev) => { const s = new Set(prev); s.delete(webhookId); return s; }); return; } setExpandedDeliveries((prev) => new Set([...prev, webhookId])); if (deliveries[webhookId]) return; setLoadingDeliveries(webhookId); try { const res = await fetch(`/api/v1/admin/webhooks/${webhookId}/deliveries`); if (res.ok) { const data = await res.json() as Delivery[]; setDeliveries((prev) => ({ ...prev, [webhookId]: data })); } } finally { setLoadingDeliveries(null); } } async function handleRedeliver(webhookId: string, deliveryId: string) { setRedelivering(deliveryId); try { const res = await fetch(`/api/v1/admin/webhooks/${webhookId}/redeliver`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ deliveryId }), }); if (res.ok) toast.success("Redelivered"); else toast.error("Redelivery failed"); } finally { setRedelivering(null); } } return (
setUrl(e.target.value)} maxLength={2048} required />

Leave all unchecked to receive every event.

{ALL_EVENTS.map((event) => { const checked = selectedEvents.includes(event); return ( ); })}
{newSecret && (

Save this secret now — it won't be shown again.

Used to verify the X-Epicure-Signature header on each delivery.

{newSecret.secret}
)} {webhookList.length === 0 ? (

No webhooks configured yet.

) : (
{webhookList.map((w) => { const isExpanded = expandedDeliveries.has(w.id); const wDeliveries = deliveries[w.id] ?? []; return (

{w.url}

Added {formatDate(w.createdAt)} · {w.active ? "Active" : "Inactive"}
{w.events.length > 0 ? (
{w.events.map((ev) => ( {ev} ))}
) : (

All events

)}
{isExpanded && (
{loadingDeliveries === w.id ? (

Loading…

) : wDeliveries.length === 0 ? (

No deliveries yet.

) : (
{wDeliveries.map((d) => (
{d.statusCode ?? "err"} {d.event} {formatDateTime(d.createdAt)}
))}
)}
)}
); })}
)}
); }