"use client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter } from "next/navigation"; import { apiFetch } from "@/lib/api"; // ─── Types ──────────────────────────────────────────────────────────────────── 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; } // ─── Helpers ────────────────────────────────────────────────────────────────── 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 Alert({ 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}
); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function WhatsAppAdminPage() { const { user, loading, token } = useAuth(); const router = useRouter(); const isAdmin = useMemo(() => user?.role === "admin", [user]); useEffect(() => { if (loading) return; if (!user || !isAdmin) router.replace("/dashboard"); }, [user, loading, isAdmin, router]); // ── Config state (drives wizard steps) ────────────────────────────────────── const [cfg, setCfg] = useState(null); const [cfgLoading, setCfgLoading] = useState(true); // Derived wizard step: 1 = no token, 2 = token but no instance, 3 = fully configured const step = !cfg ? 0 : !cfg.hasToken ? 1 : !cfg.hasInstance ? 2 : 3; // ── Step 1 inputs ──────────────────────────────────────────────────────────── const [inputToken, setInputToken] = useState(""); const [savingToken, setSavingToken] = useState(false); // ── Step 2 inputs ──────────────────────────────────────────────────────────── const [instanceMode, setInstanceMode] = useState<"enter" | "create">("create"); const [inputInstanceId, setInputInstanceId] = useState(""); const [savingInstance, setSavingInstance] = useState(false); // ── Step 3: session state ──────────────────────────────────────────────────── const [status, setStatus] = useState(null); const [statusMsg, setStatusMsg] = useState(null); const [qrSrc, setQrSrc] = useState(null); const [pairingPhone, setPairingPhone] = useState(""); // ── Shared action feedback ─────────────────────────────────────────────────── const [actionMsg, setActionMsg] = useState<{ type: "ok" | "err"; text: string } | null>(null); const [busy, setBusy] = useState(null); // ── Load config ────────────────────────────────────────────────────────────── 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, user sees loading state } finally { setCfgLoading(false); } }, [token]); useEffect(() => { fetchConfig(); }, [fetchConfig]); // ── Status fetch (step 3 only) ─────────────────────────────────────────────── 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) { // Re-fetch config — if the session was not found, backend clears the // instance ID and the step recomputes to 2 (Session Instance setup). await fetchConfig(); setStatus("FAILED"); setStatusMsg(null); } }, [token, step, fetchConfig]); useEffect(() => { if (step === 3) fetchStatus(); }, [step, fetchStatus]); // Auto-poll status when not stable useEffect(() => { if (step !== 3 || status === null) return; if (ACTIVE_STATUSES.has(status)) return; const id = setInterval(fetchStatus, 5_000); return () => clearInterval(id); }, [step, status, fetchStatus]); // ── QR fetch ───────────────────────────────────────────────────────────────── 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 (status === "SCAN_QR_CODE") { fetchQr(); } else { setQrSrc(null); } }, [status, fetchQr]); // Auto-refresh QR every 20s while waiting useEffect(() => { if (status !== "SCAN_QR_CODE") return; const id = setInterval(fetchQr, 20_000); return () => clearInterval(id); }, [status, fetchQr]); // ── Generic session action ─────────────────────────────────────────────────── 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 {} // SESSION_NOT_FOUND: backend cleared the instance ID — re-fetch config so // the wizard steps back to Step 2; no need to show an error message. await fetchConfig(); if (!msg.includes("SESSION_NOT_FOUND")) { setActionMsg({ type: "err", text: msg }); } await fetchStatus(); } finally { setBusy(null); } }; // ─── Step 1: Save token ────────────────────────────────────────────────────── 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); } }; // ─── Step 2: Enter existing instance ID ────────────────────────────────────── 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() }, // token left blank → backend keeps existing token }); setInputInstanceId(""); await fetchConfig(); } catch (e: any) { setActionMsg({ type: "err", text: e?.message || "Failed to save Instance ID." }); } finally { setSavingInstance(false); } }; // ─── Step 2: Create new instance ───────────────────────────────────────────── 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); } }; // ─── Pairing code ──────────────────────────────────────────────────────────── const requestPairingCode = async () => { if (!pairingPhone.trim()) { setActionMsg({ type: "err", text: "Enter your phone number first." }); return; } await doAction("request-code", { phoneNumber: pairingPhone.trim() }); }; // ─── Reset credentials (go back to step 1) ─────────────────────────────────── 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 {} // Force a re-read — even if the above fails, clear local state setCfg(prev => prev ? { ...prev, hasToken: false, hasInstance: false, configured: false, tokenMasked: "", instanceId: "" } : null); }; // ──────────────────────────────────────────────────────────────────────────── // Render // ──────────────────────────────────────────────────────────────────────────── if (loading || cfgLoading) { return (
Loading…
); } return (
{/* Back button */} {/* Header */}
💬

WhatsApp Integration

Powered by WAWP

{/* Step indicator */} {/* Global action message */} {actionMsg && ( {actionMsg.text} )} {/* ── STEP 1: Enter access token ──────────────────────────────────────── */} {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-indigo-500" />
)} {/* ── STEP 2: Instance ID ─────────────────────────────────────────────── */} {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.

{/* Tab toggle */}
{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-indigo-500" />
)}
)} {/* ── STEP 3: Full management ─────────────────────────────────────────── */} {step === 3 && ( <> {/* Status card */}

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…

)} {/* Config info strip */}
Token: {cfg?.tokenMasked || "—"} Instance: {cfg?.instanceId || "—"}
{/* QR Code */} {status === "SCAN_QR_CODE" && (

Scan QR Code

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

{qrSrc ? ( WhatsApp QR Code ) : (
Loading QR…
)}

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

)} {/* Pairing code */} {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-indigo-500" />
)} {/* Session controls */}

Session Controls

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

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 */}
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-indigo-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-indigo-500" />
)}

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

); } // ─── Sub-components ─────────────────────────────────────────────────────────── 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 ( ); }