"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, Webhook } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { PasswordInput } from "@/components/ui/password-input"; import { NativeSelect } from "@/components/ui/native-select"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { TestConnectionButton } from "@/components/settings/test-connection-button"; import { isWebhookFormDirty } from "@/components/settings/channel-form-dirty"; import { optimisticallySetChannelEnabled, persistChannelEnabled, rollbackChannelEnabled, } from "@/components/settings/notification-channel-toggle"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch"; import type { WebhookPayloadFormat } from "@/lib/notifications/types"; interface WebhookSettings { enabled: boolean; url: string; headerName: string; hasHeaderValue: boolean; format?: WebhookPayloadFormat; } export function WebhookCard({ isAuthenticated }: { isAuthenticated: boolean }) { const { t } = useTranslations(); const queryClient = useQueryClient(); const [url, setUrl] = useState(""); const [headerName, setHeaderName] = useState(""); const [headerValue, setHeaderValue] = useState(""); const [format, setFormat] = useState("generic"); const [saveMsg, setSaveMsg] = useState(null); const [saveMsgType, setSaveMsgType] = useState<"success" | "error" | null>( null, ); const { data: settings } = useQuery({ queryKey: queryKeys.settingsWebhook(), queryFn: async () => { return apiGet("/api/settings/webhook"); }, enabled: isAuthenticated, }); // React-recommended sync-from-server pattern (no setState-in-effect). const settingsKey = settings ? `${settings.url}|${settings.headerName}|${settings.format ?? "generic"}` : null; const [seededKey, setSeededKey] = useState(null); if (settingsKey && settingsKey !== seededKey) { setSeededKey(settingsKey); setUrl(settings!.url); setHeaderName(settings!.headerName); setFormat(settings!.format === "gotify" ? "gotify" : "generic"); } const dirty = isWebhookFormDirty( { url, headerName, headerValue, format }, settings, ); const save = useMutation({ mutationFn: async (enabled: boolean) => { const res = await apiFetchRaw("/api/settings/webhook", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, headerName: headerName || undefined, headerValue: headerValue || undefined, format, enabled, }), }); if (!res.ok) { const json = (await res.json()) as { error?: string; meta?: { errorCode?: string }; }; // A coded refusal (the private-origin policy) translates like the // test button's; anything else shows the server sentence. const code = json.meta?.errorCode; const key = code ? `settings.testConnection.errors.${code}` : null; const translated = key ? t(key) : null; throw new Error( translated && translated !== key ? translated : json.error || t("common.error"), ); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.settingsWebhook() }); setSaveMsg(t("settings.saved")); setSaveMsgType("success"); setHeaderValue(""); }, onError: (err: Error) => { setSaveMsg(err.message); setSaveMsgType("error"); }, }); const toggleEnabled = useMutation< void, Error, boolean, WebhookSettings | undefined >({ mutationFn: (enabled) => persistChannelEnabled( "/api/settings/webhook", enabled, t("common.error"), ), onMutate: async (enabled) => { const queryKey = queryKeys.settingsWebhook(); await queryClient.cancelQueries({ queryKey }); setSaveMsg(null); setSaveMsgType(null); return optimisticallySetChannelEnabled( queryClient, queryKey, enabled, ); }, onSuccess: () => { setSaveMsg(t("settings.saved")); setSaveMsgType("success"); }, onError: (err, _enabled, previous) => { rollbackChannelEnabled( queryClient, queryKeys.settingsWebhook(), previous, ); setSaveMsg(err.message); setSaveMsgType("error"); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: queryKeys.settingsWebhook() }); }, }); return (
toggleEnabled.mutate(checked)} disabled={save.isPending || toggleEnabled.isPending} />
{ e.preventDefault(); save.mutate(settings?.enabled ?? false); }} className="space-y-4" >
setUrl(e.target.value)} />

{t("settings.webhookUrlHint")}

setFormat(e.target.value === "gotify" ? "gotify" : "generic") } > {format === "gotify" && (

{t("settings.webhookFormatGotifyHint")}

)}
setHeaderName(e.target.value)} />
setHeaderValue(e.target.value)} />
{saveMsg && (saveMsgType === "error" || !dirty) && (

{saveMsg}

)}
); }