"use client"; import { useState } from "react"; import Link from "next/link"; import { useTranslations } from "next-intl"; 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 = [ "recipe.created", "recipe.updated", "recipe.published", "recipe.deleted", "meal_plan.updated", "shopping_list.completed", "comment.added", ] as const; type WebhookEventType = (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 WebhooksManager({ initialWebhooks }: { initialWebhooks: Webhook[] }) { const t = useTranslations("settingsForm"); 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: WebhookEventType) { 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/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 ?? t("webhookCreateFailed")); } 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 : t("webhookCreateFailed")); } finally { setCreating(false); } } async function handleDelete(id: string) { try { const res = await fetch(`/api/v1/webhooks/${id}`, { method: "DELETE" }); if (!res.ok && res.status !== 204) { const data = await res.json() as { error?: string }; throw new Error(data.error ?? t("webhookDeleteFailed")); } setWebhookList((prev) => prev.filter((w) => w.id !== id)); if (newSecret?.id === id) setNewSecret(null); } catch (err) { toast.error(err instanceof Error ? err.message : t("webhookDeleteFailed")); } } async function handleToggleActive(id: string, currentActive: boolean) { try { const res = await fetch(`/api/v1/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 ?? t("webhookUpdateFailed")); } setWebhookList((prev) => prev.map((w) => (w.id === id ? { ...w, active: !currentActive } : w)) ); } catch (err) { toast.error(err instanceof Error ? err.message : t("webhookUpdateFailed")); } } async function handleCopySecret() { if (!newSecret) return; try { await navigator.clipboard.writeText(newSecret.secret); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast.error(t("copyFailed")); } } 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/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/webhooks/${webhookId}/redeliver`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ deliveryId }), }); if (res.ok) { toast.success(t("redeliverySuccess")); } else { toast.error(t("redeliveryFailed")); } } finally { setRedelivering(null); } } return (
View webhook docs & Zapier integration →
{/* Create form */}
setUrl(e.target.value)} maxLength={2048} required />

Select which events trigger this webhook. Leave all unchecked to receive all events.

{ALL_EVENTS.map((event) => { const checked = selectedEvents.includes(event); return ( ); })}
{/* New secret reveal */} {newSecret && (

Save this signing secret — it will not be shown again.

Use it to verify the X-Epicure-Signature header on incoming requests.

{newSecret.secret}
)} {/* Webhook list */} {webhookList.length === 0 ? (

No webhooks 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} ))}
)} {w.events.length === 0 && (

All events

)}
{/* Delivery history */} {isExpanded && (
{loadingDeliveries === w.id ? (

Loading…

) : wDeliveries.length === 0 ? (

No deliveries yet.

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