This commit is contained in:
debu-2 2026-10-12 23:19:09 +05:30
parent 8726c931fa
commit 8c46d091ff
49 changed files with 1607 additions and 712 deletions

View file

@ -1,6 +1,6 @@
import { Check, ChevronLeft, Copy, ExternalLink, Loader2, MessageCircle, MoreHorizontal, Plus, RefreshCw } from 'lucide-react'
import { CustomerWhatsAppInbox } from './CustomerWhatsAppInbox'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState, type FormEvent } from 'react'
import {
whatsappApi,
type WhatsAppAccount,
@ -32,16 +32,16 @@ type FacebookSdk = {
}
const STEPS = [
'Connect or create a Meta business',
'Create or select a WhatsApp Business Account',
'Sign in to your Facebook business',
'Choose or create a WhatsApp Business account',
'Add and verify a business phone number',
'Grant Urmi access to manage messaging',
'Allow Urmi to send and receive messages',
]
const WHY = [
['Send and receive messages', 'Use the number you verify with Meta for customer conversations.'],
['Manage templates', 'Review template status and quality for each WhatsApp Business Account.'],
['Multiple numbers', 'Connect the businesses, accounts, and numbers Meta makes available to you.'],
['Send and receive messages', 'Use the number you verify with Facebook for customer conversations.'],
['Manage templates', 'Review template status and quality for each WhatsApp account.'],
['Multiple numbers', 'Connect the businesses, accounts, and numbers Facebook makes available to you.'],
['Built for businesses of all sizes', 'Each connection stays inside this Urmi workspace.'],
]
@ -138,6 +138,10 @@ export function WhatsAppSetupPage() {
const [developer, setDeveloper] = useState<WhatsAppDeveloperConfig | null>(null)
const [logs, setLogs] = useState<WhatsAppAccountLogs | null>(null)
const [copied, setCopied] = useState(false)
const [showToken, setShowToken] = useState(false)
const [tokenValue, setTokenValue] = useState('')
const [wabaId, setWabaId] = useState('')
const [phoneNumberId, setPhoneNumberId] = useState('')
const [pins, setPins] = useState<Record<string, string>>({})
const sessionRef = useRef<SignupSession | null>(null)
const codeRef = useRef<string | null>(null)
@ -257,6 +261,41 @@ export function WhatsAppSetupPage() {
reportRef.current = reportSession
finishRef.current = finishIfReady
function mergeAccounts(incoming: WhatsAppAccount[]) {
if (!incoming.length) return
setAccounts((current) => {
const byId = new Map(incoming.map((account) => [account.id, account]))
const merged = current.map((account) => byId.get(account.id) || account)
for (const account of incoming) {
if (!merged.some((item) => item.id === account.id)) merged.unshift(account)
}
return merged
})
}
async function submitToken(event: FormEvent) {
event.preventDefault()
setBusy(true)
setError('')
setNotice('')
try {
const payload = await whatsappApi.connectToken({
access_token: tokenValue.trim(),
waba_id: wabaId.trim(),
phone_number_id: phoneNumberId.trim() || null,
})
setResult(payload)
mergeAccounts(payload.accounts)
setTokenValue('')
setShowToken(false)
setPhase('result')
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'WhatsApp could not be connected')
} finally {
setBusy(false)
}
}
async function finishIfReady() {
const session = sessionRef.current
const code = codeRef.current
@ -389,18 +428,35 @@ export function WhatsAppSetupPage() {
<div className="account-heading">
<div className="wa-biz-heading">
<div>
<span className="eyebrow">Communicate</span>
<span className="eyebrow">Messages</span>
<h1>WhatsApp Business</h1>
<p>
{accounts.length
? 'Manage WhatsApp Business accounts and phone numbers connected to this Urmi workspace.'
: 'Connect and manage WhatsApp Business accounts with Urmi.'}
? 'The WhatsApp Business accounts and phone numbers on this workspace.'
: 'Connect WhatsApp Business so you can message customers from Urmi.'}
</p>
</div>
{accounts.length > 0 && phase !== 'provisioning' && (
<button className="primary-button" type="button" onClick={() => void startEmbeddedSignup()} disabled={busy}>
<Plus size={16} /> Connect account
</button>
<div className="wa-biz-heading-actions">
<button
className="secondary-button"
type="button"
disabled={busy}
onClick={() => {
if (phase === 'list' && showToken) {
setShowToken(false)
return
}
setShowToken(true)
if (phase === 'result') setPhase('list')
}}
>
{phase === 'list' && showToken ? 'Hide token setup' : 'Use a token'}
</button>
<button className="primary-button" type="button" onClick={() => void startEmbeddedSignup()} disabled={busy}>
<Plus size={16} /> Connect account
</button>
</div>
)}
</div>
</div>
@ -413,8 +469,8 @@ export function WhatsAppSetupPage() {
<div className="wa-biz-hero">
<section className="wa-biz-card">
<div className="wa-biz-mark"><MessageCircle size={22} /></div>
<h2>Set up WhatsApp with Meta</h2>
<p>Set up WhatsApp Business Platform through Meta&apos;s secure onboarding process.</p>
<h2>Connect WhatsApp</h2>
<p>We’ll walk you through Facebook’s secure setup. You stay in control of the business account.</p>
<ul>
{STEPS.map((step) => (
<li key={step}><Check size={16} /> {step}</li>
@ -422,16 +478,20 @@ export function WhatsAppSetupPage() {
</ul>
<button className="primary-button wa-biz-cta" onClick={() => void startEmbeddedSignup()} disabled={busy || !config?.ready}>
{busy ? <Loader2 size={16} className="wa-spin" /> : null}
Set up WhatsApp with Meta
Continue with Facebook
</button>
<button className="text-button wa-token-toggle" type="button" onClick={() => setShowToken((open) => !open)}>
{showToken ? 'Hide token setup' : 'Use an access token instead'}
</button>
{showToken && <TokenConnectForm embedded busy={busy} tokenValue={tokenValue} wabaId={wabaId} phoneNumberId={phoneNumberId} onToken={setTokenValue} onWaba={setWabaId} onPhone={setPhoneNumberId} onSubmit={submitToken} />}
<small>
{config?.ready
? 'Setup usually takes a few minutes. Meta decides which businesses, accounts, and numbers you can use.'
: 'Embedded Signup is not available until the Meta configuration id is set for this environment.'}
? 'Setup usually takes a few minutes. Facebook decides which businesses, accounts, and numbers you can use.'
: 'WhatsApp setup is not available until this workspace is configured.'}
</small>
{config?.ready && (
<small>
If Meta shows a blank “Sorry, something went wrong” page, add https://urmiai.com and https://urmiai.com/console/whatsapp to Valid OAuth redirect URIs, allow urmiai.com for the JavaScript SDK, and confirm configuration 1871678794201108 was created as WhatsApp Embedded Signup.
If Facebook shows a blank error page, this site still needs to be allowed in your Facebook app. Add https://urmiai.com and https://urmiai.com/console/whatsapp as return addresses, allow urmiai.com for Facebook sign-in, and confirm setup 1871678794201108 is for WhatsApp signup.
</small>
)}
</section>
@ -450,9 +510,9 @@ export function WhatsAppSetupPage() {
{phase === 'provisioning' && (
<section className="wa-biz-card wa-biz-provision">
<div className="wa-biz-mark ok"><Check size={18} /></div>
<h2>Meta authorization completed</h2>
<p>We&apos;re configuring your WhatsApp Business connection. This screen updates from the setup result, not from a placeholder status.</p>
<div className="wa-biz-wait"><Loader2 size={18} className="wa-spin" /> Checking the business, account, and phone Meta returned</div>
<h2>Facebook sign-in completed</h2>
<p>We’re finishing your WhatsApp connection. This screen updates from the result Facebook just returned.</p>
<div className="wa-biz-wait"><Loader2 size={18} className="wa-spin" /> Checking the business, account, and phone</div>
</section>
)}
@ -472,7 +532,7 @@ export function WhatsAppSetupPage() {
<div><dt>Status</dt><dd><StatusDot status={resultAccount.status} /></dd></div>
<div><dt>Messaging</dt><dd><StatusDot status={resultAccount.messaging_status} label={capabilityLabel(resultAccount.messaging_status)} /></dd></div>
<div><dt>Management</dt><dd><StatusDot status={resultAccount.management_status} label={capabilityLabel(resultAccount.management_status)} /></dd></div>
<div><dt>Webhooks</dt><dd><StatusDot status={resultAccount.webhooks_status} label={capabilityLabel(resultAccount.webhooks_status)} /></dd></div>
<div><dt>Delivery updates</dt><dd><StatusDot status={resultAccount.webhooks_status} label={capabilityLabel(resultAccount.webhooks_status)} /></dd></div>
</dl>
)}
<div className="wa-biz-actions">
@ -486,6 +546,10 @@ export function WhatsAppSetupPage() {
</section>
)}
{phase === 'list' && showToken && (
<TokenConnectForm busy={busy} tokenValue={tokenValue} wabaId={wabaId} phoneNumberId={phoneNumberId} onToken={setTokenValue} onWaba={setWabaId} onPhone={setPhoneNumberId} onSubmit={submitToken} />
)}
{phase === 'list' && (
<div className="wa-biz-list">
{accounts.map((account) => (
@ -520,7 +584,7 @@ export function WhatsAppSetupPage() {
<small>{account.primary_phone?.verified_name || account.name}</small>
</div>
<div>
<span>WABA</span>
<span>Account reference</span>
<strong>{account.waba_id_masked || '—'}</strong>
</div>
<div>
@ -566,7 +630,7 @@ export function WhatsAppSetupPage() {
<div className="wa-biz-tabs">
{(['overview', 'numbers', 'templates', 'configuration', 'logs'] as DetailTab[]).map((item) => (
<button key={item} className={tab === item ? 'active' : ''} onClick={() => setTab(item)}>
{item === 'numbers' ? 'Phone Numbers' : item === 'configuration' ? 'Configuration' : item[0].toUpperCase() + item.slice(1)}
{item === 'numbers' ? 'Phone numbers' : item === 'configuration' ? 'Delivery' : item === 'logs' ? 'History' : item[0].toUpperCase() + item.slice(1)}
</button>
))}
</div>
@ -578,7 +642,7 @@ export function WhatsAppSetupPage() {
<dl className="wa-biz-facts">
<div><dt>Business portfolio</dt><dd>{selected.business_name || '—'}</dd></div>
<div><dt>WhatsApp Business Account</dt><dd>{selected.name}</dd></div>
<div><dt>WABA</dt><dd>{selected.waba_id_masked}</dd></div>
<div><dt>Account reference</dt><dd>{selected.waba_id_masked}</dd></div>
<div><dt>Connection</dt><dd><StatusDot status={selected.status === 'connected' ? 'connected' : selected.status} label={selected.status === 'connected' ? 'Healthy' : statusLabel(selected.status)} /></dd></div>
</dl>
{selected.last_error && <p className="wa-biz-inline-error">{selected.last_error}</p>}
@ -589,7 +653,7 @@ export function WhatsAppSetupPage() {
<li>Messaging <StatusDot status={selected.messaging_status} label={capabilityLabel(selected.messaging_status)} /></li>
<li>Business management <StatusDot status={selected.management_status} label={capabilityLabel(selected.management_status)} /></li>
<li>Templates <StatusDot status={selected.templates_status} label={capabilityLabel(selected.templates_status)} /></li>
<li>Webhooks <StatusDot status={selected.webhooks_status} label={capabilityLabel(selected.webhooks_status)} /></li>
<li>Delivery updates <StatusDot status={selected.webhooks_status} label={capabilityLabel(selected.webhooks_status)} /></li>
</ul>
{!selected.usable && (
<p className="wa-biz-inline-error">Messaging stays unavailable until Urmi can verify a connected phone on this account.</p>
@ -601,15 +665,15 @@ export function WhatsAppSetupPage() {
{tab === 'numbers' && (
<section className="wa-biz-card">
<h2>Phone numbers</h2>
<p className="wa-biz-muted">Messaging stays unavailable until Meta reports this number as connected for Cloud API. Pending means the number is on the account but not registered yet. Quality stays unknown until then.</p>
{selected.phones.length === 0 && <p className="wa-biz-muted">Meta has not returned a phone number for this account yet.</p>}
<p className="wa-biz-muted">Messaging stays off until Facebook confirms this number is connected. Pending means the number is on the account but not registered yet.</p>
{selected.phones.length === 0 && <p className="wa-biz-muted">Facebook has not shared a phone number for this account yet.</p>}
<ul className="wa-biz-numbers">
{selected.phones.map((phone) => (
<li key={phone.id}>
<div>
<strong>{phone.display_phone || 'Number pending'}</strong>
<small>{phone.verified_name || 'Display name pending'}</small>
{phone.needs_otp && <small>Complete the SMS or voice code in Meta, then refresh. Log this number out of the WhatsApp app first.</small>}
{phone.needs_otp && <small>Finish the text or voice code in Facebook, then refresh. Sign this number out of the WhatsApp app first.</small>}
</div>
<StatusDot status={phone.status} />
<span>Quality: {phone.quality || '—'}</span>
@ -660,12 +724,12 @@ export function WhatsAppSetupPage() {
<section className="wa-biz-card">
<div className="wa-biz-card-head">
<div>
<h2>Developer configuration</h2>
<p>Webhook delivery for this workspace. Verification credentials stay on Urmi&apos;s servers.</p>
<h2>Message delivery</h2>
<p>Where WhatsApp sends updates for this account. The verification details stay on Urmi.</p>
</div>
{developer && (
<a className="ghost-button" href={developer.docs_url} target="_blank" rel="noreferrer">
Meta docs <ExternalLink size={14} />
WhatsApp help <ExternalLink size={14} />
</a>
)}
</div>
@ -674,7 +738,7 @@ export function WhatsAppSetupPage() {
<>
<div className="whatsapp-config-row">
<div>
<span>Callback URL</span>
<span>Update address</span>
<code>{developer.webhook_url}</code>
</div>
<button className="secondary-button" onClick={() => void copyWebhook(developer.webhook_url)}>
@ -683,18 +747,18 @@ export function WhatsAppSetupPage() {
</div>
<div className="whatsapp-config-row">
<div>
<span>Verify token</span>
<code>{developer.verify_token_configured ? 'Hidden — managed by Urmi' : 'Not configured'}</code>
<small>The verify token is not shown in the customer console.</small>
<span>Verification secret</span>
<code>{developer.verify_token_configured ? 'Hidden — managed by Urmi' : 'Not set up yet'}</code>
<small>This secret stays on Urmi and is not shown here.</small>
</div>
</div>
<p className="wa-biz-subhead">Webhook subscriptions</p>
<p className="wa-biz-subhead">Updates we receive</p>
<ul className="whatsapp-field-list">
{developer.recommended_fields.map((field) => (
<li key={field}><Check size={14} /> {field}</li>
))}
</ul>
<p className="wa-biz-subhead">Webhook status <StatusDot status={developer.webhooks_status} label={capabilityLabel(developer.webhooks_status)} /></p>
<p className="wa-biz-subhead">Delivery status <StatusDot status={developer.webhooks_status} label={capabilityLabel(developer.webhooks_status)} /></p>
</>
)}
</section>
@ -704,7 +768,7 @@ export function WhatsAppSetupPage() {
<div className="wa-biz-overview">
<section className="wa-biz-card">
<h2>Connection history</h2>
{logs === null && <p className="wa-biz-muted">Loading logs…</p>}
{logs === null && <p className="wa-biz-muted">Loading history…</p>}
{logs && logs.audits.length === 0 && <p className="wa-biz-muted">No connection events yet.</p>}
<ul className="wa-biz-log">
{logs?.audits.map((item) => (
@ -717,8 +781,8 @@ export function WhatsAppSetupPage() {
</ul>
</section>
<section className="wa-biz-card">
<h2>Recent webhook events</h2>
{logs && logs.events.length === 0 && <p className="wa-biz-muted">No webhook events for this account.</p>}
<h2>Recent delivery updates</h2>
{logs && logs.events.length === 0 && <p className="wa-biz-muted">No delivery updates for this account yet.</p>}
<ul className="wa-biz-log">
{logs?.events.map((item) => (
<li key={item.id}>
@ -755,6 +819,56 @@ export function WhatsAppSetupPage() {
)
}
function TokenConnectForm({
embedded = false,
busy,
tokenValue,
wabaId,
phoneNumberId,
onToken,
onWaba,
onPhone,
onSubmit,
}: {
embedded?: boolean
busy: boolean
tokenValue: string
wabaId: string
phoneNumberId: string
onToken: (value: string) => void
onWaba: (value: string) => void
onPhone: (value: string) => void
onSubmit: (event: FormEvent) => void
}) {
return (
<form className={embedded ? 'wa-token-embed' : 'wa-biz-card wa-token-card'} onSubmit={onSubmit}>
<h2>Use your own access token</h2>
<p>Paste the token and WhatsApp Business account ID from Meta. Urmi stores them for this workspace and does not show the token again.</p>
<div className="wa-token-form">
<label className="explorer-input">
<strong>Access token</strong>
<span>A token that can manage this WhatsApp account and send messages.</span>
<input type="password" value={tokenValue} onChange={(event) => onToken(event.target.value)} autoComplete="off" required />
</label>
<label className="explorer-input">
<strong>WhatsApp Business account ID</strong>
<span>The account ID Meta shows for this business.</span>
<input value={wabaId} onChange={(event) => onWaba(event.target.value)} inputMode="numeric" required />
</label>
<label className="explorer-input">
<strong>Phone number ID</strong>
<span>Optional. Add it if you want a specific number connected with this account.</span>
<input value={phoneNumberId} onChange={(event) => onPhone(event.target.value)} inputMode="numeric" />
</label>
<button className="primary-button" type="submit" disabled={busy}>
{busy ? <Loader2 size={16} className="wa-spin" /> : null}
Save and connect
</button>
</div>
</form>
)
}
function StatusDot({ status, label }: { status: string; label?: string }) {
const tone = status === 'connected' || status === 'active' ? 'ok' : status === 'unavailable' || status === 'failed' || status === 'expired' || status === 'disconnected' ? 'bad' : 'wait'
return (