whatsapp integration

This commit is contained in:
debu-2 2026-10-02 12:51:46 +05:30
parent d077292bbc
commit 141ca9fda3
15 changed files with 578 additions and 1 deletions

View file

@ -18,6 +18,7 @@ import { VoicePage } from './features/voice/VoicePage'
import { TestingCenterPage } from './features/testing/TestingCenterPage'
import { LogsPage } from './features/logs/LogsPage'
import { AnalyticsPage } from './features/analytics/AnalyticsPage'
import { WhatsAppSetupPage } from './features/whatsapp/WhatsAppSetupPage'
import { UrmiOtpPage } from './features/agent/UrmiOtpPage'
import { UrmiOtpDocsPage } from './features/agent/UrmiOtpDocsPage'
import { KycPage } from './features/compliance/KycPage'
@ -53,6 +54,7 @@ export default function App() {
<Route path="message-settings" element={<MessageSettings />} />
<Route path="templates" element={<TemplatesPage />} />
<Route path="messaging" element={<ComposeMessage />} />
<Route path="whatsapp" element={<WhatsAppSetupPage />} />
<Route path="campaigns" element={<CampaignsPage />} />
<Route path="campaigns/:campaignId" element={<CampaignDetailPage />} />
<Route path="api-keys" element={<ApiKeysPage />} />

View file

@ -2,8 +2,9 @@ import { useState } from 'react'
import {
Activity, Bell, BookOpen, Bot, Boxes, ChevronDown, CircleDollarSign,
Code2, CreditCard, Cpu, FileCheck2, Gauge, KeyRound, LayoutDashboard,
LogOut, Menu, MessageSquareText, Network, Phone, Search, Settings, ShieldCheck,
LogOut, Menu, MessageSquareText, Network, Phone, Search, Settings, ShieldCheck,
SlidersHorizontal, TestTube2, Users, Webhook, X, Zap,
MessageCircle,
} from 'lucide-react'
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
import { useAuth } from '../features/auth/auth-context'
@ -28,6 +29,7 @@ const groups = [
]},
{ label: 'Communicate', links: [
{ label: 'Messaging', path: '/console/messaging', icon: MessageSquareText },
{ label: 'WhatsApp', path: '/console/whatsapp', icon: MessageCircle },
{ label: 'Campaigns', path: '/console/campaigns', icon: Zap },
{ label: 'Templates', path: '/console/templates', icon: BookOpen },
]},

View file

@ -0,0 +1,139 @@
import { Check, Copy, ExternalLink, MessageCircle } from 'lucide-react'
import { useEffect, useState } from 'react'
import { whatsappApi, type WhatsAppSetup, type WhatsAppWebhookEvent } from '../../lib/api'
function ConfigBadge({ ok, label }: { ok: boolean; label: string }) {
return (
<span className={`whatsapp-config-badge ${ok ? 'ok' : 'missing'}`}>
<i /> {label}: {ok ? 'Configured' : 'Missing'}
</span>
)
}
export function WhatsAppSetupPage() {
const [setup, setSetup] = useState<WhatsAppSetup | null>(null)
const [events, setEvents] = useState<WhatsAppWebhookEvent[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [copied, setCopied] = useState<string | null>(null)
useEffect(() => {
Promise.all([whatsappApi.setup(), whatsappApi.events()])
.then(([setupPayload, eventPayload]) => {
setSetup(setupPayload)
setEvents(eventPayload)
})
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load WhatsApp setup'))
.finally(() => setLoading(false))
}, [])
async function copyValue(key: string, value: string) {
await navigator.clipboard.writeText(value)
setCopied(key)
window.setTimeout(() => setCopied(null), 2000)
}
if (loading) return <p className="explorer-loading">Loading WhatsApp setup…</p>
return (
<div className="whatsapp-setup-page">
<div className="account-heading">
<span className="eyebrow">Communicate</span>
<h1>WhatsApp</h1>
<p>Configure Meta webhooks so Urmi can receive messages and delivery updates on behalf of your customers.</p>
</div>
{error && <p className="auth-error">{error}</p>}
{setup && (
<>
<section className="panel whatsapp-panel">
<div className="panel-heading">
<div>
<h2><MessageCircle size={18} /> Meta webhook configuration</h2>
<p>Paste these into <strong>App Dashboard → WhatsApp → Configuration → Configure Webhooks</strong></p>
</div>
<a className="ghost-button" href={setup.docs_url} target="_blank" rel="noreferrer">
Meta docs <ExternalLink size={14} />
</a>
</div>
<div className="whatsapp-config-row">
<div>
<span>Callback URL</span>
<code>{setup.webhook_url}</code>
</div>
<button className="secondary-button" onClick={() => copyValue('url', setup.webhook_url)}>
<Copy size={14} /> {copied === 'url' ? 'Copied' : 'Copy'}
</button>
</div>
<div className="whatsapp-config-row">
<div>
<span>Verify token</span>
<code>{setup.verify_token_configured ? 'Set in server env (WHATSAPP_VERIFY_TOKEN)' : 'Not configured'}</code>
<small>Use the exact same string in Meta and in your <code>.env</code> file.</small>
</div>
</div>
<div className="whatsapp-badges">
<ConfigBadge ok={setup.verify_token_configured} label="Verify token" />
<ConfigBadge ok={setup.app_secret_configured} label="App secret" />
<ConfigBadge ok={setup.access_token_configured} label="Access token" />
<ConfigBadge ok={Boolean(setup.phone_number_id)} label="Phone number ID" />
<ConfigBadge ok={Boolean(setup.business_account_id)} label="WABA ID" />
</div>
</section>
<section className="panel whatsapp-panel">
<div className="panel-heading">
<div>
<h2>Subscribe to webhook fields</h2>
<p>In Meta, subscribe to these fields after verifying the callback URL.</p>
</div>
</div>
<ul className="whatsapp-field-list">
{setup.recommended_fields.map((field) => (
<li key={field}><Check size={14} /> <code>{field}</code></li>
))}
</ul>
<p className="relay-note">
Minimum for messaging: <code>messages</code>. For a tech provider / BSP, also enable template and account fields.
</p>
</section>
<section className="panel whatsapp-panel">
<div className="panel-heading">
<div>
<h2>Recent webhook events</h2>
<p>Incoming payloads from Meta (after verification succeeds).</p>
</div>
</div>
{events.length === 0 ? (
<p className="analytics-empty">No webhook events yet. Click <strong>Verify and save</strong> in Meta to send a test.</p>
) : (
<div className="logs-table">
<div className="logs-table-head logs-cols-5">
<span>Field</span>
<span>Type</span>
<span>From</span>
<span>Status</span>
<span>Time</span>
</div>
{events.map((event) => (
<div className="logs-table-row logs-cols-5" key={event.id}>
<span>{event.field}</span>
<span>{event.event_type}</span>
<span>{event.from_wa_id || '—'}</span>
<span>{event.status || '—'}</span>
<span>{new Date(event.created_at).toLocaleString()}</span>
</div>
))}
</div>
)}
</section>
</>
)}
</div>
)
}

View file

@ -590,6 +590,23 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
@media (max-width: 980px) {
.analytics-toolbar-row, .analytics-grid, .analytics-summary-grid { grid-template-columns: 1fr; }
}
.whatsapp-setup-page { max-width: 900px; }
.whatsapp-panel { margin-bottom: 16px; }
.whatsapp-config-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 14px 0; border-bottom: 1px solid #edf0f4; }
.whatsapp-config-row span { display: block; color: #8a95a7; font-size: 10px; font-weight: 700; text-transform: uppercase; margin-bottom: 6px; }
.whatsapp-config-row code { display: block; color: #35435a; font-size: 12px; word-break: break-all; }
.whatsapp-config-row small { display: block; margin-top: 6px; color: #8a95a7; font-size: 11px; }
.whatsapp-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.whatsapp-config-badge { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; }
.whatsapp-config-badge i { width: 6px; height: 6px; border-radius: 50%; }
.whatsapp-config-badge.ok { color: #0f7a49; background: #e8f8ef; }
.whatsapp-config-badge.ok i { background: #0f7a49; }
.whatsapp-config-badge.missing { color: #8a5b00; background: #fff7df; }
.whatsapp-config-badge.missing i { background: #d78219; }
.whatsapp-field-list { margin: 0; padding: 0; list-style: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; }
.whatsapp-field-list li { display: flex; align-items: center; gap: 8px; color: #35435a; font-size: 12px; }
.whatsapp-field-list svg { color: #159768; }
@media (max-width: 860px) {
.logs-toolbar-row { grid-template-columns: 1fr; }
.logs-table-head { display: none; }

View file

@ -788,3 +788,38 @@ export const analyticsApi = {
return jsonRequest<UsageAnalytics>(`/api/v1/analytics/usage?${query.toString()}`)
},
}
export type WhatsAppSetup = {
webhook_url: string
verify_token_configured: boolean
app_secret_configured: boolean
access_token_configured: boolean
phone_number_id: string | null
business_account_id: string | null
meta_app_id: string | null
api_version: string
recommended_fields: string[]
docs_url: string
}
export type WhatsAppWebhookEvent = {
id: string
waba_id: string | null
phone_number_id: string | null
field: string
event_type: string
from_wa_id: string | null
message_id: string | null
status: string | null
created_at: string
}
export const whatsappApi = {
setup: () => jsonRequest<WhatsAppSetup>('/api/v1/whatsapp/setup'),
events: () => jsonRequest<WhatsAppWebhookEvent[]>('/api/v1/whatsapp/events'),
sendText: (payload: { to: string; body: string }) =>
jsonRequest<Record<string, unknown>>('/api/v1/whatsapp/messages/send', {
method: 'POST',
body: JSON.stringify(payload),
}),
}