import { useCallback, useEffect, useState } from "react"; import { CalendarClock, Check, CheckCircle2, AlertCircle, Globe, GitBranch, Radio, Webhook, } from "lucide-react"; import { SetupWizardNavigation, SetupWizardFooter, } from "@/components/SetupWizard"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { cn } from "@/lib/utils"; import { AgentInstructions, CopyField } from "./WebhookFields"; import { WebhookUrlWarning } from "./WebhookUrlWarning"; export type TriggerDraft = { kind: "choose" | "schedule" | "webhook"; step: number; availableStep: number; sender: "custom" | "github"; /** Retained when resuming webhooks created before generic signed-app support. */ signingMode?: "bearer" | "app_webhook" | "fireflies_hmac"; frequency: string; time: string; weekday: string; timezone: string; created: boolean; }; export const defaultTriggerDraft: TriggerDraft = { kind: "choose", step: 0, availableStep: 0, sender: "custom", frequency: "weekdays", time: "09:00", weekday: "Monday", timezone: "America/Chicago", created: false, }; export function webhookAgentInstructions( sender: TriggerDraft["sender"], routineTitle: string, webhookUrl: string, webhookSecret: string, setupPending = true, signingMode: TriggerDraft["signingMode"] = "app_webhook", ) { const common = [ `Connect the sending app to the Paperclip routine ${JSON.stringify(routineTitle)}.`, `Webhook URL: ${webhookUrl}`, "Send an HTTP POST request with a JSON object as the body (not an array or string).", "Content-Type: application/json", ]; const auth = sender === "github" ? [ "In GitHub, open your repository → Settings → Webhooks → Add webhook.", "Use the webhook URL above as Payload URL and select application/json as Content type.", `Secret: ${webhookSecret}`, "Paste this value into GitHub’s Secret field. GitHub signs requests with X-Hub-Signature-256; do not use Bearer authentication.", "Select the events that should start this routine, enable the webhook, and save.", "To check the connection, open Recent Deliveries and redeliver an event.", ] : [ `Secret key: ${webhookSecret}`, ...(signingMode === "bearer" ? [] : [ `If the app asks for a signing secret, paste the secret key above. Paperclip accepts HMAC-SHA256 over the exact request body in ${signingMode === "fireflies_hmac" ? "X-Hub-Signature" : "X-Hub-Signature or X-Hub-Signature-256"}, formatted sha256=.`, ]), ...(signingMode === "fireflies_hmac" ? [] : [ `For apps with custom headers, use Authorization: Bearer ${webhookSecret}`, ]), "Subscribe only to the events that should start this routine. Public services need a publicly reachable HTTPS URL.", "In the sending app, add a webhook using this URL, POST method, JSON body, and headers, then save it.", "Send a unique Idempotency-Key header for each event and reuse it on retries, so retrying a setup test after activation cannot start the routine.", 'Example JSON body: {"event":"deployment.completed","environment":"production"}', "To check the connection, send a test event from the app or perform the action that triggers a delivery.", ]; return [ ...common, ...auth, "Open Check connection in Paperclip to see whether the event arrived and authentication passed.", ...(setupPending ? [ "During setup, deliveries only test the connection. They do not start the routine or create a task.", "Finish setup in Paperclip to enable this webhook for future events. Test events are not replayed.", ] : [ "This webhook is enabled. Deliveries can start the routine and create tasks.", ]), "Store the key securely; do not put it in source control or logs.", ].join("\n"); } export function describeSchedule(draft: TriggerDraft) { return `${draft.frequency === "daily" ? "Every day" : draft.frequency === "weekly" ? `Every ${draft.weekday}` : "Every weekday"} at ${draft.time}`; } export function RoutineTriggerWizard({ initialDraft, onSaveExit, onFinish, onCreateWebhook, onRotateKey, routineTitle, routineId, routineActive = true, webhookUrl = "", webhookSecret = "", checkResult = "waiting", }: { initialDraft: TriggerDraft; routineTitle: string; routineId: string; routineActive?: boolean; webhookUrl?: string; webhookSecret?: string; onCreateWebhook?: (draft: TriggerDraft) => Promise; onRotateKey?: () => Promise; onSaveExit: (draft: TriggerDraft) => void | Promise; onFinish: (draft: TriggerDraft) => void | Promise; checkResult?: "waiting" | "received" | "rejected" | "no_event"; }) { const [draft, setDraft] = useState(initialDraft); const [busy, setBusy] = useState(false); const [saveError, setSaveError] = useState(""); const { setBreadcrumbs } = useBreadcrumbs(); const perform = useCallback( async (action: () => void | Promise) => { if (busy) return; setBusy(true); setSaveError(""); try { await action(); } catch (error) { setSaveError( error instanceof Error ? error.message : "Couldn’t save. Please try again.", ); } finally { setBusy(false); } }, [busy], ); const saveAndExit = useCallback(() => { void perform(() => onSaveExit(draft)); }, [draft, onSaveExit, perform]); useEffect(() => { setBreadcrumbs([ { label: routineTitle, href: `/routines/${routineId}/triggers`, onClick: (event) => { if ( event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) return; event.preventDefault(); saveAndExit(); }, }, { label: "Add trigger" }, ]); }, [saveAndExit, setBreadcrumbs, routineTitle, routineId]); const schedule = draft.kind === "schedule"; const github = draft.sender === "github"; const labels = schedule ? ["Choose trigger", "Set schedule", "Review schedule"] : ["Choose trigger", "Connect your app", "Check connection"]; function patch(values: Partial) { setDraft((current) => ({ ...current, ...values })); } function advance() { void perform(async () => { if (draft.kind === "webhook" && draft.step === 0 && !draft.created) await onCreateWebhook?.(draft); const step = draft.step + 1; patch({ step, availableStep: Math.max(draft.availableStep, step), created: draft.created || (draft.kind === "webhook" && draft.step === 0), }); }); } const title = draft.step === 0 ? "When should this routine run?" : schedule ? draft.step === 1 ? "Set a schedule" : "Review your schedule" : draft.step === 1 ? `Connect ${github ? "GitHub" : "your app"}` : "Check your connection"; const subtitle = draft.step === 0 ? `Choose how to start “${routineTitle}”. You can add another trigger later.` : schedule ? draft.step === 1 ? "Choose when Paperclip should start this routine automatically." : "This schedule starts the routine automatically. You can pause or change it later." : draft.step === 1 ? "Copy these details into the sending app, then save its webhook settings." : "Test that events arrive and authentication works. This won’t start the routine."; const selectClass = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm"; const goBack = ( ); return (
patch({ step })} />

{title}

{subtitle}

{!schedule && draft.step > 0 && } {draft.step === 0 && (
Trigger type {( [ { kind: "schedule", label: "On a schedule", detail: "Every day, on weekdays, or once a week.", Icon: CalendarClock, }, { kind: "webhook", label: "When another app sends a webhook", detail: "When something happens in GitHub, another app, or a script.", Icon: Webhook, }, ] as const ).map(({ kind, label, detail, Icon }) => ( ))}
)} {schedule && draft.step === 1 && (
patch({ time: event.target.value })} />
{draft.frequency === "weekly" && (
)}

The time follows this zone, including daylight saving changes.

)} {schedule && draft.step === 2 && (

{describeSchedule(draft)}

{draft.timezone}

Each scheduled run creates a task for the routine’s assigned agent. Any existing webhook triggers will continue to work.

)} {draft.step === 0 && draft.kind === "webhook" && (
What’s sending the webhook?
{( [ { sender: "custom", label: "Another app or script", Icon: Globe, }, { sender: "github", label: "GitHub", Icon: GitBranch }, ] as const ).map(({ sender, label, Icon }) => ( ))}
)} {draft.kind === "webhook" && draft.step === 0 && (

Public services need a publicly reachable HTTPS webhook URL.

)} {!schedule && draft.step === 1 && (
{webhookSecret && ( )} {!github && draft.signingMode !== "bearer" && (

Paste this key into your app’s signing secret field. {draft.signingMode !== "fireflies_hmac" && <> {" "}If your app uses custom headers instead, set Authorization to Bearer followed by a space and this key. }

)} {webhookSecret ? ( ) : (

The key is hidden after leaving setup. If you haven’t saved it in your app, generate a replacement.

)}
)} {!schedule && draft.step === 2 && (

Connection test only

Events received during setup won’t start the routine or create tasks.

Send an event from {github ? "GitHub" : "your app"}

{github ? "Open this webhook in your repository settings. Under Recent Deliveries, choose Redeliver on an event." : "Look for “Send test” in your app’s webhook settings. If it doesn’t have one, do the action that should trigger the webhook—for example, complete a deployment."}

Keep this page open to see the test result.

{checkResult === "received" ? ( ) : checkResult === "rejected" ? ( ) : ( )}

{checkResult === "received" ? "Test event received · Connection working" : checkResult === "rejected" ? "Event arrived, but the key was rejected" : checkResult === "no_event" ? "No event received yet" : "Waiting for an event from your app…"}

{checkResult === "received" ? "Authentication passed. No routine run or task was created." : checkResult === "rejected" ? "Go back to Connect your app, update the key in your sending app, and resend. No task was created." : "Waiting to verify delivery and authentication. The routine is not running."}

Troubleshoot delivery

Check that the webhook is enabled in your sending app and that its URL matches. Scripts must send POST with a JSON body.

)} {!schedule && draft.step === 2 && (

{routineActive ? "Finish setup to enable this webhook. Future events will start the routine; this test event won’t be replayed." : "Finish setup to save this webhook. The routine is paused; enable its automatic triggers when you’re ready. This test event won’t be replayed."}

)} {schedule && draft.step === 2 && !routineActive && (

The routine is paused. Enable its automatic triggers when you’re ready to use this schedule.

)} {saveError && (

{saveError}

)} {draft.step > 0 && goBack} {draft.step === 0 ? ( ) : schedule ? ( draft.step === 1 ? ( ) : ( ) ) : draft.step === 1 ? ( ) : ( )}
); }