'use client'; import { useEffect, useState, useCallback } from 'react'; import { toast } from 'sonner'; import { Eye, EyeOff, Copy, CheckCircle2, XCircle, Loader2, ExternalLink, Zap, AlertTriangle, RotateCcw, } from 'lucide-react'; import { createClient } from '@/lib/supabase/client'; import { useAuth } from '@/hooks/use-auth'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { SettingsPanelHead } from './settings-panel-head'; import { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from '@/components/ui/accordion'; import type { WhatsAppConfig as WhatsAppConfigType } from '@/types'; const MASKED_TOKEN = '••••••••••••••••'; type ConnectionStatus = 'connected' | 'disconnected' | 'unknown'; type ResetReason = 'token_corrupted' | 'meta_api_error' | null; export function WhatsAppConfig() { const supabase = createClient(); // After multi-user, whatsapp_config is one-row-per-account, not // one-row-per-user. We pull `accountId` straight off the auth // context and key every read off it — so a teammate who just // joined an account sees the inviter's saved config without // having to re-enter anything. const { user, accountId, loading: authLoading, profileLoading } = useAuth(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [resetting, setResetting] = useState(false); const [showToken, setShowToken] = useState(false); const [config, setConfig] = useState(null); const [connectionStatus, setConnectionStatus] = useState('unknown'); const [resetReason, setResetReason] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [phoneNumberId, setPhoneNumberId] = useState(''); const [wabaId, setWabaId] = useState(''); const [accessToken, setAccessToken] = useState(''); const [verifyToken, setVerifyToken] = useState(''); const [pin, setPin] = useState(''); const [tokenEdited, setTokenEdited] = useState(false); // True once /register has succeeded on Meta's side (timestamp set // in the row). When false, the saved config is metadata-only and // Meta will silently drop every inbound event — that's the // multi-number bug that prompted this work. const isRegistered = Boolean(config?.registered_at); const lastRegistrationError = config?.last_registration_error ?? null; const [verifyingRegistration, setVerifyingRegistration] = useState(false); type RegistrationProbe = { live: boolean; checks: Record; errors?: string[]; last_registration_error?: string | null; registered_at?: string | null; subscribed_apps_at?: string | null; }; const [registrationProbe, setRegistrationProbe] = useState(null); const webhookUrl = typeof window !== 'undefined' ? `${window.location.origin}/api/whatsapp/webhook` : ''; const fetchConfig = useCallback(async (acctId: string) => { setLoading(true); try { // Load form values from Supabase (shows what's in DB). // Switched from `user_id` (which would only match the row's // original author) to `account_id` so every member of the // account sees the same saved configuration. UNIQUE(account_id) // on the table guarantees the .maybeSingle() return type // remains accurate. const { data, error } = await supabase .from('whatsapp_config') .select('*') .eq('account_id', acctId) .maybeSingle(); if (error) { console.error('Failed to load config row:', error); } if (data) { setConfig(data); setPhoneNumberId(data.phone_number_id || ''); setWabaId(data.waba_id || ''); setAccessToken(MASKED_TOKEN); setVerifyToken(''); setPin(''); setTokenEdited(false); } else { setConfig(null); setPhoneNumberId(''); setWabaId(''); setAccessToken(''); setVerifyToken(''); setPin(''); setTokenEdited(false); } // Clear any stale probe result when reloading the row. setRegistrationProbe(null); // Then verify health via the API (decrypts token + pings Meta) if (data) { try { const res = await fetch('/api/whatsapp/config', { method: 'GET' }); const payload = await res.json(); if (payload.connected) { setConnectionStatus('connected'); setResetReason(null); setStatusMessage(''); } else { setConnectionStatus('disconnected'); setResetReason(payload.needs_reset ? 'token_corrupted' : payload.reason === 'meta_api_error' ? 'meta_api_error' : null); setStatusMessage(payload.message || ''); } } catch (err) { console.error('Health check failed:', err); setConnectionStatus('disconnected'); } } else { setConnectionStatus('disconnected'); setResetReason(null); setStatusMessage(''); } } catch (err) { console.error('fetchConfig error:', err); toast.error('Failed to load WhatsApp configuration'); } finally { setLoading(false); } }, [supabase]); useEffect(() => { // Need both the auth session (`!authLoading`) AND the profile // (`!profileLoading`, which carries `accountId`). Without the // second guard, the effect would fire with `accountId === null` // for the first render window and bail without ever retrying // once the profile arrives. if (authLoading || profileLoading) return; if (!user || !accountId) { setLoading(false); return; } fetchConfig(accountId); }, [authLoading, profileLoading, user, accountId, fetchConfig]); async function handleSave() { if (!phoneNumberId.trim()) { toast.error('Phone Number ID is required'); return; } if (!config && (!accessToken.trim() || !tokenEdited)) { toast.error('Access Token is required for initial setup'); return; } try { setSaving(true); // Always POST through the API — it verifies with Meta and encrypts // the access_token server-side with ENCRYPTION_KEY. Skipping this // and writing direct to Supabase stores the token in plaintext, // which then fails decryption on every subsequent health check. const payload: Record = { phone_number_id: phoneNumberId.trim(), waba_id: wabaId.trim() || null, verify_token: verifyToken.trim() || null, // Optional — only sent when the user filled it in. The server // requires it on first save or when changing numbers; for a // simple token rotation, leaving it blank skips re-register. pin: pin.trim() || null, }; if (tokenEdited && accessToken !== MASKED_TOKEN && accessToken.trim()) { payload.access_token = accessToken.trim(); } else if (config) { // Existing config — reuse stored encrypted token by decrypting on the // server. But our POST handler requires an access_token to verify // with Meta. If the user didn't change the token, we need to signal // that. Simplest: require token re-entry if they're updating. toast.error('Please re-enter the Access Token to save changes'); setSaving(false); return; } const res = await fetch('/api/whatsapp/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) { toast.error(data.error || 'Failed to save configuration'); setSaving(false); return; } // The route now returns a structured outcome: // * registered=true → number is live, events will flow // * registered=false → credentials saved but /register // failed; UI shows the specific error // and a retry path. registration_error // is human-readable from Meta. if (data.registered === false && data.registration_error) { toast.error( `Saved, but Meta couldn't register the number: ${data.registration_error}`, { duration: 12000 }, ); } else if (data.registration_skipped) { // Credentials saved + verified, but /register was skipped // because no PIN was supplied (e.g. a Meta test number). // Don't claim the number is "Live" — point at the // Registration status banner instead. toast.success( 'Credentials saved and verified. Inbound registration was skipped (no PIN) — see Registration status below.', { duration: 10000 }, ); setPin(''); } else { toast.success( data.phone_info?.verified_name ? `Live — ${data.phone_info.verified_name} can now receive events.` : 'WhatsApp connected. Events will start flowing within a minute.', ); // Clear the PIN so subsequent saves don't accidentally // re-register (which would void the active subscription if // the PIN became stale). setPin(''); } if (accountId) await fetchConfig(accountId); } catch (err) { console.error('Save error:', err); toast.error('Failed to save configuration'); } finally { setSaving(false); } } async function handleTestConnection() { try { setTesting(true); const res = await fetch('/api/whatsapp/config', { method: 'GET' }); const payload = await res.json(); if (payload.connected) { setConnectionStatus('connected'); setResetReason(null); setStatusMessage(''); toast.success( payload.phone_info?.verified_name ? `Connected to ${payload.phone_info.verified_name}` : 'API connection successful' ); } else { setConnectionStatus('disconnected'); setResetReason(payload.needs_reset ? 'token_corrupted' : payload.reason === 'meta_api_error' ? 'meta_api_error' : null); setStatusMessage(payload.message || ''); toast.error(payload.message || 'API connection failed'); } } catch (err) { console.error('Test connection error:', err); setConnectionStatus('disconnected'); toast.error('Connection test failed. Check network and try again.'); } finally { setTesting(false); } } async function handleVerifyRegistration() { setVerifyingRegistration(true); setRegistrationProbe(null); try { const res = await fetch('/api/whatsapp/config/verify-registration', { method: 'GET', }); const data = (await res.json()) as RegistrationProbe; setRegistrationProbe(data); if (data.live) { toast.success('Number is fully wired — Meta is delivering events.'); } else { toast.error( 'Number is not fully registered. See the checks below for which step failed.', { duration: 8000 }, ); } if (accountId) await fetchConfig(accountId); } catch (err) { console.error('verify-registration failed:', err); toast.error('Could not reach the verification endpoint.'); } finally { setVerifyingRegistration(false); } } async function handleReset() { if (!confirm('This will delete the current WhatsApp config so you can re-enter it. Continue?')) { return; } try { setResetting(true); const res = await fetch('/api/whatsapp/config', { method: 'DELETE' }); const data = await res.json(); if (!res.ok) { toast.error(data.error || 'Failed to reset configuration'); return; } toast.success('Configuration cleared. You can now re-enter your credentials.'); setConfig(null); setPhoneNumberId(''); setWabaId(''); setAccessToken(''); setVerifyToken(''); setTokenEdited(false); setConnectionStatus('disconnected'); setResetReason(null); setStatusMessage(''); } catch (err) { console.error('Reset error:', err); toast.error('Failed to reset configuration'); } finally { setResetting(false); } } function handleCopyWebhookUrl() { navigator.clipboard.writeText(webhookUrl); toast.success('Webhook URL copied to clipboard'); } if (loading) { return (
); } const showResetBanner = resetReason === 'token_corrupted'; return (
{/* Main config form */}
{/* Corrupted-token reset banner */} {showResetBanner && (
Stored token can't be decrypted {statusMessage}
)} {/* Connection Status */}
{connectionStatus === 'connected' ? ( ) : ( )} {connectionStatus === 'connected' ? 'Credentials valid' : 'Not Connected'}
{connectionStatus === 'connected' ? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.' : statusMessage || 'Configure your Meta API credentials below to connect your WhatsApp Business account.'}
{/* Registration Status — the "is it actually live?" check. Credentials being valid is necessary but not sufficient; without a successful /register call the number won't receive inbound events. Surface this dimension separately so users don't trust a misleading green banner. */} {config && (
{isRegistered ? ( ) : ( )} {isRegistered ? 'Registered — Meta will deliver events to wacrm' : 'Not registered — Meta will not deliver events'}
{isRegistered ? ( <> Subscribed since{' '} {config.registered_at ? new Date(config.registered_at).toLocaleString() : 'unknown'} . Click Verify with Meta if events stop arriving. ) : lastRegistrationError ? ( <> Last attempt failed with:{' '} "{lastRegistrationError}" . Enter (or correct) the 2-step PIN below and click Save Configuration to retry. ) : ( <> This number was saved before registration tracking existed, or registration was skipped. Enter the 2-step PIN below and click Save Configuration to subscribe it. )} {registrationProbe && (

Diagnostic — last run: {' '} {registrationProbe.live ? 'live' : 'not live'}

    {Object.entries(registrationProbe.checks).map(([k, v]) => (
  • {v === true ? ( ) : v === false ? ( ) : ( )} {k}
  • ))}
{(registrationProbe.errors ?? []).length > 0 && (
    {registrationProbe.errors?.map((e, i) => (
  • • {e}
  • ))}
)}
)}
)} {/* API Credentials */} API Credentials Enter your Meta WhatsApp Business API credentials.
setPhoneNumberId(e.target.value)} className="bg-muted border-border text-foreground placeholder:text-muted-foreground" />
setWabaId(e.target.value)} className="bg-muted border-border text-foreground placeholder:text-muted-foreground" />
{ setAccessToken(e.target.value); setTokenEdited(true); }} onFocus={() => { if (accessToken === MASKED_TOKEN) { setAccessToken(''); setTokenEdited(true); } }} className="bg-muted border-border text-foreground placeholder:text-muted-foreground pr-10" />
{config && !tokenEdited && (

Token is hidden for security. Re-enter it to update configuration.

)}
setVerifyToken(e.target.value)} className="bg-muted border-border text-foreground placeholder:text-muted-foreground" />

A custom string you create. Must match the token you set in Meta webhook settings.

setPin(e.target.value.replace(/\D/g, '').slice(0, 6)) } className="bg-muted border-border text-foreground placeholder:text-muted-foreground tracking-widest" />

Needed only to wire inbound messages for a production number. Set it in{' '} Meta Business Manager → WhatsApp Accounts → Phone Numbers → Two-step verification , then paste it here so wacrm can subscribe the number — otherwise Meta routes inbound events to whichever app last claimed it (the symptom that hits second numbers under a shared WABA).{' '} Meta test numbers have no PIN and are pre-registered — leave this blank for them. Leaving it blank also keeps an existing registration untouched.

{/* Webhook URL */} Webhook Configuration Use this URL as your webhook callback in the Meta App Dashboard.
{/* Action Buttons */}
{config && ( )}
{/* Setup Instructions Sidebar */}
Setup Instructions Follow these steps to connect your WhatsApp Business API. 1 Create a Meta App
  1. Go to developers.facebook.com
  2. Click "My Apps" and then "Create App"
  3. Select "Business" as the app type
  4. Fill in app details and create
2 Add WhatsApp Product
  1. In your app dashboard, click "Add Product"
  2. Find "WhatsApp" and click "Set Up"
  3. Follow the setup wizard to link your business
3 Get API Credentials
  1. Go to WhatsApp > API Setup
  2. Copy your Phone Number ID
  3. Copy your WhatsApp Business Account ID
  4. Generate a Permanent Access Token from Business Settings > System Users
4 Configure Webhooks
  1. Go to WhatsApp > Configuration
  2. Click "Edit" on the Webhook section
  3. Paste the Webhook Callback URL from above
  4. Enter the same Verify Token you set here
  5. Subscribe to "messages" webhook field
); }