/* 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}
)}
{busy ? 'Verifying…' : 'Verify and load phones'}
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) => (
setSelectedPhoneId(p.id)}
style={{ accentColor: 'var(--accent)' }}
/>
{p.display_phone_number}
{p.verified_name || '(no display name)'}
{p.code_verification_status ? ` · ${p.code_verification_status}` : ''}
{p.quality_rating && (
)}
))}
{err && (
{err}
)}
setStep(1)} disabled={busy}>Back
{busy ? 'Connecting…' : 'Connect'}
>
)}
);
}
function CredField({ label, value, onChange, type = 'text', placeholder }) {
return (
{label}
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}
copy(webhookUrl)}>Copy
Verify token
{channel.meta_webhook_verify_token}
copy(channel.meta_webhook_verify_token)}>Copy
Subscribe to webhook fields: messages. Then click "Verify and Save" in Meta. After it's green, refresh status here.
{/* Profile editor */}
{/* Templates */}
{/* Bottom actions */}
{busy ? 'Syncing…' : 'Sync now'}
Disconnect
);
}
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}` : ''}
)}
setOpen((v) => !v)}>{open ? 'Cancel' : 'Edit'}
{open && (
setDraft({ ...draft, about: v.slice(0, 139) })} />
setDraft({ ...draft, description: v.slice(0, 256) })} />
setDraft({ ...draft, address: v })} />
setDraft({ ...draft, email: v })} />
Vertical
setDraft({ ...draft, vertical: e.target.value })}>
—
{['AUTO','BEAUTY','APPAREL','EDU','ENTERTAIN','EVENT_PLAN','FINANCE','GROCERY','GOVT','HOTEL','HEALTH','NONPROFIT','PROF_SERVICES','RETAIL','TRAVEL','RESTAURANT','OTHER'].map((v) => (
{v}
))}
Websites (max 2, one per line)
{err && (
{err}
)}
{busy ? 'Saving…' : 'Save profile'}
)}
);
}
function TemplatesPanel({ onSync, busy }) {
const [templates, setTemplates] = useState(null);
const [creating, setCreating] = useState(false);
const load = () => {
fetch('/api/whatsapp-cloud/templates', { credentials: 'include' })
.then((r) => (r.ok ? r.json() : { items: [] }))
.then((d) => setTemplates(d.items || []));
};
useEffect(() => { load(); }, []);
const STATUS_COLORS = {
APPROVED: { bg: 'rgba(34,197,94,0.12)', fg: '#22c55e' },
PENDING: { bg: 'rgba(234,179,8,0.12)', fg: '#eab308' },
REJECTED: { bg: 'rgba(239,68,68,0.12)', fg: '#ef4444' },
DISABLED: { bg: 'rgba(140,140,140,0.12)', fg: '#8d8d8d' },
PAUSED: { bg: 'rgba(140,140,140,0.12)', fg: '#8d8d8d' },
IN_APPEAL:{ bg: 'rgba(234,179,8,0.12)', fg: '#eab308' },
};
return (
Templates
{templates === null ? 'Loading…' : `${templates.length} synced from Meta`}
{ onSync(); setTimeout(load, 1500); }} disabled={busy}>
{busy ? 'Syncing…' : 'Sync templates'}
setCreating(true)}>
+ Create template
{templates && templates.length > 0 && (
{templates.map((t) => {
const s = STATUS_COLORS[t.status] || STATUS_COLORS.PENDING;
return (
{t.name}
{t.category} · {t.language}
{t.rejection_reason ? ` · ${t.rejection_reason}` : ''}
{t.status}
);
})}
)}
{templates && templates.length === 0 && (
No templates yet. Click "Create template" above. Meta reviews in 24-48h.
)}
{creating && (
setCreating(false)}
onCreated={() => { setCreating(false); load(); }}
/>
)}
);
}
function CreateTemplateModal({ onClose, onCreated }) {
const [name, setName] = useState('');
const [language, setLanguage] = useState('en');
const [category, setCategory] = useState('MARKETING');
const [headerType, setHeaderType] = useState('NONE'); // NONE | TEXT
const [headerText, setHeaderText] = useState('');
const [body, setBody] = useState('');
const [footer, setFooter] = useState('');
const [busy, setBusy] = useState(false);
const [err, setErr] = useState('');
const variableCount = (body.match(/\{\{\d+\}\}/g) || []).length;
const sortedVars = Array.from({ length: variableCount }, (_, i) => `{{${i + 1}}}`);
const sampleValues = sortedVars.map((_, i) => `Sample${i + 1}`);
const submit = async () => {
setErr('');
if (!/^[a-z0-9_]+$/.test(name)) {
setErr('Name must be lowercase letters, numbers, underscores only (e.g. discovery_call_v1)');
return;
}
if (!body.trim()) {
setErr('Body required');
return;
}
const components = [];
if (headerType === 'TEXT' && headerText.trim()) {
components.push({ type: 'HEADER', format: 'TEXT', text: headerText.trim() });
}
const bodyComponent = { type: 'BODY', text: body.trim() };
if (variableCount > 0) {
bodyComponent.example = { body_text: [sampleValues] };
}
components.push(bodyComponent);
if (footer.trim()) {
components.push({ type: 'FOOTER', text: footer.trim() });
}
setBusy(true);
try {
const r = await fetch('/api/whatsapp-cloud/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ name, language, category, components }),
});
const d = await r.json();
if (!r.ok) {
setErr(d.detail || 'Create failed');
return;
}
onCreated();
} catch (e) {
setErr(String(e));
} finally {
setBusy(false);
}
};
return (
e.stopPropagation()} style={{
background: 'var(--surface)', border: '1px solid var(--divider)',
width: 640, maxHeight: '90vh', overflowY: 'auto', padding: 24,
}}>
Create WhatsApp template
Submitted to Meta for review. APPROVED in 24-48h, then usable from the inbox.
×
Name (snake_case, no spaces)
setName(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))} placeholder="discovery_call_v1" />
Language
setLanguage(e.target.value)}>
en
en_US
ar
it
Category
setCategory(e.target.value)}>
MARKETING
UTILITY
AUTHENTICATION
Body * (use {`{{1}}`}, {`{{2}}`} for variables, max 1024 chars)
Footer (optional, 60 char max)
setFooter(e.target.value)} placeholder="Linea Group · lineagroup.ae" maxLength={60} />
Preview
{headerType === 'TEXT' && headerText && (
{headerText}
)}
{body || (body) }
{footer &&
{footer}
}
{err && (
{err}
)}
Cancel
{busy ? 'Submitting…' : 'Submit for review'}
);
}
function Label({ children }) {
return (
{children}
);
}
Object.assign(window, { WhatsAppCloudSettings });