feat(i18n): complete settings module internationalization

This commit is contained in:
Ivan Mercedes 2026-07-07 19:56:55 -04:00
parent caf0f3660e
commit 9357fd9ec9
4 changed files with 267 additions and 128 deletions

View File

@ -808,13 +808,34 @@
"close_conversation": "Close Conversation" "close_conversation": "Close Conversation"
}, },
"triggers": { "triggers": {
"new_message_received": { "label": "New Message Received", "hint": "Any incoming message" }, "new_message_received": {
"first_inbound_message": { "label": "First Message from Contact", "hint": "First time this contact ever messages you (works for manually-added contacts too)" }, "label": "New Message Received",
"keyword_match": { "label": "Keyword Match", "hint": "Message contains specific keyword(s)" }, "hint": "Any incoming message"
"new_contact_created": { "label": "New Contact Created", "hint": "When a contact is auto-created from an incoming message" }, },
"conversation_assigned": { "label": "Conversation Assigned", "hint": "When assigned to an agent" }, "first_inbound_message": {
"tag_added": { "label": "Tag Added", "hint": "When a tag is added to a contact" }, "label": "First Message from Contact",
"time_based": { "label": "Time-Based", "hint": "On a recurring schedule" } "hint": "First time this contact ever messages you (works for manually-added contacts too)"
},
"keyword_match": {
"label": "Keyword Match",
"hint": "Message contains specific keyword(s)"
},
"new_contact_created": {
"label": "New Contact Created",
"hint": "When a contact is auto-created from an incoming message"
},
"conversation_assigned": {
"label": "Conversation Assigned",
"hint": "When assigned to an agent"
},
"tag_added": {
"label": "Tag Added",
"hint": "When a tag is added to a contact"
},
"time_based": {
"label": "Time-Based",
"hint": "On a recurring schedule"
}
}, },
"trigger": "Trigger", "trigger": "Trigger",
"triggerType": "Trigger type", "triggerType": "Trigger type",
@ -1065,17 +1086,148 @@
"flow": "Flow control" "flow": "Flow control"
}, },
"nodes": { "nodes": {
"start": { "label": "Start", "blurb": "Entry point of the flow" }, "start": {
"send_message": { "label": "Send message", "blurb": "Sends a WhatsApp text message" }, "label": "Start",
"send_buttons": { "label": "Send buttons", "blurb": "Sends quick-reply buttons" }, "blurb": "Entry point of the flow"
"send_list": { "label": "Send list", "blurb": "Sends a tappable list of options" }, },
"send_media": { "label": "Send media", "blurb": "Sends an image, video, or document" }, "send_message": {
"collect_input": { "label": "Collect input", "blurb": "Asks a question, saves the reply" }, "label": "Send message",
"condition": { "label": "If / else", "blurb": "Branches on a rule" }, "blurb": "Sends a WhatsApp text message"
"set_tag": { "label": "Tag contact", "blurb": "Adds or removes a contact tag" }, },
"handoff": { "label": "Handoff to agent", "blurb": "Hands the conversation to a human" }, "send_buttons": {
"end": { "label": "End", "blurb": "Ends the flow" } "label": "Send buttons",
"blurb": "Sends quick-reply buttons"
},
"send_list": {
"label": "Send list",
"blurb": "Sends a tappable list of options"
},
"send_media": {
"label": "Send media",
"blurb": "Sends an image, video, or document"
},
"collect_input": {
"label": "Collect input",
"blurb": "Asks a question, saves the reply"
},
"condition": {
"label": "If / else",
"blurb": "Branches on a rule"
},
"set_tag": {
"label": "Tag contact",
"blurb": "Adds or removes a contact tag"
},
"handoff": {
"label": "Handoff to agent",
"blurb": "Hands the conversation to a human"
},
"end": {
"label": "End",
"blurb": "Ends the flow"
}
} }
} }
},
"Settings": {
"overview": {
"notSetup": "Not set up yet",
"connected": "Connected",
"needsReconnecting": "Needs reconnecting",
"viewTeamMembers": "View team members",
"membersCount": "{count} {count, plural, =1 {member} other {members}}",
"pendingInvites": "{count} pending {count, plural, =1 {invite} other {invites}}",
"manageTemplates": "Manage message templates",
"templatesCount": "{count} {count, plural, =1 {template} other {templates}}",
"pendingReview": "{count} pending review",
"tagsAndFields": "Tags and custom fields",
"tagsCount": "{count} {count, plural, =1 {tag} other {tags}}",
"fieldsCount": "{count} custom {count, plural, =1 {field} other {fields}}",
"appearance": "{mode} mode · {theme} accent",
"yourAccount": "Your account",
"loading": "Loading…"
},
"whatsapp": {
"title": "WhatsApp connection",
"description": "Connect your Meta WhatsApp Business API. Credentials, webhook, and setup steps all live here.",
"tokenCorrupted": "Stored token can't be decrypted",
"resetting": "Resetting...",
"resetConfig": "Reset Configuration",
"credentialsValid": "Credentials valid",
"notConnected": "Not Connected",
"connectedDesc": "Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.",
"notConnectedDesc": "Configure your Meta API credentials below to connect your WhatsApp Business account.",
"registered": "Registered — Meta will deliver events to wacrm",
"notRegistered": "Not registered — Meta will not deliver events",
"verifyWithMeta": "Verify with Meta",
"subscribedSince": "Subscribed since {date}. Click Verify with Meta if events stop arriving.",
"unknownDate": "unknown",
"lastAttemptFailed": "Last attempt failed with: ",
"retryHint": "Enter (or correct) the 2-step PIN below and click Save Configuration to retry.",
"noRegistrationHint": "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.",
"diagnosticLastRun": "Diagnostic — last run: ",
"live": "live",
"notLive": "not live",
"apiCredentialsTitle": "API Credentials",
"apiCredentialsDesc": "Enter your Meta WhatsApp Business API credentials.",
"phoneNumberId": "Phone Number ID",
"wabaId": "WhatsApp Business Account ID",
"accessToken": "Permanent Access Token",
"accessTokenPlaceholder": "Enter your access token",
"tokenHidden": "Token is hidden for security. Re-enter it to update configuration.",
"webhookVerifyToken": "Webhook Verify Token",
"webhookVerifyTokenPlaceholder": "Create a custom verify token",
"webhookVerifyTokenHint": "A custom string you create. Must match the token you set in Meta webhook settings.",
"twoStepPin": "Two-step verification PIN",
"optional": "(optional)",
"pinPlaceholder": "6-digit PIN from Meta WhatsApp Manager",
"pinHint": "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.",
"webhookTitle": "Webhook Configuration",
"webhookDesc": "Use this URL as your webhook callback in the Meta App Dashboard.",
"webhookUrl": "Webhook Callback URL",
"saving": "Saving...",
"saveConfig": "Save Configuration",
"testing": "Testing...",
"testConnection": "Test API Connection",
"setupInstructions": "Setup Instructions",
"setupInstructionsDesc": "Follow these steps to connect your WhatsApp Business API.",
"step1": "Create a Meta App",
"step1_1": "Go to developers.facebook.com",
"step1_2": "Click \"My Apps\" and then \"Create App\"",
"step1_3": "Select \"Business\" as the app type",
"step1_4": "Fill in app details and create",
"step2": "Add WhatsApp Product",
"step2_1": "In your app dashboard, click \"Add Product\"",
"step2_2": "Find \"WhatsApp\" and click \"Set Up\"",
"step2_3": "Follow the setup wizard to link your business",
"step3": "Get API Credentials",
"step3_1": "Go to WhatsApp > API Setup",
"step3_2": "Copy your <strong class=\"text-foreground\">Phone Number ID</strong>",
"step3_3": "Copy your <strong class=\"text-foreground\">WhatsApp Business Account ID</strong>",
"step3_4": "Generate a <strong class=\"text-foreground\">Permanent Access Token</strong> from Business Settings > System Users",
"step4": "Configure Webhooks",
"step4_1": "Go to WhatsApp > Configuration",
"step4_2": "Click \"Edit\" on the Webhook section",
"step4_3": "Paste the <strong class=\"text-foreground\">Webhook Callback URL</strong> from above",
"step4_4": "Enter the same <strong class=\"text-foreground\">Verify Token</strong> you set here",
"step4_5": "Subscribe to \"messages\" webhook field",
"metaDocs": "Meta WhatsApp API Documentation"
},
"sections": {
"overview": "Overview",
"profile": "Your profile",
"security": "Login & security",
"appearance": "Appearance",
"whatsapp": "WhatsApp",
"templates": "Templates",
"fields": "Fields & tags",
"deals": "Deals & currency",
"members": "Team members",
"api": "API keys"
},
"groups": {
"account": "Account",
"workspace": "Workspace"
}
} }
} }

View File

@ -2,6 +2,7 @@
import { useEffect, useState, type ReactNode } from 'react'; import { useEffect, useState, type ReactNode } from 'react';
import { ChevronRight, Loader2 } from 'lucide-react'; import { ChevronRight, Loader2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth'; import { useAuth } from '@/hooks/use-auth';
@ -38,6 +39,8 @@ export function SettingsOverview({
const { user, profile, accountId, accountRole, defaultCurrency, canManageMembers } = const { user, profile, accountId, accountRole, defaultCurrency, canManageMembers } =
useAuth(); useAuth();
const { mode, theme } = useTheme(); const { mode, theme } = useTheme();
const t = useTranslations('Settings.overview');
const tSections = useTranslations('Settings.sections');
const [counts, setCounts] = useState<OverviewCounts | null>(null); const [counts, setCounts] = useState<OverviewCounts | null>(null);
const [countsLoading, setCountsLoading] = useState(true); const [countsLoading, setCountsLoading] = useState(true);
@ -137,7 +140,7 @@ export function SettingsOverview({
}; };
}, [user?.id, accountId, canManageMembers]); }, [user?.id, accountId, canManageMembers]);
const displayName = profile?.full_name || profile?.email || 'Your account'; const displayName = profile?.full_name || profile?.email || t('yourAccount');
const initial = (profile?.full_name || profile?.email || 'U').charAt(0).toUpperCase(); const initial = (profile?.full_name || profile?.email || 'U').charAt(0).toUpperCase();
const roleMeta = accountRole ? ROLE_META[accountRole] : null; const roleMeta = accountRole ? ROLE_META[accountRole] : null;
const RoleIcon = roleMeta?.icon; const RoleIcon = roleMeta?.icon;
@ -158,14 +161,14 @@ export function SettingsOverview({
section: 'whatsapp', section: 'whatsapp',
loading: whatsappLoading, loading: whatsappLoading,
subtitle: !whatsapp?.configured ? ( subtitle: !whatsapp?.configured ? (
'Not set up yet' t('notSetup')
) : whatsapp.connected ? ( ) : whatsapp.connected ? (
<> <>
<StatusDot tone="ok" /> Connected <StatusDot tone="ok" /> {t('connected')}
</> </>
) : ( ) : (
<> <>
<StatusDot tone="muted" /> Needs reconnecting <StatusDot tone="muted" /> {t('needsReconnecting')}
</> </>
), ),
}, },
@ -174,12 +177,10 @@ export function SettingsOverview({
loading: countsLoading, loading: countsLoading,
subtitle: subtitle:
counts?.members == null counts?.members == null
? 'View team members' ? t('viewTeamMembers')
: `${counts.members} member${counts.members === 1 ? '' : 's'}${ : `${t('membersCount', { count: counts.members })}${
counts.pendingInvites counts.pendingInvites
? ` · ${counts.pendingInvites} pending invite${ ? ` · ${t('pendingInvites', { count: counts.pendingInvites })}`
counts.pendingInvites === 1 ? '' : 's'
}`
: '' : ''
}`, }`,
}, },
@ -188,10 +189,10 @@ export function SettingsOverview({
loading: countsLoading, loading: countsLoading,
subtitle: subtitle:
counts?.templates == null counts?.templates == null
? 'Manage message templates' ? t('manageTemplates')
: `${counts.templates} template${counts.templates === 1 ? '' : 's'}${ : `${t('templatesCount', { count: counts.templates })}${
counts.templatesPending counts.templatesPending
? ` · ${counts.templatesPending} pending review` ? ` · ${t('pendingReview', { count: counts.templatesPending })}`
: '' : ''
}`, }`,
}, },
@ -205,15 +206,15 @@ export function SettingsOverview({
loading: countsLoading, loading: countsLoading,
subtitle: subtitle:
counts?.tags == null && counts?.customFields == null counts?.tags == null && counts?.customFields == null
? 'Tags and custom fields' ? t('tagsAndFields')
: `${counts?.tags ?? 0} tag${counts?.tags === 1 ? '' : 's'} · ${ : `${t('tagsCount', { count: counts?.tags ?? 0 })} · ${t('fieldsCount', {
counts?.customFields ?? 0 count: counts?.customFields ?? 0,
} custom field${counts?.customFields === 1 ? '' : 's'}`, })}`,
}, },
{ {
section: 'appearance', section: 'appearance',
loading: false, loading: false,
subtitle: `${cap(mode)} mode · ${themeName} accent`, subtitle: t('appearance', { mode: cap(mode), theme: themeName }),
}, },
]; ];
@ -267,12 +268,12 @@ export function SettingsOverview({
</span> </span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block text-sm font-semibold text-foreground"> <span className="block text-sm font-semibold text-foreground">
{meta.label} {tSections(section)}
</span> </span>
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground"> <span className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
{loading ? ( {loading ? (
<> <>
<Loader2 className="size-3 animate-spin" /> Loading… <Loader2 className="size-3 animate-spin" /> {t('loading')}
</> </>
) : ( ) : (
subtitle subtitle

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useRef, type ReactNode } from 'react'; import { useEffect, useRef, type ReactNode } from 'react';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@ -30,6 +31,7 @@ export function SettingsRail({
onSelect: (section: SettingsSection) => void; onSelect: (section: SettingsSection) => void;
hints?: Partial<Record<SettingsSection, ReactNode>>; hints?: Partial<Record<SettingsSection, ReactNode>>;
}) { }) {
const t = useTranslations('Settings');
const activeRef = useRef<HTMLButtonElement>(null); const activeRef = useRef<HTMLButtonElement>(null);
// When horizontal (mobile), keep the active chip in view. On desktop // When horizontal (mobile), keep the active chip in view. On desktop
@ -64,7 +66,7 @@ export function SettingsRail({
> >
{label ? ( {label ? (
<div className="hidden px-3 pt-3.5 pb-1.5 text-[11px] font-semibold tracking-[0.09em] text-muted-foreground uppercase lg:block"> <div className="hidden px-3 pt-3.5 pb-1.5 text-[11px] font-semibold tracking-[0.09em] text-muted-foreground uppercase lg:block">
{label} {t(`groups.${group}`)}
</div> </div>
) : null} ) : null}
{items.map((s) => { {items.map((s) => {
@ -87,7 +89,7 @@ export function SettingsRail({
)} )}
> >
<Icon className="size-4 shrink-0" /> <Icon className="size-4 shrink-0" />
<span className="flex-1">{meta.label}</span> <span className="flex-1">{t(`sections.${s}`)}</span>
{hints?.[s] != null ? ( {hints?.[s] != null ? (
<span <span
className={cn( className={cn(

View File

@ -16,6 +16,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth'; import { useAuth } from '@/hooks/use-auth';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@ -36,6 +37,7 @@ type ConnectionStatus = 'connected' | 'disconnected' | 'unknown';
type ResetReason = 'token_corrupted' | 'meta_api_error' | null; type ResetReason = 'token_corrupted' | 'meta_api_error' | null;
export function WhatsAppConfig() { export function WhatsAppConfig() {
const t = useTranslations('Settings.whatsapp');
const supabase = createClient(); const supabase = createClient();
// After multi-user, whatsapp_config is one-row-per-account, not // After multi-user, whatsapp_config is one-row-per-account, not
// one-row-per-user. We pull `accountId` straight off the auth // one-row-per-user. We pull `accountId` straight off the auth
@ -373,8 +375,8 @@ export function WhatsAppConfig() {
return ( return (
<section className="animate-in fade-in-50 duration-200"> <section className="animate-in fade-in-50 duration-200">
<SettingsPanelHead <SettingsPanelHead
title="WhatsApp connection" title={t("title")}
description="Connect your Meta WhatsApp Business API. Credentials, webhook, and setup steps all live here." description={t("description")}
/> />
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="size-6 animate-spin text-primary" /> <Loader2 className="size-6 animate-spin text-primary" />
@ -388,8 +390,8 @@ export function WhatsAppConfig() {
return ( return (
<section className="animate-in fade-in-50 duration-200"> <section className="animate-in fade-in-50 duration-200">
<SettingsPanelHead <SettingsPanelHead
title="WhatsApp connection" title={t("title")}
description="Connect your Meta WhatsApp Business API. Credentials, webhook, and setup steps all live here." description={t("description")}
/> />
<div className="grid gap-6 lg:grid-cols-[1fr_380px]"> <div className="grid gap-6 lg:grid-cols-[1fr_380px]">
{/* Main config form */} {/* Main config form */}
@ -415,12 +417,12 @@ export function WhatsAppConfig() {
{resetting ? ( {resetting ? (
<> <>
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
Resetting... {t('resetting')}
</> </>
) : ( ) : (
<> <>
<RotateCcw className="size-4" /> <RotateCcw className="size-4" />
Reset Configuration {t('resetConfig')}
</> </>
)} )}
</Button> </Button>
@ -438,14 +440,14 @@ export function WhatsAppConfig() {
<XCircle className="size-4 text-red-500" /> <XCircle className="size-4 text-red-500" />
)} )}
<AlertTitle className="text-foreground mb-0"> <AlertTitle className="text-foreground mb-0">
{connectionStatus === 'connected' ? 'Credentials valid' : 'Not Connected'} {connectionStatus === 'connected' ? t('credentialsValid') : t('notConnected')}
</AlertTitle> </AlertTitle>
</div> </div>
<AlertDescription className="text-muted-foreground"> <AlertDescription className="text-muted-foreground">
{connectionStatus === 'connected' {connectionStatus === 'connected'
? 'Your access token authenticates with Meta. See Registration status below for whether webhooks are actually wired.' ? t('connectedDesc')
: statusMessage || : statusMessage ||
'Configure your Meta API credentials below to connect your WhatsApp Business account.'} t('notConnectedDesc')}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -475,8 +477,8 @@ export function WhatsAppConfig() {
} }
> >
{isRegistered {isRegistered
? 'Registered — Meta will deliver events to wacrm' ? t('registered')
: 'Not registered — Meta will not deliver events'} : t('notRegistered')}
</AlertTitle> </AlertTitle>
</div> </div>
<Button <Button
@ -491,44 +493,39 @@ export function WhatsAppConfig() {
) : ( ) : (
<Zap className="size-3.5" /> <Zap className="size-3.5" />
)} )}
Verify with Meta {t('verifyWithMeta')}
</Button> </Button>
</div> </div>
<AlertDescription className="text-muted-foreground mt-2 text-xs leading-relaxed"> <AlertDescription className="text-muted-foreground mt-2 text-xs leading-relaxed">
{isRegistered ? ( {isRegistered ? (
<> <span
Subscribed since{' '} dangerouslySetInnerHTML={{
{config.registered_at __html: t('subscribedSince', {
? new Date(config.registered_at).toLocaleString() date: config.registered_at
: 'unknown'} ? new Date(config.registered_at).toLocaleString()
. Click <strong>Verify with Meta</strong> if events : t('unknownDate'),
stop arriving. }),
</> }}
/>
) : lastRegistrationError ? ( ) : lastRegistrationError ? (
<> <>
Last attempt failed with:{' '} {t('lastAttemptFailed')}
<span className="text-red-300"> <span className="text-red-300">
&quot;{lastRegistrationError}&quot; &quot;{lastRegistrationError}&quot;
</span> </span>
. Enter (or correct) the 2-step PIN below and click . {t('retryHint')}
Save Configuration to retry.
</> </>
) : ( ) : (
<> <>{t('noRegistrationHint')}</>
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.
</>
)} )}
</AlertDescription> </AlertDescription>
{registrationProbe && ( {registrationProbe && (
<div className="mt-3 rounded border border-border bg-card/60 px-3 py-2 space-y-1.5 text-[11px]"> <div className="mt-3 rounded border border-border bg-card/60 px-3 py-2 space-y-1.5 text-[11px]">
<p className="font-medium text-foreground"> <p className="font-medium text-foreground">
Diagnostic — last run: {' '} {t('diagnosticLastRun')}
<span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}> <span className={registrationProbe.live ? 'text-emerald-400' : 'text-amber-400'}>
{registrationProbe.live ? 'live' : 'not live'} {registrationProbe.live ? t('live') : t('notLive')}
</span> </span>
</p> </p>
<ul className="space-y-0.5 text-muted-foreground"> <ul className="space-y-0.5 text-muted-foreground">
@ -560,14 +557,14 @@ export function WhatsAppConfig() {
{/* API Credentials */} {/* API Credentials */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-foreground">API Credentials</CardTitle> <CardTitle className="text-foreground">{t('apiCredentialsTitle')}</CardTitle>
<CardDescription className="text-muted-foreground"> <CardDescription className="text-muted-foreground">
Enter your Meta WhatsApp Business API credentials. {t('apiCredentialsDesc')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-muted-foreground">Phone Number ID</Label> <Label className="text-muted-foreground">{t('phoneNumberId')}</Label>
<Input <Input
placeholder="e.g. 100234567890123" placeholder="e.g. 100234567890123"
value={phoneNumberId} value={phoneNumberId}
@ -577,7 +574,7 @@ export function WhatsAppConfig() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-muted-foreground">WhatsApp Business Account ID</Label> <Label className="text-muted-foreground">{t('wabaId')}</Label>
<Input <Input
placeholder="e.g. 100234567890456" placeholder="e.g. 100234567890456"
value={wabaId} value={wabaId}
@ -587,11 +584,11 @@ export function WhatsAppConfig() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-muted-foreground">Permanent Access Token</Label> <Label className="text-muted-foreground">{t('accessToken')}</Label>
<div className="relative"> <div className="relative">
<Input <Input
type={showToken ? 'text' : 'password'} type={showToken ? 'text' : 'password'}
placeholder="Enter your access token" placeholder={t('accessTokenPlaceholder')}
value={accessToken} value={accessToken}
onChange={(e) => { onChange={(e) => {
setAccessToken(e.target.value); setAccessToken(e.target.value);
@ -615,34 +612,34 @@ export function WhatsAppConfig() {
</div> </div>
{config && !tokenEdited && ( {config && !tokenEdited && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Token is hidden for security. Re-enter it to update configuration. {t('tokenHidden')}
</p> </p>
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-muted-foreground">Webhook Verify Token</Label> <Label className="text-muted-foreground">{t('webhookVerifyToken')}</Label>
<Input <Input
placeholder="Create a custom verify token" placeholder={t('webhookVerifyTokenPlaceholder')}
value={verifyToken} value={verifyToken}
onChange={(e) => setVerifyToken(e.target.value)} onChange={(e) => setVerifyToken(e.target.value)}
className="bg-muted border-border text-foreground placeholder:text-muted-foreground" className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
A custom string you create. Must match the token you set in Meta webhook settings. {t('webhookVerifyTokenHint')}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-muted-foreground"> <Label className="text-muted-foreground">
Two-step verification PIN {t('twoStepPin')}
<span className="ml-1 text-muted-foreground">(optional)</span> <span className="ml-1 text-muted-foreground">{t('optional')}</span>
</Label> </Label>
<Input <Input
type="text" type="text"
inputMode="numeric" inputMode="numeric"
maxLength={6} maxLength={6}
placeholder="6-digit PIN from Meta WhatsApp Manager" placeholder={t('pinPlaceholder')}
value={pin} value={pin}
onChange={(e) => onChange={(e) =>
setPin(e.target.value.replace(/\D/g, '').slice(0, 6)) setPin(e.target.value.replace(/\D/g, '').slice(0, 6))
@ -650,20 +647,7 @@ export function WhatsAppConfig() {
className="bg-muted border-border text-foreground placeholder:text-muted-foreground tracking-widest" className="bg-muted border-border text-foreground placeholder:text-muted-foreground tracking-widest"
/> />
<p className="text-xs text-muted-foreground leading-relaxed"> <p className="text-xs text-muted-foreground leading-relaxed">
Needed only to wire <strong className="text-muted-foreground">inbound</strong> messages <span dangerouslySetInnerHTML={{ __html: t('pinHint') }} />
for a <strong className="text-muted-foreground">production</strong> number. Set it in{' '}
<strong className="text-muted-foreground">
Meta Business Manager → WhatsApp Accounts → Phone
Numbers → Two-step verification
</strong>
, 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).{' '}
<strong className="text-muted-foreground">Meta test numbers</strong> have no
PIN and are pre-registered — leave this blank for them.
Leaving it blank also keeps an existing registration
untouched.
</p> </p>
</div> </div>
</CardContent> </CardContent>
@ -672,14 +656,14 @@ export function WhatsAppConfig() {
{/* Webhook URL */} {/* Webhook URL */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-foreground">Webhook Configuration</CardTitle> <CardTitle className="text-foreground">{t('webhookTitle')}</CardTitle>
<CardDescription className="text-muted-foreground"> <CardDescription className="text-muted-foreground">
Use this URL as your webhook callback in the Meta App Dashboard. {t('webhookDesc')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-muted-foreground">Webhook Callback URL</Label> <Label className="text-muted-foreground">{t('webhookUrl')}</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
readOnly readOnly
@ -709,10 +693,10 @@ export function WhatsAppConfig() {
{saving ? ( {saving ? (
<> <>
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
Saving... {t('saving')}
</> </>
) : ( ) : (
'Save Configuration' t('saveConfig')
)} )}
</Button> </Button>
<Button <Button
@ -724,12 +708,12 @@ export function WhatsAppConfig() {
{testing ? ( {testing ? (
<> <>
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
Testing... {t('testing')}
</> </>
) : ( ) : (
<> <>
<Zap className="size-4" /> <Zap className="size-4" />
Test API Connection {t('testConnection')}
</> </>
)} )}
</Button> </Button>
@ -743,12 +727,12 @@ export function WhatsAppConfig() {
{resetting ? ( {resetting ? (
<> <>
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
Resetting... {t('resetting')}
</> </>
) : ( ) : (
<> <>
<RotateCcw className="size-4" /> <RotateCcw className="size-4" />
Reset Configuration {t('resetConfig')}
</> </>
)} )}
</Button> </Button>
@ -760,9 +744,9 @@ export function WhatsAppConfig() {
<div> <div>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-foreground text-base">Setup Instructions</CardTitle> <CardTitle className="text-foreground text-base">{t('setupInstructions')}</CardTitle>
<CardDescription className="text-muted-foreground"> <CardDescription className="text-muted-foreground">
Follow these steps to connect your WhatsApp Business API. {t('setupInstructionsDesc')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -771,15 +755,15 @@ export function WhatsAppConfig() {
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">1</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">1</span>
Create a Meta App {t('step1')}
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-muted-foreground"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to <span className="text-primary">developers.facebook.com</span></li> <li dangerouslySetInnerHTML={{ __html: t('step1_1') }} />
<li>Click &quot;My Apps&quot; and then &quot;Create App&quot;</li> <li>{t('step1_2')}</li>
<li>Select &quot;Business&quot; as the app type</li> <li>{t('step1_3')}</li>
<li>Fill in app details and create</li> <li>{t('step1_4')}</li>
</ol> </ol>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@ -788,14 +772,14 @@ export function WhatsAppConfig() {
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">2</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">2</span>
Add WhatsApp Product {t('step2')}
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-muted-foreground"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>In your app dashboard, click &quot;Add Product&quot;</li> <li>{t('step2_1')}</li>
<li>Find &quot;WhatsApp&quot; and click &quot;Set Up&quot;</li> <li>{t('step2_2')}</li>
<li>Follow the setup wizard to link your business</li> <li>{t('step2_3')}</li>
</ol> </ol>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@ -804,15 +788,15 @@ export function WhatsAppConfig() {
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">3</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">3</span>
Get API Credentials {t('step3')}
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-muted-foreground"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to WhatsApp &gt; API Setup</li> <li>{t('step3_1')}</li>
<li>Copy your <strong className="text-foreground">Phone Number ID</strong></li> <li dangerouslySetInnerHTML={{ __html: t('step3_2') }} />
<li>Copy your <strong className="text-foreground">WhatsApp Business Account ID</strong></li> <li dangerouslySetInnerHTML={{ __html: t('step3_3') }} />
<li>Generate a <strong className="text-foreground">Permanent Access Token</strong> from Business Settings &gt; System Users</li> <li dangerouslySetInnerHTML={{ __html: t('step3_4') }} />
</ol> </ol>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@ -821,16 +805,16 @@ export function WhatsAppConfig() {
<AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline"> <AccordionTrigger className="text-muted-foreground hover:text-foreground hover:no-underline">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">4</span> <span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground">4</span>
Configure Webhooks {t('step4')}
</span> </span>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="text-muted-foreground"> <AccordionContent className="text-muted-foreground">
<ol className="list-decimal list-inside space-y-1 text-sm"> <ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to WhatsApp &gt; Configuration</li> <li>{t('step4_1')}</li>
<li>Click &quot;Edit&quot; on the Webhook section</li> <li>{t('step4_2')}</li>
<li>Paste the <strong className="text-foreground">Webhook Callback URL</strong> from above</li> <li dangerouslySetInnerHTML={{ __html: t('step4_3') }} />
<li>Enter the same <strong className="text-foreground">Verify Token</strong> you set here</li> <li dangerouslySetInnerHTML={{ __html: t('step4_4') }} />
<li>Subscribe to &quot;messages&quot; webhook field</li> <li>{t('step4_5')}</li>
</ol> </ol>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@ -844,7 +828,7 @@ export function WhatsAppConfig() {
className="inline-flex items-center gap-1.5 text-sm text-primary hover:text-primary/80 transition-colors" className="inline-flex items-center gap-1.5 text-sm text-primary hover:text-primary/80 transition-colors"
> >
<ExternalLink className="size-3.5" /> <ExternalLink className="size-3.5" />
Meta WhatsApp API Documentation {t('metaDocs')}
</a> </a>
</div> </div>
</CardContent> </CardContent>