import { useState } from "react"; import { Check, Copy, ExternalLink, Webhook } from "lucide-react"; import type { WebhookProvider } from "@/contracts"; import { Badge, Button, Dialog, ErrorBanner, Input } from "@/client/components/ui"; import { formatApiError, getApiClient } from "@/client/lib"; import { useToast } from "@/client/toast"; import { inferWebhookInstanceUrl, webhookProviderCatalog, webhookProviderCatalogList } from "@/lib/webhooks"; import { getRecommendedProvider, getWebhookProviderIcon } from "@/client/components/webhooks/webhook-presentation"; interface AddWebhookDialogProps { open: boolean; onClose: () => void; projectId: string; project: { ownerSlug: string; projectSlug: string; repoUrl: string }; configuredProviders: WebhookProvider[]; onCreated: (provider: WebhookProvider, generatedSecret: string | null, webhookUrl: string) => void; } export const AddWebhookDialog = ({ open, onClose, projectId, project, configuredProviders, onCreated, }: AddWebhookDialogProps) => { const { pushToast } = useToast(); const [step, setStep] = useState<1 | 2>(1); const [selectedProvider, setSelectedProvider] = useState(null); const [instanceUrl, setInstanceUrl] = useState(""); const [creating, setCreating] = useState(false); const [error, setError] = useState(null); const [copiedUrl, setCopiedUrl] = useState(false); const handleClose = () => { setStep(1); setSelectedProvider(null); setInstanceUrl(""); setCreating(false); setError(null); setCopiedUrl(false); onClose(); }; const recommended = getRecommendedProvider(project.repoUrl); const catalog = selectedProvider ? webhookProviderCatalog[selectedProvider] : null; const webhookUrl = selectedProvider ? `${window.location.origin}/api/public/hooks/${selectedProvider}/${project.ownerSlug}/${project.projectSlug}` : ""; const handleSelectProvider = (provider: WebhookProvider) => { if (configuredProviders.includes(provider)) return; setSelectedProvider(provider); const entry = webhookProviderCatalog[provider]; setInstanceUrl( entry.configMode === "forbidden" ? "" : inferWebhookInstanceUrl(provider, project.repoUrl) || entry.defaultInstanceUrl || "", ); setStep(2); }; const handleCopyUrl = () => { void navigator.clipboard.writeText(webhookUrl).then(() => { setCopiedUrl(true); pushToast({ tone: "success", title: "Copied", message: "Webhook URL copied to clipboard." }); setTimeout(() => setCopiedUrl(false), 2000); }); }; const handleCreate = () => { if (!selectedProvider) return; const entry = webhookProviderCatalog[selectedProvider]; const trimmedInstanceUrl = instanceUrl.trim(); if (entry.configMode === "required" && trimmedInstanceUrl.length === 0) { setError(`${entry.displayName} webhooks require an instance URL.`); return; } setCreating(true); setError(null); const config = entry.configMode === "forbidden" ? undefined : trimmedInstanceUrl.length > 0 ? { instanceUrl: trimmedInstanceUrl } : null; void getApiClient() .createWebhook(projectId, selectedProvider, { enabled: true, ...(config === undefined ? {} : { config }), }) .then((response) => { onCreated(selectedProvider, response.generatedSecret, webhookUrl); handleClose(); }) .catch((reason: unknown) => { setError(formatApiError(reason)); }) .finally(() => { setCreating(false); }); }; const handleBack = () => { setStep(1); setSelectedProvider(null); setInstanceUrl(""); setError(null); setCopiedUrl(false); }; if (step === 2 && catalog && selectedProvider) { const instanceUrlRequired = catalog.configMode === "required"; return (
{catalog.configMode !== "forbidden" ? ( setInstanceUrl(e.target.value)} placeholder={catalog.defaultInstanceUrl ?? "https://example.com"} required={instanceUrlRequired} /> ) : null}

Webhook URL

{webhookUrl}

Setup Instructions

    {catalog.setupInstructions.map((instruction) => (
  1. {instruction}
  2. ))}
Provider documentation {error ? : null}
); } return ( } >
{webhookProviderCatalogList.map((entry) => { const isConfigured = configuredProviders.includes(entry.provider); const isRecommended = entry.provider === recommended; return ( ); })}
); };