"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import {
Check,
Copy,
Eye,
EyeOff,
Loader2,
Plus,
Power,
RefreshCw,
Send,
Trash2,
Webhook,
} from "lucide-react"
import { WEBHOOK_EVENTS } from "@/lib/webhooks/events"
import {
createWebhookEndpoint,
deleteWebhookEndpoint,
rotateWebhookSecret,
sendTestWebhook,
updateWebhookEndpoint,
type WebhookEndpointDTO,
} from "@/app/actions/webhooks"
const inputClass =
"w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none ring-indigo-500 transition focus:border-indigo-500/50 focus:ring-1"
function formatDate(value: string | null): string {
if (!value) return "Never"
const d = new Date(value)
return Number.isNaN(d.getTime())
? "—"
: d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
}
function eventsSummary(events: string[]): string {
if (events.length === 0) return "All events"
if (events.length === 1) return events[0]
return `${events.length} events`
}
function SecretField({ secret }: { secret: string }) {
const [shown, setShown] = useState(false)
const [copied, setCopied] = useState(false)
async function copy() {
try {
await navigator.clipboard.writeText(secret)
setCopied(true)
toast.success("Signing secret copied")
setTimeout(() => setCopied(false), 2000)
} catch {
toast.error("Copy failed — reveal and copy manually")
}
}
return (
{shown ? secret : "whsec_" + "•".repeat(24)}
)
}
export function WebhookManager({ initialEndpoints }: { initialEndpoints: WebhookEndpointDTO[] }) {
const router = useRouter()
const [url, setUrl] = useState("")
const [description, setDescription] = useState("")
const [allEvents, setAllEvents] = useState(true)
const [selected, setSelected] = useState>(new Set())
const [creating, setCreating] = useState(false)
const [busyId, setBusyId] = useState(null)
function toggleEvent(id: string) {
setSelected((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
async function handleCreate(e: React.FormEvent) {
e.preventDefault()
const trimmed = url.trim()
if (!trimmed) return
setCreating(true)
try {
await createWebhookEndpoint({
url: trimmed,
description: description.trim() || undefined,
events: allEvents ? [] : Array.from(selected),
})
setUrl("")
setDescription("")
setAllEvents(true)
setSelected(new Set())
toast.success("Webhook endpoint created")
router.refresh()
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to create webhook")
} finally {
setCreating(false)
}
}
async function withBusy(id: string, fn: () => Promise) {
setBusyId(id)
try {
await fn()
} finally {
setBusyId(null)
}
}
async function handleTest(ep: WebhookEndpointDTO) {
await withBusy(ep.id, async () => {
try {
const res = await sendTestWebhook(ep.id)
if (res.ok) toast.success(`Test delivered (HTTP ${res.responseStatus})`)
else toast.error(`Test failed: ${res.error ?? "no response"}`)
router.refresh()
} catch (err) {
toast.error(err instanceof Error ? err.message : "Test failed")
}
})
}
async function handleToggle(ep: WebhookEndpointDTO) {
await withBusy(ep.id, async () => {
try {
await updateWebhookEndpoint(ep.id, { status: ep.status === "active" ? "disabled" : "active" })
toast.success(ep.status === "active" ? "Webhook disabled" : "Webhook enabled")
router.refresh()
} catch (err) {
toast.error(err instanceof Error ? err.message : "Update failed")
}
})
}
async function handleRotate(ep: WebhookEndpointDTO) {
if (!confirm("Rotate the signing secret? The current secret stops working immediately.")) return
await withBusy(ep.id, async () => {
try {
await rotateWebhookSecret(ep.id)
toast.success("Signing secret rotated")
router.refresh()
} catch (err) {
toast.error(err instanceof Error ? err.message : "Rotate failed")
}
})
}
async function handleDelete(ep: WebhookEndpointDTO) {
if (!confirm(`Delete webhook for ${ep.url}? This cannot be undone.`)) return
await withBusy(ep.id, async () => {
try {
await deleteWebhookEndpoint(ep.id)
toast.success("Webhook deleted")
router.refresh()
} catch (err) {
toast.error(err instanceof Error ? err.message : "Delete failed")
}
})
}
return (
{/* Create form */}
{/* Endpoints list */}
Endpoints ({initialEndpoints.length})
{initialEndpoints.length === 0 ? (
No endpoints yet. Add one above to start receiving events.
) : (
{initialEndpoints.map((ep) => {
const busy = busyId === ep.id
const disabled = ep.status === "disabled"
return (
-
{ep.url}
{disabled && (
Disabled
)}
{ep.source === "zapier" && (
Zapier
)}
{eventsSummary(ep.events)}
Last success {formatDate(ep.last_success_at)}
{ep.failure_count > 0 && (
{ep.failure_count} recent failure{ep.failure_count > 1 ? "s" : ""}
)}
{ep.description && (
{ep.description}
)}
)
})}
)}
)
}