- {statusKey === 'APPROVED' && (
-
- )}
- {(statusKey === 'REJECTED' || statusKey === 'PAUSED') && (
-
- )}
+
+ {canManageTemplates && (
+ <>
+
+ {statusKey === 'APPROVED' && (
+
+ )}
+ {(statusKey === 'REJECTED' || statusKey === 'PAUSED') && (
+
+ )}
+
+ >
+ )}
@@ -634,6 +707,7 @@ export function TemplateManager() {
setDialogOpen(open);
if (!open) {
setEditingId(null);
+ setViewingOnly(false);
setForm(emptyForm);
}
}}
@@ -641,12 +715,18 @@ export function TemplateManager() {
- {editingId ? t('dialogEditTitle') : t('dialogNewTitle')}
+ {viewingOnly
+ ? t('dialogViewTitle')
+ : editingId
+ ? t('dialogEditTitle')
+ : t('dialogNewTitle')}
- {editingId
- ? t('dialogEditDesc')
- : t('dialogNewDesc')}
+ {viewingOnly
+ ? t('dialogViewDesc')
+ : editingId
+ ? t('dialogEditDesc')
+ : t('dialogNewDesc')}
@@ -664,7 +744,7 @@ export function TemplateManager() {
placeholder={t('namePlaceholder')}
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
- disabled={editingId !== null}
+ disabled={editingId !== null || viewingOnly}
className="bg-muted border-border text-foreground placeholder:text-muted-foreground disabled:opacity-60 disabled:cursor-not-allowed"
/>
@@ -679,6 +759,7 @@ export function TemplateManager() {
@@ -801,7 +885,7 @@ export function TemplateManager() {
{headerNeedsMedia && (
- {form.header_format === 'image' && (
+ {form.header_format === 'image' && !viewingOnly && (
headerFileRef.current?.click()}
>
{uploadingHeader ? (
@@ -839,7 +923,8 @@ export function TemplateManager() {
onChange={(e) =>
setForm({ ...form, header_media_url: e.target.value })
}
- className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
+ disabled={viewingOnly}
+ className="bg-muted border-border text-foreground placeholder:text-muted-foreground disabled:opacity-60 disabled:cursor-not-allowed"
/>
{form.header_format === 'image' && form.header_media_url && (
// eslint-disable-next-line @next/next/no-img-element
@@ -870,14 +955,15 @@ export function TemplateManager() {
onChange={(e) =>
setForm({ ...form, body_text: e.target.value })
}
+ disabled={viewingOnly}
rows={4}
maxLength={TEMPLATE_LIMITS.bodyMaxLength}
- className="bg-muted border-border text-foreground placeholder:text-muted-foreground resize-none"
+ className="bg-muted border-border text-foreground placeholder:text-muted-foreground resize-none disabled:opacity-60 disabled:cursor-not-allowed"
/>
{t('bodyHint')}
-
+
{bodyVarCount > 0 && (
-
+ {!viewingOnly && (
+
+ )}
{form.buttons.length === 0 ? (
@@ -947,6 +1037,7 @@ export function TemplateManager() {
{btn.type === 'URL' && (
updateButton(i, { url: e.target.value })
}
- className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
+ className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs disabled:opacity-60 disabled:cursor-not-allowed"
/>
{extractVariableIndices(btn.url).length > 0 && (
updateButton(i, { example: e.target.value })
}
- className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
+ className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs disabled:opacity-60 disabled:cursor-not-allowed"
/>
)}
@@ -1030,20 +1127,22 @@ export function TemplateManager() {
updateButton(i, { phone_number: e.target.value })
}
- className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
+ className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs disabled:opacity-60 disabled:cursor-not-allowed"
/>
)}
{btn.type === 'COPY_CODE' && (
updateButton(i, { example: e.target.value })
}
- className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs"
+ className="bg-muted border-border text-foreground placeholder:text-muted-foreground h-8 text-xs disabled:opacity-60 disabled:cursor-not-allowed"
/>
)}
@@ -1054,29 +1153,40 @@ export function TemplateManager() {
-
-
+ {viewingOnly ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
diff --git a/src/components/settings/whatsapp-config.tsx b/src/components/settings/whatsapp-config.tsx
index 85b7a3b819..c29cdeb357 100644
--- a/src/components/settings/whatsapp-config.tsx
+++ b/src/components/settings/whatsapp-config.tsx
@@ -39,12 +39,7 @@ type ResetReason = 'token_corrupted' | 'meta_api_error' | null;
export function WhatsAppConfig() {
const t = useTranslations('Settings.whatsapp');
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 { user, accountId, canEditSettings, loading: authLoading, profileLoading } = useAuth();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -69,6 +64,7 @@ export function WhatsAppConfig() {
const [verifyToken, setVerifyToken] = useState('');
const [pin, setPin] = useState('');
const [tokenEdited, setTokenEdited] = useState(false);
+ const [useMarketingEndpoint, setUseMarketingEndpoint] = 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
@@ -121,6 +117,7 @@ export function WhatsAppConfig() {
setVerifyToken('');
setPin('');
setTokenEdited(false);
+ setUseMarketingEndpoint(data.use_marketing_endpoint || false);
} else {
setConfig(null);
setPhoneNumberId('');
@@ -129,6 +126,7 @@ export function WhatsAppConfig() {
setVerifyToken('');
setPin('');
setTokenEdited(false);
+ setUseMarketingEndpoint(false);
}
// Clear any stale probe result when reloading the row.
setRegistrationProbe(null);
@@ -183,6 +181,7 @@ export function WhatsAppConfig() {
}, [authLoading, profileLoading, user?.id, accountId, fetchConfig]);
async function handleSave() {
+ if (!canEditSettings) return;
if (!phoneNumberId.trim()) {
toast.error('Phone Number ID is required');
return;
@@ -207,6 +206,7 @@ export function WhatsAppConfig() {
// requires it on first save or when changing numbers; for a
// simple token rotation, leaving it blank skips re-register.
pin: pin.trim() || null,
+ use_marketing_endpoint: useMarketingEndpoint,
};
if (tokenEdited && accessToken !== MASKED_TOKEN && accessToken.trim()) {
@@ -278,6 +278,7 @@ export function WhatsAppConfig() {
}
async function handleTestConnection() {
+ if (!canEditSettings) return;
try {
setTesting(true);
const res = await fetch('/api/whatsapp/config', { method: 'GET' });
@@ -308,6 +309,7 @@ export function WhatsAppConfig() {
}
async function handleVerifyRegistration() {
+ if (!canEditSettings) return;
setVerifyingRegistration(true);
setRegistrationProbe(null);
try {
@@ -334,6 +336,7 @@ export function WhatsAppConfig() {
}
async function handleReset() {
+ if (!canEditSettings) return;
if (!confirm('This will delete the current WhatsApp config so you can re-enter it. Continue?')) {
return;
}
@@ -396,16 +399,26 @@ export function WhatsAppConfig() {
{/* Main config form */}
+ {!canEditSettings && (
+
+
+ Read-only access
+
+ You must be an Owner or Admin to configure the WhatsApp connection.
+
+
+ )}
+
{/* Corrupted-token reset banner */}
{showResetBanner && (
-
+
-
+
-
+
Stored token can't be decrypted
-
+
{statusMessage}