/* global React, Icon */ const { useState, useEffect } = React; /* ============================================================ WHATSAPP BUSINESS CLOUD API — Settings panel Shown inside Settings → Channels. Mirrors GHL's WhatsApp page: setup wizard, status, profile, templates. ============================================================ */ function WhatsAppCloudSettings() { const [status, setStatus] = useState(null); // { connected, channel? } const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const load = () => { fetch('/api/whatsapp-cloud/status', { credentials: 'include' }) .then((r) => (r.ok ? r.json() : Promise.reject(r.statusText))) .then(setStatus) .catch((e) => setError(String(e))); }; useEffect(() => { load(); }, []); return (

WhatsApp Business Cloud API

Connect your WhatsApp number through Meta's official API. Required for templates, broadcasts, and 24h-window replies.

{status === null && !error && (
Loading…
)} {error && (
Failed to load: {error}
)} {status && !status.connected && ( )} {status && status.connected && ( )}
); } /* ============================================================ SETUP WIZARD — paste creds → list phones → pick → connect ============================================================ */ function WhatsAppCloudWizard({ onDone }) { const [step, setStep] = useState(1); const [appId, setAppId] = useState(''); const [appSecret, setAppSecret] = useState(''); const [accessToken, setAccessToken] = useState(''); const [wabaId, setWabaId] = useState(''); const [phones, setPhones] = useState([]); const [selectedPhoneId, setSelectedPhoneId] = useState(''); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); const verifyAndList = async () => { setErr(''); setBusy(true); try { const r = await fetch('/api/whatsapp-cloud/verify-creds', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ app_id: appId, app_secret: appSecret, access_token: accessToken, waba_id: wabaId }), }); const d = await r.json(); if (!d.ok) { setErr(d.error || 'Verification failed'); return; } setPhones(d.phones || []); setStep(2); } catch (e) { setErr(String(e)); } finally { setBusy(false); } }; const connect = async () => { setErr(''); setBusy(true); try { const r = await fetch('/api/whatsapp-cloud/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ app_id: appId, app_secret: appSecret, access_token: accessToken, waba_id: wabaId, phone_number_id: selectedPhoneId, }), }); const d = await r.json(); if (!r.ok || !d.ok) { setErr(d.detail || 'Connect failed'); return; } onDone(); } catch (e) { setErr(String(e)); } finally { setBusy(false); } }; return (
Setup wizard
Step {step} of 2
{step === 1 && ( <>
{err && (
{err}
)}
Where do I find these?
App ID + App Secret: developers.facebook.com → your app → Settings → Basic.
System User Access Token: business.facebook.com → Business Settings → System Users → generate token with permissions whatsapp_business_messaging + whatsapp_business_management. Pick "Never" for expiry.
WABA ID: business.facebook.com → WhatsApp Accounts → click the account → ID in the URL or top header.
)} {step === 2 && ( <>
Found {phones.length} phone {phones.length === 1 ? 'number' : 'numbers'} on this WABA. Pick one to connect.
{phones.map((p) => ( ))}
{err && (
{err}
)}
)}
); } function CredField({ label, value, onChange, type = 'text', placeholder }) { return (
onChange(e.target.value)} placeholder={placeholder} className="input" style={{ fontFamily: type === 'password' ? 'var(--font-mono)' : undefined }} />
); } function QualityChip({ rating }) { const palette = { GREEN: { bg: 'rgba(34,197,94,0.12)', fg: '#22c55e', label: 'Green' }, YELLOW: { bg: 'rgba(234,179,8,0.12)', fg: '#eab308', label: 'Yellow' }, RED: { bg: 'rgba(239,68,68,0.12)', fg: '#ef4444', label: 'Red' }, UNKNOWN: { bg: 'rgba(140,140,140,0.12)', fg: '#8d8d8d', label: '—' }, }; const p = palette[rating] || palette.UNKNOWN; return ( {p.label} ); } /* ============================================================ CONNECTED STATE — status, profile, templates, disconnect ============================================================ */ function WhatsAppCloudConnected({ channel, onUpdate, busy, setBusy }) { const webhookUrl = `${window.location.origin}/api/whatsapp-cloud/webhook`; const sync = async () => { setBusy(true); try { const r = await fetch('/api/whatsapp-cloud/sync', { method: 'POST', credentials: 'include' }); const d = await r.json(); if (!d.ok) { window.alert('Sync failed'); } onUpdate(); } finally { setBusy(false); } }; const disconnect = async () => { if (!window.confirm('Disconnect WhatsApp Cloud API? Outbound messages will stop until reconnected. (Existing inbound history is kept.)')) return; setBusy(true); try { await fetch('/api/whatsapp-cloud/disconnect', { method: 'POST', credentials: 'include' }); onUpdate(); } finally { setBusy(false); } }; const copy = (text) => { navigator.clipboard.writeText(text); }; return (
{/* Top status card */}
{channel.meta_phone_display || '(unverified)'}
{channel.meta_verified_name || channel.name} {channel.meta_messaging_limit ? ` · ${channel.meta_messaging_limit.replace('TIER_', 'Tier ')}` : ''}
{channel.meta_quality_rating && }
Active
{/* Identifier row */}
Meta IDs
WABA ID
{channel.meta_waba_id}
Phone Number ID
{channel.meta_phone_number_id}
App ID
{channel.meta_app_id}
Last sync
{channel.meta_last_synced_at ? new Date(channel.meta_last_synced_at).toLocaleString() : '—'}
{/* Webhook config */}
Webhook configuration
{channel.meta_webhook_subscribed ? '● Subscribed to WABA' : '● Not subscribed'}

In Meta App → WhatsApp → Configuration, paste these into the Webhook fields:

Callback URL {webhookUrl}
Verify token {channel.meta_webhook_verify_token}
Subscribe to webhook fields: messages. Then click "Verify and Save" in Meta. After it's green, refresh status here.
{/* Profile editor */} {/* Templates */} {/* Bottom actions */}
); } function ProfileEditor({ profile, onSaved }) { const [open, setOpen] = useState(false); const [draft, setDraft] = useState({ about: profile.about || '', description: profile.description || '', address: profile.address || '', email: profile.email || '', vertical: profile.vertical || '', websites: (profile.websites || []).join('\n'), }); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); useEffect(() => { setDraft({ about: profile.about || '', description: profile.description || '', address: profile.address || '', email: profile.email || '', vertical: profile.vertical || '', websites: (profile.websites || []).join('\n'), }); }, [profile]); const save = async () => { setErr(''); setBusy(true); try { const websites = draft.websites.split('\n').map((s) => s.trim()).filter(Boolean).slice(0, 2); const body = { ...draft, websites }; const r = await fetch('/api/whatsapp-cloud/profile', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body), }); const d = await r.json(); if (!r.ok) { setErr(d.detail || 'Save failed'); return; } setOpen(false); onSaved(); } catch (e) { setErr(String(e)); } finally { setBusy(false); } }; return (
Business profile
{!open && (
{profile.about ? `"${profile.about}"` : 'No "about" text yet'} {profile.vertical ? ` · ${profile.vertical}` : ''}
)}
{open && (
setDraft({ ...draft, about: v.slice(0, 139) })} /> setDraft({ ...draft, description: v.slice(0, 256) })} /> setDraft({ ...draft, address: v })} /> setDraft({ ...draft, email: v })} />