CognxSafeTrack Claude Sonnet 4.6 commited on
Commit ·
70a5a84
1
Parent(s): bde43ec
feat(meta-status): store metaBusinessId for reliable business verification
Browse filesAdd optional `metaBusinessId` field to Organization (schema + shared-types).
fetchMetaStatus now queries /{businessId}?fields=verification_status directly
when metaBusinessId is stored — bypasses WABA edge permission restrictions
that block test WABAs and certain system user token scopes.
Direct setup form gains a "Business ID Meta" input that saves via PUT /organizations/:id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
apps/admin/src/pages/ClientsManagementView.tsx
CHANGED
|
@@ -57,7 +57,7 @@ export default function ClientsManagementView() {
|
|
| 57 |
const [billingOrg, setBillingOrg] = useState<Organization | null>(null);
|
| 58 |
const [metaStatuses, setMetaStatuses] = useState<Record<string, MetaStatus>>({});
|
| 59 |
const [setupOrg, setSetupOrg] = useState<Organization | null>(null);
|
| 60 |
-
const [directSetup, setDirectSetup] = useState({ wabaId: '', accessToken: '', phoneNumberId: '' });
|
| 61 |
const [isDirectSetupSaving, setIsDirectSetupSaving] = useState(false);
|
| 62 |
|
| 63 |
const fetchMetaStatus = async (orgId: string, force = false) => {
|
|
@@ -99,9 +99,12 @@ export default function ClientsManagementView() {
|
|
| 99 |
...(directSetup.accessToken && { accessToken: directSetup.accessToken }),
|
| 100 |
...(directSetup.phoneNumberId && { phoneNumberId: directSetup.phoneNumberId }),
|
| 101 |
}, token!);
|
|
|
|
|
|
|
|
|
|
| 102 |
toast.success('Configuration WhatsApp mise à jour !');
|
| 103 |
setSetupOrg(null);
|
| 104 |
-
setDirectSetup({ wabaId: '', accessToken: '', phoneNumberId: '' });
|
| 105 |
await fetchClients();
|
| 106 |
setTimeout(() => fetchMetaStatus(setupOrg.id, true), 1000);
|
| 107 |
} catch (err: any) {
|
|
@@ -204,7 +207,7 @@ export default function ClientsManagementView() {
|
|
| 204 |
<div className="h-10 w-px bg-slate-100"></div>
|
| 205 |
{metaStatuses[client.id] && !metaStatuses[client.id].loading && !metaStatuses[client.id].configured ? (
|
| 206 |
<button
|
| 207 |
-
onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: '', accessToken: '', phoneNumberId: client.phoneNumbers?.[0]?.id || '' }); }}
|
| 208 |
className="flex items-center gap-2 bg-amber-500 text-white px-4 py-2 rounded-xl font-semibold hover:bg-amber-600 transition text-xs"
|
| 209 |
>
|
| 210 |
<AlertTriangle className="w-3.5 h-3.5" /> Reconfigurer WhatsApp
|
|
@@ -484,6 +487,19 @@ export default function ClientsManagementView() {
|
|
| 484 |
Trouvez-le sur <a href="https://business.facebook.com/wa/manage/home" target="_blank" rel="noreferrer" className="text-indigo-500 underline">business.facebook.com → WhatsApp Manager</a>
|
| 485 |
</p>
|
| 486 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
<div>
|
| 488 |
<label className="block text-xs font-bold text-slate-500 mb-1">Token système <span className="text-slate-300">(optionnel — utilise le token env par défaut)</span></label>
|
| 489 |
<input
|
|
|
|
| 57 |
const [billingOrg, setBillingOrg] = useState<Organization | null>(null);
|
| 58 |
const [metaStatuses, setMetaStatuses] = useState<Record<string, MetaStatus>>({});
|
| 59 |
const [setupOrg, setSetupOrg] = useState<Organization | null>(null);
|
| 60 |
+
const [directSetup, setDirectSetup] = useState({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: '' });
|
| 61 |
const [isDirectSetupSaving, setIsDirectSetupSaving] = useState(false);
|
| 62 |
|
| 63 |
const fetchMetaStatus = async (orgId: string, force = false) => {
|
|
|
|
| 99 |
...(directSetup.accessToken && { accessToken: directSetup.accessToken }),
|
| 100 |
...(directSetup.phoneNumberId && { phoneNumberId: directSetup.phoneNumberId }),
|
| 101 |
}, token!);
|
| 102 |
+
if (directSetup.metaBusinessId) {
|
| 103 |
+
await api.put(`/v1/organizations/${setupOrg.id}`, { metaBusinessId: directSetup.metaBusinessId }, token!);
|
| 104 |
+
}
|
| 105 |
toast.success('Configuration WhatsApp mise à jour !');
|
| 106 |
setSetupOrg(null);
|
| 107 |
+
setDirectSetup({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: '' });
|
| 108 |
await fetchClients();
|
| 109 |
setTimeout(() => fetchMetaStatus(setupOrg.id, true), 1000);
|
| 110 |
} catch (err: any) {
|
|
|
|
| 207 |
<div className="h-10 w-px bg-slate-100"></div>
|
| 208 |
{metaStatuses[client.id] && !metaStatuses[client.id].loading && !metaStatuses[client.id].configured ? (
|
| 209 |
<button
|
| 210 |
+
onClick={() => { setSetupOrg(client); setDirectSetup({ wabaId: '', metaBusinessId: '', accessToken: '', phoneNumberId: client.phoneNumbers?.[0]?.id || '' }); }}
|
| 211 |
className="flex items-center gap-2 bg-amber-500 text-white px-4 py-2 rounded-xl font-semibold hover:bg-amber-600 transition text-xs"
|
| 212 |
>
|
| 213 |
<AlertTriangle className="w-3.5 h-3.5" /> Reconfigurer WhatsApp
|
|
|
|
| 487 |
Trouvez-le sur <a href="https://business.facebook.com/wa/manage/home" target="_blank" rel="noreferrer" className="text-indigo-500 underline">business.facebook.com → WhatsApp Manager</a>
|
| 488 |
</p>
|
| 489 |
</div>
|
| 490 |
+
<div>
|
| 491 |
+
<label className="block text-xs font-bold text-slate-500 mb-1">Business ID Meta <span className="text-slate-300">(optionnel — pour afficher la vérification)</span></label>
|
| 492 |
+
<input
|
| 493 |
+
type="text"
|
| 494 |
+
placeholder="ex: 25855038707486178"
|
| 495 |
+
value={directSetup.metaBusinessId}
|
| 496 |
+
onChange={e => setDirectSetup(s => ({ ...s, metaBusinessId: e.target.value }))}
|
| 497 |
+
className="w-full border border-slate-200 rounded-xl px-3 py-2.5 text-sm font-mono outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400"
|
| 498 |
+
/>
|
| 499 |
+
<p className="text-[10px] text-slate-400 mt-1">
|
| 500 |
+
Visible sur <a href="https://business.facebook.com/settings/info" target="_blank" rel="noreferrer" className="text-indigo-500 underline">business.facebook.com → Paramètres → Informations</a>
|
| 501 |
+
</p>
|
| 502 |
+
</div>
|
| 503 |
<div>
|
| 504 |
<label className="block text-xs font-bold text-slate-500 mb-1">Token système <span className="text-slate-300">(optionnel — utilise le token env par défaut)</span></label>
|
| 505 |
<input
|
apps/api/src/services/organization.ts
CHANGED
|
@@ -133,26 +133,44 @@ export async function fetchMetaStatus(organizationId: string, forceRefresh = fal
|
|
| 133 |
logger.error({ err, organizationId }, '[META-STATUS] WABA fetch failed');
|
| 134 |
}
|
| 135 |
|
| 136 |
-
// Call 2 — Business verification (optional,
|
| 137 |
let businessId: string | undefined;
|
| 138 |
let businessName: string | undefined;
|
| 139 |
let businessVerified: boolean | undefined;
|
| 140 |
|
| 141 |
-
// Attempt A:
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
// Attempt
|
| 156 |
if (businessId === undefined) {
|
| 157 |
try {
|
| 158 |
const obRes = await fetch(
|
|
@@ -163,7 +181,6 @@ export async function fetchMetaStatus(organizationId: string, forceRefresh = fal
|
|
| 163 |
if (!obData.error && obData.on_behalf_of_business_info?.id) {
|
| 164 |
businessId = obData.on_behalf_of_business_info.id;
|
| 165 |
businessName = obData.on_behalf_of_business_info.name;
|
| 166 |
-
// Fetch verification status directly from the business node
|
| 167 |
const vRes = await fetch(
|
| 168 |
`https://graph.facebook.com/v19.0/${businessId}?fields=name,verification_status`,
|
| 169 |
{ headers }
|
|
|
|
| 133 |
logger.error({ err, organizationId }, '[META-STATUS] WABA fetch failed');
|
| 134 |
}
|
| 135 |
|
| 136 |
+
// Call 2 — Business verification (optional, three attempts in priority order)
|
| 137 |
let businessId: string | undefined;
|
| 138 |
let businessName: string | undefined;
|
| 139 |
let businessVerified: boolean | undefined;
|
| 140 |
|
| 141 |
+
// Attempt A: stored metaBusinessId — most reliable, bypasses WABA edge permissions
|
| 142 |
+
if ((org as any).metaBusinessId) {
|
| 143 |
+
try {
|
| 144 |
+
const vRes = await fetch(
|
| 145 |
+
`https://graph.facebook.com/v19.0/${(org as any).metaBusinessId}?fields=name,verification_status`,
|
| 146 |
+
{ headers }
|
| 147 |
+
);
|
| 148 |
+
const vData = await vRes.json() as any;
|
| 149 |
+
if (!vData.error) {
|
| 150 |
+
businessId = (org as any).metaBusinessId;
|
| 151 |
+
businessName = vData.name;
|
| 152 |
+
businessVerified = vData.verification_status === 'verified';
|
| 153 |
+
}
|
| 154 |
+
} catch { /* non-fatal */ }
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Attempt B: business edge on WABA (works on production WABAs with business_management scope)
|
| 158 |
+
if (businessId === undefined) {
|
| 159 |
+
try {
|
| 160 |
+
const bizRes = await fetch(
|
| 161 |
+
`https://graph.facebook.com/v19.0/${org.wabaId}?fields=business{id,name,verification_status}`,
|
| 162 |
+
{ headers }
|
| 163 |
+
);
|
| 164 |
+
const bizData = await bizRes.json() as any;
|
| 165 |
+
if (!bizData.error && bizData.business) {
|
| 166 |
+
businessId = bizData.business.id;
|
| 167 |
+
businessName = bizData.business.name;
|
| 168 |
+
businessVerified = bizData.business.verification_status === 'verified';
|
| 169 |
+
}
|
| 170 |
+
} catch { /* non-fatal */ }
|
| 171 |
+
}
|
| 172 |
|
| 173 |
+
// Attempt C: on_behalf_of_business_info fallback (broader token compatibility)
|
| 174 |
if (businessId === undefined) {
|
| 175 |
try {
|
| 176 |
const obRes = await fetch(
|
|
|
|
| 181 |
if (!obData.error && obData.on_behalf_of_business_info?.id) {
|
| 182 |
businessId = obData.on_behalf_of_business_info.id;
|
| 183 |
businessName = obData.on_behalf_of_business_info.name;
|
|
|
|
| 184 |
const vRes = await fetch(
|
| 185 |
`https://graph.facebook.com/v19.0/${businessId}?fields=name,verification_status`,
|
| 186 |
{ headers }
|
packages/database/prisma/schema.prisma
CHANGED
|
@@ -11,6 +11,7 @@ model Organization {
|
|
| 11 |
id String @id @default(uuid())
|
| 12 |
name String
|
| 13 |
wabaId String? @unique
|
|
|
|
| 14 |
systemUserToken String?
|
| 15 |
createdAt DateTime @default(now())
|
| 16 |
updatedAt DateTime @updatedAt
|
|
|
|
| 11 |
id String @id @default(uuid())
|
| 12 |
name String
|
| 13 |
wabaId String? @unique
|
| 14 |
+
metaBusinessId String?
|
| 15 |
systemUserToken String?
|
| 16 |
createdAt DateTime @default(now())
|
| 17 |
updatedAt DateTime @updatedAt
|
packages/shared-types/src/organization.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const OrganizationSchema = z.object({
|
|
| 38 |
webhookSecret: z.string().optional(),
|
| 39 |
knowledgeBaseUrl: z.string().url().optional().or(z.literal('')),
|
| 40 |
systemUserToken: z.string().optional(),
|
|
|
|
| 41 |
brandingData: z.any().optional(),
|
| 42 |
});
|
| 43 |
|
|
|
|
| 38 |
webhookSecret: z.string().optional(),
|
| 39 |
knowledgeBaseUrl: z.string().url().optional().or(z.literal('')),
|
| 40 |
systemUserToken: z.string().optional(),
|
| 41 |
+
metaBusinessId: z.string().optional(),
|
| 42 |
brandingData: z.any().optional(),
|
| 43 |
});
|
| 44 |
|