"use client"; import React, { Suspense, useCallback, useEffect, useRef, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter, useSearchParams } from "next/navigation"; import { apiFetch, API_BASE, resolveToApiOrigin } from "@/lib/api"; import { useSiteSettings } from "@/contexts/SiteSettingsContext"; import { useDismissingState } from "@/hooks/useDismissingState"; import { Building2, Palette, Bell, Mail, Scale, MessageCircle, type LucideIcon } from "lucide-react"; import { ColorPickerField } from "@/components/admin/ColorPickerField"; import { BrandingPreviewPanel } from "@/components/admin/BrandingPreviewPanel"; import { extractDominantColors } from "@/lib/extractColors"; import { mapsSearchUrl } from "@/lib/maps"; type TabId = "organisation" | "branding" | "notifications" | "email" | "legal" | "whatsapp"; const TABS: { id: TabId; label: string; icon: LucideIcon }[] = [ { id: "organisation", label: "Organisation", icon: Building2 }, { id: "branding", label: "Branding", icon: Palette }, { id: "notifications", label: "Notifications", icon: Bell }, { id: "email", label: "Email", icon: Mail }, { id: "legal", label: "Legal", icon: Scale }, { id: "whatsapp", label: "WhatsApp", icon: MessageCircle }, ]; const inputCls = "w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400"; function Field({ label, hint, required, children, }: { label: string; hint?: string; required?: boolean; children: React.ReactNode; }) { return (
{children} {hint &&

{hint}

}
); } function SaveBar({ saving, onSave, result, }: { saving: boolean; onSave: () => void; result: { ok: boolean; message: string } | null; }) { return (
{result ? ( {result.ok ? "✓" : "✗"} {result.message} ) : ( )}
); } export default function SiteSettingsPage() { return ( Loading settings…}> ); } function SiteSettingsPageInner() { const { token } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const { reload: reloadSettings } = useSiteSettings(); const initialTab = TABS.some(t => t.id === searchParams.get("tab")) ? (searchParams.get("tab") as TabId) : "organisation"; const [activeTab, setActiveTab] = useState(initialTab); const [loadingInitial, setLoadingInitial] = useState(true); // Per-tab save state const [saving, setSaving] = useState(false); const [result, setResult] = useDismissingState<{ ok: boolean; message: string } | null>(null); // ── Organisation ── const [orgName, setOrgName] = useState(""); const [orgTagline, setOrgTagline] = useState(""); const [orgEmail, setOrgEmail] = useState(""); const [orgPhone, setOrgPhone] = useState(""); const [orgAddress, setOrgAddress] = useState(""); const [appBaseUrl, setAppBaseUrl] = useState(""); // ── Branding ── const [primaryColor, setPrimaryColor] = useState("#4F46E5"); const [secondaryColor, setSecondaryColor] = useState("#8B5CF6"); const [accentColor, setAccentColor] = useState("#EC4899"); const [logoUrl, setLogoUrl] = useState(""); const [logoFile, setLogoFile] = useState(null); const [logoPreview, setLogoPreview] = useState(null); const [suggestedColors, setSuggestedColors] = useState([]); const fileRef = useRef(null); const [faviconUrl, setFaviconUrl] = useState(""); const [faviconFile, setFaviconFile] = useState(null); const [faviconPreview, setFaviconPreview] = useState(null); const faviconRef = useRef(null); // ── Notifications ── const [notifEmails, setNotifEmails] = useState(""); // ── SMTP ── const [smtpHost, setSmtpHost] = useState(""); const [smtpPort, setSmtpPort] = useState("587"); const [smtpSecure, setSmtpSecure] = useState(false); const [smtpFrom, setSmtpFrom] = useState(""); const [smtpUser, setSmtpUser] = useState(""); const [smtpPass, setSmtpPass] = useState(""); const [smtpPassSet, setSmtpPassSet] = useState(false); const [smtpTesting, setSmtpTesting] = useState(false); const [smtpTestResult, setSmtpTestResult] = useDismissingState<{ ok: boolean; message: string; raw?: string } | null>(null); // ── Legal ── const [legalOperatorName, setLegalOperatorName] = useState(""); const [legalIoName, setLegalIoName] = useState(""); const [legalIoEmail, setLegalIoEmail] = useState(""); const [legalWebsiteUrl, setLegalWebsiteUrl] = useState(""); const [legalEffectiveDate, setLegalEffectiveDate] = useState(""); // ── Load all settings once ──────────────────────────────────────────────── useEffect(() => { if (!token) return; apiFetch>("/api/settings/all", { authToken: token }) .then((s) => { setOrgName(s.org_name || ""); setOrgTagline(s.org_tagline || ""); setOrgEmail(s.org_email || ""); setOrgPhone(s.org_phone || ""); setOrgAddress(s.org_address || ""); setAppBaseUrl(s.app_base_url || ""); // primary_color falls back to the legacy accent_color value (which used // to be "the one brand color" before this 3-color system existed). // accent_color is only trusted as the NEW tertiary color once // primary_color already exists — otherwise it's still doing legacy- // primary duty and must not also seed the Accent field. setPrimaryColor(s.primary_color || s.accent_color || "#4F46E5"); setSecondaryColor(s.secondary_color || "#8B5CF6"); setAccentColor(s.primary_color ? (s.accent_color || "#EC4899") : "#EC4899"); setLogoUrl(s.logo_url || ""); setFaviconUrl(s.favicon_url || ""); setNotifEmails(s.reg_notification_emails || ""); setSmtpHost(s.smtp_host || ""); setSmtpPort(s.smtp_port || "587"); setSmtpSecure((s.smtp_secure || "").toLowerCase() === "true"); setSmtpFrom(s.smtp_from || ""); setSmtpUser(s.smtp_user || ""); setSmtpPassSet(!!s.smtp_pass && s.smtp_pass !== ""); setSmtpPass(""); setLegalOperatorName(s.legal_operator_name || ""); setLegalIoName(s.legal_io_name || ""); setLegalIoEmail(s.legal_io_email || ""); setLegalWebsiteUrl(s.legal_website_url || ""); setLegalEffectiveDate(s.legal_effective_date || ""); }) .catch((e: any) => setResult({ ok: false, message: e?.message || "Failed to load settings" })) .finally(() => setLoadingInitial(false)); }, [token]); // Clear result when switching tabs const switchTab = (id: TabId) => { setActiveTab(id); setResult(null); }; // ── Save helpers ────────────────────────────────────────────────────────── const save = async (updates: Record) => { if (!token) return; setSaving(true); setResult(null); try { await apiFetch("/api/settings", { method: "PUT", authToken: token, body: updates }); setResult({ ok: true, message: "Saved." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to save" }); } finally { setSaving(false); } }; const saveOrganisation = () => save({ org_name: orgName.trim(), org_tagline: orgTagline.trim(), org_email: orgEmail.trim(), org_phone: orgPhone.trim(), org_address: orgAddress.trim(), app_base_url: appBaseUrl.trim(), }); const uploadBrandingAsset = async (file: File, kind: "logo" | "favicon"): Promise => { const fd = new FormData(); fd.append("image", file); const res = await fetch(`${API_BASE}/api/uploads/${kind}`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: fd, }); if (!res.ok) throw new Error(`${kind === "logo" ? "Logo" : "Favicon"} upload failed`); const data = await res.json(); return data.url || ""; }; const saveBranding = async () => { if (!token) return; setSaving(true); setResult(null); try { let finalLogoUrl = logoUrl; if (logoFile) finalLogoUrl = await uploadBrandingAsset(logoFile, "logo") || logoUrl; let finalFaviconUrl = faviconUrl; if (faviconFile) finalFaviconUrl = await uploadBrandingAsset(faviconFile, "favicon") || faviconUrl; await apiFetch("/api/settings", { method: "PUT", authToken: token, body: { primary_color: primaryColor, secondary_color: secondaryColor, accent_color: accentColor, logo_url: finalLogoUrl, favicon_url: finalFaviconUrl, }, }); setLogoUrl(finalLogoUrl); setLogoFile(null); setLogoPreview(null); setFaviconUrl(finalFaviconUrl); setFaviconFile(null); setFaviconPreview(null); setSuggestedColors([]); setResult({ ok: true, message: "Saved." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to save" }); } finally { setSaving(false); } }; const resetBranding = async () => { if (!token) return; if (!confirm("Reset branding to defaults? This clears your custom colors, logo, and favicon.")) return; setSaving(true); setResult(null); try { await apiFetch("/api/settings", { method: "PUT", authToken: token, body: { primary_color: "", secondary_color: "", accent_color: "", logo_url: "", favicon_url: "" }, }); setPrimaryColor("#4F46E5"); setSecondaryColor("#8B5CF6"); setAccentColor("#EC4899"); setLogoUrl(""); setLogoFile(null); setLogoPreview(null); setFaviconUrl(""); setFaviconFile(null); setFaviconPreview(null); setSuggestedColors([]); setResult({ ok: true, message: "Reset to defaults." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to reset" }); } finally { setSaving(false); } }; const handleLogoFileChange = async (file: File | undefined) => { if (!file) return; setLogoFile(file); const preview = URL.createObjectURL(file); setLogoPreview(preview); setSuggestedColors(await extractDominantColors(preview)); }; const saveNotifications = () => save({ reg_notification_emails: notifEmails.trim() }); const saveSmtp = async () => { if (!token) return; setSaving(true); setResult(null); try { const updates: Record = { smtp_host: smtpHost.trim(), smtp_port: smtpPort.trim(), smtp_secure: smtpSecure ? "true" : "false", smtp_from: smtpFrom.trim(), smtp_user: smtpUser.trim(), }; if (smtpPass.trim() !== "") updates.smtp_pass = smtpPass.trim(); await apiFetch("/api/settings", { method: "PUT", authToken: token, body: updates }); if (smtpPass.trim() !== "") { setSmtpPassSet(true); setSmtpPass(""); } setResult({ ok: true, message: "Saved." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to save" }); } finally { setSaving(false); } }; const saveLegal = () => save({ legal_operator_name: legalOperatorName.trim(), legal_io_name: legalIoName.trim(), legal_io_email: legalIoEmail.trim(), legal_website_url: legalWebsiteUrl.trim(), legal_effective_date: legalEffectiveDate.trim(), }); const handleTestSmtp = async () => { if (!token) return; setSmtpTesting(true); setSmtpTestResult(null); try { await apiFetch("/api/settings/test-smtp", { method: "POST", authToken: token, body: { host: smtpHost.trim(), port: smtpPort.trim(), secure: smtpSecure, from: smtpFrom.trim(), user: smtpUser.trim(), pass: smtpPass.trim() !== "" ? smtpPass.trim() : "••••••••", }, }); setSmtpTestResult({ ok: true, message: "Connection successful — check your inbox for a test email." }); } catch (e: any) { setSmtpTestResult({ ok: false, message: e?.message || "SMTP test failed", raw: e?.data?.raw }); } finally { setSmtpTesting(false); } }; if (loadingInitial) return
Loading settings…
; const currentLogoSrc = logoPreview || (logoUrl ? resolveToApiOrigin(logoUrl) : null); const currentFaviconSrc = faviconPreview || (faviconUrl ? resolveToApiOrigin(faviconUrl) : null); return (
{/* Header */}

Site Settings

Configure your organisation, branding, email, and legal pages.

{/* Tab bar */}
{TABS.map((tab) => { const Icon = tab.icon; return ( ); })}
{/* ── Organisation ─────────────────────────────────────────────────── */} {activeTab === "organisation" && (

Organisation details

setOrgName(e.target.value)} /> setOrgTagline(e.target.value)} />
setOrgEmail(e.target.value)} /> setOrgPhone(e.target.value)} />
setOrgAddress(e.target.value)} /> {orgAddress.trim() && ( View on map ↗ )} setAppBaseUrl(e.target.value)} />
)} {/* ── Branding ─────────────────────────────────────────────────────── */} {activeTab === "branding" && (

Branding

{currentLogoSrc && (
{/* eslint-disable-next-line @next/next/no-img-element */} Current logo
)} handleLogoFileChange(e.target.files?.[0])} className="text-sm" /> {suggestedColors.length > 0 && (

Suggested from your logo — click a swatch to apply it

{suggestedColors.map((hex) => (
))}
)} {currentFaviconSrc && (
{/* eslint-disable-next-line @next/next/no-img-element */} Current favicon
)} { const file = e.target.files?.[0]; if (!file) return; setFaviconFile(file); setFaviconPreview(URL.createObjectURL(file)); }} className="text-sm" />
{result && ( {result.ok ? "✓" : "✗"} {result.message} )}
)} {/* ── Notifications ─────────────────────────────────────────────────── */} {activeTab === "notifications" && (

Notifications

setNotifEmails(e.target.value)} />
)} {/* ── Email / SMTP ──────────────────────────────────────────────────── */} {activeTab === "email" && (

Email

Outgoing email for tickets, payment confirmations, and account notifications. Leave blank to use server environment variables. The password is stored encrypted.

setSmtpHost(e.target.value)} /> setSmtpPort(e.target.value)} />
setSmtpSecure(e.target.checked)} className="rounded" />
setSmtpFrom(e.target.value)} />
setSmtpUser(e.target.value)} autoComplete="username" />
setSmtpPass(e.target.value)} autoComplete="new-password" /> {smtpPassSet && smtpPass === "" && ( ● saved )}
{/* Test connection */}
{smtpTestResult && ( {smtpTestResult.ok ? "✓" : "✗"} {smtpTestResult.message} )}
{smtpTestResult && !smtpTestResult.ok && smtpTestResult.raw && (
Show technical details
{smtpTestResult.raw}
)}
)} {/* ── Legal ─────────────────────────────────────────────────────────── */} {activeTab === "legal" && (

Legal

These values populate the Terms of Use and Privacy Policy pages automatically.

setLegalOperatorName(e.target.value)} /> setLegalWebsiteUrl(e.target.value)} /> setLegalEffectiveDate(e.target.value)} />

Information Officer (POPIA)

setLegalIoName(e.target.value)} /> setLegalIoEmail(e.target.value)} />
)} {/* ── WhatsApp ──────────────────────────────────────────────────────── */} {activeTab === "whatsapp" && }
{activeTab === "branding" && (
)}
); } // ─── WhatsApp tab (migrated from the old standalone /dashboard/admin/whatsapp page) ─── type WAStatus = | "WORKING" | "CONNECTED" | "SCAN_QR_CODE" | "STARTING" | "FAILED" | "STOPPED" | string; interface ConfigResponse { tokenMasked: string; instanceId: string; hasToken: boolean; hasInstance: boolean; configured: boolean; } interface StatusResponse { status: WAStatus; message?: string; } const STATUS_COLORS: Record = { WORKING: "bg-green-100 text-green-800 border-green-300", CONNECTED: "bg-green-100 text-green-800 border-green-300", SCAN_QR_CODE: "bg-yellow-100 text-yellow-800 border-yellow-300", STARTING: "bg-blue-100 text-blue-800 border-blue-300", FAILED: "bg-red-100 text-red-800 border-red-300", STOPPED: "bg-gray-100 text-gray-700 border-gray-300", }; const STATUS_ICONS: Record = { WORKING: "🟢", CONNECTED: "🟢", SCAN_QR_CODE: "📷", STARTING: "🔄", FAILED: "🔴", STOPPED: "⚫", }; const ACTIVE_STATUSES = new Set(["WORKING", "CONNECTED"]); const POLLING_STATUSES = new Set(["STARTING", "SCAN_QR_CODE", "FAILED", "STOPPED"]); function Spinner() { return ( ); } function WAAlert({ type, children }: { type: "ok" | "err" | "info"; children: React.ReactNode }) { const cls = type === "ok" ? "bg-green-50 text-green-800 border-green-200" : type === "err" ? "bg-red-50 text-red-800 border-red-200" : "bg-blue-50 text-blue-800 border-blue-200"; return
{children}
; } function StepIndicator({ step }: { step: number }) { const steps = [ { n: 1, label: "Access Token" }, { n: 2, label: "Session Instance" }, { n: 3, label: "Connected" }, ]; return (
{steps.map((s, i) => { const done = step > s.n; const current = step === s.n; return (
{done ? "✓" : s.n}
{s.label}
{i < steps.length - 1 && (
)} ); })}
); } type ButtonColor = "green" | "amber" | "blue" | "red-outline"; function ActionButton({ label, busyLabel, isBusy, disabled, color, onClick, }: { label: string; busyLabel: string; isBusy: boolean; disabled: boolean; color: ButtonColor; onClick: () => void; }) { const base = "flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50 transition-colors"; const colors: Record = { green: "bg-green-600 text-white hover:bg-green-700", amber: "bg-amber-500 text-white hover:bg-amber-600", blue: "bg-blue-600 text-white hover:bg-blue-700", "red-outline": "border border-red-600 text-red-600 hover:bg-red-50", }; return ( ); } /** `active` gates all polling — only run status/QR polling while this tab is actually visible. */ function WhatsAppTab({ active }: { active: boolean }) { const { token } = useAuth(); const [cfg, setCfg] = useState(null); const [cfgLoading, setCfgLoading] = useState(true); const step = !cfg ? 0 : !cfg.hasToken ? 1 : !cfg.hasInstance ? 2 : 3; const [inputToken, setInputToken] = useState(""); const [savingToken, setSavingToken] = useState(false); const [instanceMode, setInstanceMode] = useState<"enter" | "create">("create"); const [inputInstanceId, setInputInstanceId] = useState(""); const [savingInstance, setSavingInstance] = useState(false); const [status, setStatus] = useState(null); const [statusMsg, setStatusMsg] = useState(null); const [qrSrc, setQrSrc] = useState(null); const [pairingPhone, setPairingPhone] = useState(""); const [actionMsg, setActionMsg] = useDismissingState<{ type: "ok" | "err"; text: string } | null>(null); const [busy, setBusy] = useState(null); const fetchConfig = useCallback(async () => { if (!token) return; try { const res = await apiFetch("/api/whatsapp/config", { authToken: token }); setCfg(res); } catch { // network error — leave cfg null, tab shows loading state } finally { setCfgLoading(false); } }, [token]); useEffect(() => { if (active) fetchConfig(); }, [fetchConfig, active]); const fetchStatus = useCallback(async () => { if (!token || step !== 3) return; try { const res = await apiFetch("/api/whatsapp/status", { authToken: token }); setStatus(res.status ?? null); setStatusMsg(res.message ?? null); } catch (e: any) { await fetchConfig(); setStatus("FAILED"); setStatusMsg(null); } }, [token, step, fetchConfig]); useEffect(() => { if (active && step === 3) fetchStatus(); }, [step, fetchStatus, active]); // Auto-poll status when not stable — only while this tab is active. useEffect(() => { if (!active || step !== 3 || status === null) return; if (ACTIVE_STATUSES.has(status)) return; const id = setInterval(fetchStatus, 5_000); return () => clearInterval(id); }, [active, step, status, fetchStatus]); const fetchQr = useCallback(async () => { if (!token) return; try { const res = await apiFetch<{ qr?: string }>("/api/whatsapp/qr", { authToken: token }); if (res.qr) setQrSrc(`data:image/png;base64,${res.qr}`); } catch { setQrSrc(null); } }, [token]); useEffect(() => { if (active && status === "SCAN_QR_CODE") { fetchQr(); } else { setQrSrc(null); } }, [status, fetchQr, active]); // Auto-refresh QR every 20s while waiting — only while this tab is active. useEffect(() => { if (!active || status !== "SCAN_QR_CODE") return; const id = setInterval(fetchQr, 20_000); return () => clearInterval(id); }, [active, status, fetchQr]); const doAction = async (action: string, body?: object) => { if (!token) return; setBusy(action); setActionMsg(null); try { const res = await apiFetch(`/api/whatsapp/${action}`, { method: "POST", authToken: token, body }); setActionMsg({ type: "ok", text: res?.message || `${action} successful.` }); await fetchStatus(); await fetchConfig(); } catch (e: any) { let msg = e?.message || `${action} failed.`; try { msg = JSON.parse(msg)?.message || msg; } catch {} await fetchConfig(); if (!msg.includes("SESSION_NOT_FOUND")) { setActionMsg({ type: "err", text: msg }); } await fetchStatus(); } finally { setBusy(null); } }; const saveToken = async () => { if (!inputToken.trim()) { setActionMsg({ type: "err", text: "Please enter your WAWP access token." }); return; } setSavingToken(true); setActionMsg(null); try { await apiFetch("/api/whatsapp/config", { method: "POST", authToken: token!, body: { token: inputToken.trim(), instanceId: "" } }); setInputToken(""); await fetchConfig(); } catch (e: any) { setActionMsg({ type: "err", text: e?.message || "Failed to save token." }); } finally { setSavingToken(false); } }; const saveInstanceId = async () => { if (!inputInstanceId.trim()) { setActionMsg({ type: "err", text: "Please enter the Instance ID." }); return; } setSavingInstance(true); setActionMsg(null); try { await apiFetch("/api/whatsapp/config", { method: "POST", authToken: token!, body: { token: "", instanceId: inputInstanceId.trim() } }); setInputInstanceId(""); await fetchConfig(); } catch (e: any) { setActionMsg({ type: "err", text: e?.message || "Failed to save Instance ID." }); } finally { setSavingInstance(false); } }; const createInstance = async () => { setSavingInstance(true); setActionMsg(null); try { const res = await apiFetch("/api/whatsapp/create-instance", { method: "POST", authToken: token! }); setActionMsg({ type: "ok", text: res?.message || "Instance created." }); await fetchConfig(); } catch (e: any) { setActionMsg({ type: "err", text: e?.message || "Failed to create instance." }); } finally { setSavingInstance(false); } }; const requestPairingCode = async () => { if (!pairingPhone.trim()) { setActionMsg({ type: "err", text: "Enter your phone number first." }); return; } await doAction("request-code", { phoneNumber: pairingPhone.trim() }); }; const resetToken = async () => { if (!confirm("This will clear your saved access token. You will need to re-enter it. Continue?")) return; try { await apiFetch("/api/whatsapp/config", { method: "POST", authToken: token!, body: { token: "_clear_", instanceId: "" } }); } catch {} setCfg(prev => prev ? { ...prev, hasToken: false, hasInstance: false, configured: false, tokenMasked: "", instanceId: "" } : null); }; if (cfgLoading) { return (
Loading…
); } return (

WhatsApp integration

Powered by WAWP — used to send tickets and notifications via WhatsApp.

{actionMsg && {actionMsg.text}} {step === 1 && (

Step 1 — Enter your WAWP Access Token

Your access token is found in your WAWP account dashboard at{" "} app.wawp.net.

setInputToken(e.target.value)} onKeyDown={e => e.key === "Enter" && saveToken()} placeholder="Paste your WAWP access token" className="w-full border rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-brand-500" />
)} {step === 2 && (

Step 2 — Set Up Session Instance

Token: {cfg?.tokenMasked}

You need a WAWP session instance. Either create a brand-new one, or enter an existing Instance ID.

{instanceMode === "create" && (

Click below to create a new WAWP session. The Instance ID will be saved automatically.

)} {instanceMode === "enter" && (
setInputInstanceId(e.target.value)} onKeyDown={e => e.key === "Enter" && saveInstanceId()} placeholder="e.g. BF14B761C364" className="w-full border rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-brand-500" />
)}
)} {step === 3 && ( <>

Session Status

{status === null ? (
Fetching status…
) : (
{STATUS_ICONS[status] ?? "⚪"} {status}
)} {statusMsg &&

{statusMsg}

} {status === "FAILED" && ( The session has failed. The system will attempt to auto-restart. You can also restart manually below. )} {status && POLLING_STATUSES.has(status) && (

Auto-refreshing every 5 seconds…

)}
Token: {cfg?.tokenMasked || "—"} Instance: {cfg?.instanceId || "—"}
{status === "SCAN_QR_CODE" && (

Scan QR Code

Open WhatsApp → Linked Devices → Link a Device, then scan the code below.

{qrSrc ? ( // eslint-disable-next-line @next/next/no-img-element WhatsApp QR Code ) : (
Loading QR…
)}

QR codes expire after ~20 seconds — click Refresh QR if it stops working.

)} {status === "SCAN_QR_CODE" && (

Link by Phone Number Instead

Enter your WhatsApp number (SA format, e.g. 082 123 4567) to receive a pairing code on your phone.

setPairingPhone(e.target.value)} placeholder="082 123 4567" className="flex-1 border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" />
)}

Session Controls

doAction("start")} /> doAction("restart")} /> { if (!confirm("This will log out the linked WhatsApp account. Are you sure?")) return; doAction("logout"); }} />

Instance Management

Create a brand-new instance or permanently delete the current one. Deleting will require you to set up a new instance.

doAction("create-instance")} /> { if (!confirm("This will PERMANENTLY delete the instance. You'll need to create a new one. Are you sure?")) return; doAction("delete-instance"); }} />
Update Credentials expand ▾

Change your WAWP access token or Instance ID. Leave a field blank to keep the current value.

setInputToken(e.target.value)} placeholder="Leave blank to keep current token" className="w-full border rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-brand-500" />
setInputInstanceId(e.target.value)} placeholder="Leave blank to keep current instance" className="w-full border rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-brand-500" />
)}

WhatsApp notifications powered by{" "} WAWP . Session auto-recovers on failure; admin alert sent if recovery fails.

); }