CognxSafeTrack commited on
Commit Β·
d3a9684
1
Parent(s): 3078897
feat: finalize Embedded Signup flow and add webhook simulator
Browse files
apps/admin/src/components/WhatsAppConnectButton.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
import { initMetaSDK, launchEmbeddedSignup } from '../lib/meta-signup';
|
| 3 |
+
import { api } from '../lib/api';
|
| 4 |
+
import { useParams } from 'react-router-dom';
|
| 5 |
+
|
| 6 |
+
export const WhatsAppConnectButton: React.FC = () => {
|
| 7 |
+
const { orgId } = useParams<{ orgId: string }>();
|
| 8 |
+
const [loading, setLoading] = useState(false);
|
| 9 |
+
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
initMetaSDK();
|
| 13 |
+
}, []);
|
| 14 |
+
|
| 15 |
+
const handleConnect = async () => {
|
| 16 |
+
if (!orgId) return;
|
| 17 |
+
setLoading(true);
|
| 18 |
+
setStatus('idle');
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
// 1. Launch Meta Popup
|
| 22 |
+
const result = await launchEmbeddedSignup();
|
| 23 |
+
|
| 24 |
+
// 2. Send IDs and Code/Token to our Backend
|
| 25 |
+
await api.post(`/v1/organizations/${orgId}/whatsapp-setup`, {
|
| 26 |
+
wabaId: result.waba_id,
|
| 27 |
+
accessToken: result.accessToken || result.code, // Can be token or code depending on Meta config
|
| 28 |
+
phoneNumberId: result.phone_number_id,
|
| 29 |
+
phoneNumber: '' // Meta doesn't always send the phone number string in the event
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
setStatus('success');
|
| 33 |
+
console.log("[SETUP] WhatsApp connecté avec succès !");
|
| 34 |
+
} catch (err) {
|
| 35 |
+
console.error("[SETUP] Erreur lors de la configuration:", err);
|
| 36 |
+
setStatus('error');
|
| 37 |
+
} finally {
|
| 38 |
+
setLoading(false);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="p-4 border rounded-lg bg-white shadow-sm">
|
| 44 |
+
<h3 className="text-lg font-semibold mb-2">Configuration WhatsApp Business</h3>
|
| 45 |
+
<p className="text-sm text-gray-600 mb-4">
|
| 46 |
+
Connectez votre compte WhatsApp Business pour commencer Γ envoyer des messages pΓ©dagogiques.
|
| 47 |
+
</p>
|
| 48 |
+
|
| 49 |
+
<button
|
| 50 |
+
onClick={handleConnect}
|
| 51 |
+
disabled={loading}
|
| 52 |
+
className={`px-6 py-2 rounded-md font-medium text-white transition-all ${
|
| 53 |
+
loading ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700'
|
| 54 |
+
}`}
|
| 55 |
+
>
|
| 56 |
+
{loading ? 'Connexion en cours...' : 'Connecter WhatsApp'}
|
| 57 |
+
</button>
|
| 58 |
+
|
| 59 |
+
{status === 'success' && (
|
| 60 |
+
<p className="mt-2 text-sm text-green-600 font-medium">β
WhatsApp connecté avec succès !</p>
|
| 61 |
+
)}
|
| 62 |
+
{status === 'error' && (
|
| 63 |
+
<p className="mt-2 text-sm text-red-600 font-medium">β Γchec de la configuration. RΓ©essayez.</p>
|
| 64 |
+
)}
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
};
|
apps/admin/src/lib/meta-signup.ts
CHANGED
|
@@ -69,21 +69,19 @@ export const launchEmbeddedSignup = (): Promise<any> => {
|
|
| 69 |
|
| 70 |
window.FB.login((response: any) => {
|
| 71 |
if (response.authResponse) {
|
| 72 |
-
const
|
| 73 |
-
|
| 74 |
-
// We resolve with the token; the IDs will come from the event listener
|
| 75 |
-
resolve({ accessToken });
|
| 76 |
} else {
|
| 77 |
window.removeEventListener('message', sessionHandler);
|
| 78 |
reject(new Error("Connexion Facebook Γ©chouΓ©e ou annulΓ©e."));
|
| 79 |
}
|
| 80 |
}, {
|
|
|
|
|
|
|
|
|
|
| 81 |
scope: 'whatsapp_business_management,whatsapp_business_messaging',
|
| 82 |
extras: {
|
| 83 |
-
feature: 'whatsapp_embedded_signup'
|
| 84 |
-
setup: {
|
| 85 |
-
business: { name: 'XamlΓ© Cloud' }
|
| 86 |
-
}
|
| 87 |
}
|
| 88 |
});
|
| 89 |
});
|
|
|
|
| 69 |
|
| 70 |
window.FB.login((response: any) => {
|
| 71 |
if (response.authResponse) {
|
| 72 |
+
const code = response.authResponse.code;
|
| 73 |
+
resolve({ code });
|
|
|
|
|
|
|
| 74 |
} else {
|
| 75 |
window.removeEventListener('message', sessionHandler);
|
| 76 |
reject(new Error("Connexion Facebook Γ©chouΓ©e ou annulΓ©e."));
|
| 77 |
}
|
| 78 |
}, {
|
| 79 |
+
config_id: import.meta.env.VITE_META_CONFIG_ID || '', // Requested config_id
|
| 80 |
+
response_type: 'code', // Requested response_type
|
| 81 |
+
override_default_response_type: true, // Requested override
|
| 82 |
scope: 'whatsapp_business_management,whatsapp_business_messaging',
|
| 83 |
extras: {
|
| 84 |
+
feature: 'whatsapp_embedded_signup'
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
});
|
| 87 |
});
|
apps/api/src/routes/organizations.ts
CHANGED
|
@@ -145,30 +145,32 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 145 |
logger.info(`[WHATSAPP-SETUP] Processing connection for Org: ${id}, WABA: ${wabaId}`);
|
| 146 |
|
| 147 |
try {
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
// 2. Synchronize Phone Number record if provided
|
| 158 |
-
if (phoneNumberId) {
|
| 159 |
-
await (prisma as any).whatsAppPhoneNumber.upsert({
|
| 160 |
-
where: { id: phoneNumberId },
|
| 161 |
-
update: {
|
| 162 |
-
phoneNumber: phoneNumber || '',
|
| 163 |
-
organizationId: id
|
| 164 |
-
},
|
| 165 |
-
create: {
|
| 166 |
-
id: phoneNumberId,
|
| 167 |
-
phoneNumber: phoneNumber || '',
|
| 168 |
-
organizationId: id
|
| 169 |
-
}
|
| 170 |
});
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
// 3. Invalidate Redis cache to let Worker pick up new config immediately
|
| 174 |
await invalidateOrganizationCache(id, phoneNumberId);
|
|
|
|
| 145 |
logger.info(`[WHATSAPP-SETUP] Processing connection for Org: ${id}, WABA: ${wabaId}`);
|
| 146 |
|
| 147 |
try {
|
| 148 |
+
await prisma.$transaction(async (tx) => {
|
| 149 |
+
// 1. Encrypt and store Meta credentials
|
| 150 |
+
await tx.organization.update({
|
| 151 |
+
where: { id },
|
| 152 |
+
data: encryptSecrets({
|
| 153 |
+
systemUserToken: accessToken,
|
| 154 |
+
wabaId
|
| 155 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
});
|
| 157 |
+
|
| 158 |
+
// 2. Synchronize Phone Number record if provided
|
| 159 |
+
if (phoneNumberId) {
|
| 160 |
+
await (tx as any).whatsAppPhoneNumber.upsert({
|
| 161 |
+
where: { id: phoneNumberId },
|
| 162 |
+
update: {
|
| 163 |
+
phoneNumber: phoneNumber || '',
|
| 164 |
+
organizationId: id
|
| 165 |
+
},
|
| 166 |
+
create: {
|
| 167 |
+
id: phoneNumberId,
|
| 168 |
+
phoneNumber: phoneNumber || '',
|
| 169 |
+
organizationId: id
|
| 170 |
+
}
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
|
| 175 |
// 3. Invalidate Redis cache to let Worker pick up new config immediately
|
| 176 |
await invalidateOrganizationCache(id, phoneNumberId);
|
apps/api/src/scripts/simulate-webhook.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fetch from 'node-fetch';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* WhatsApp Webhook Simulator
|
| 5 |
+
*
|
| 6 |
+
* Usage: npx tsx apps/api/src/scripts/simulate-webhook.ts [phoneNumberId] [fromPhone] [text]
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
const targetUrl = 'http://localhost:8080/v1/whatsapp/webhook';
|
| 10 |
+
const phoneNumberId = process.argv[2] || '1029384756';
|
| 11 |
+
const fromPhone = process.argv[3] || '33612345678';
|
| 12 |
+
const text = process.argv[4] || 'Hello from simulation!';
|
| 13 |
+
|
| 14 |
+
async function runSimulation() {
|
| 15 |
+
console.log(`π Simulating message to ${targetUrl}...`);
|
| 16 |
+
console.log(`π± Phone ID: ${phoneNumberId}`);
|
| 17 |
+
console.log(`π€ From: ${fromPhone}`);
|
| 18 |
+
console.log(`π¬ Text: "${text}"`);
|
| 19 |
+
|
| 20 |
+
const payload = {
|
| 21 |
+
object: 'whatsapp_business_account',
|
| 22 |
+
entry: [
|
| 23 |
+
{
|
| 24 |
+
id: 'WHATSAPP_BUSINESS_ACCOUNT_ID',
|
| 25 |
+
changes: [
|
| 26 |
+
{
|
| 27 |
+
value: {
|
| 28 |
+
messaging_product: 'whatsapp',
|
| 29 |
+
metadata: {
|
| 30 |
+
display_phone_number: '123456789',
|
| 31 |
+
phone_number_id: phoneNumberId
|
| 32 |
+
},
|
| 33 |
+
contacts: [{ profile: { name: 'Test User' }, wa_id: fromPhone }],
|
| 34 |
+
messages: [
|
| 35 |
+
{
|
| 36 |
+
from: fromPhone,
|
| 37 |
+
id: `wamid.HBgLMzM2MzA0Nzg0MDYVAgARGBI1NjI5RkZCMEY3OUM2ODRDOTQA`,
|
| 38 |
+
timestamp: Math.floor(Date.now() / 1000).toString(),
|
| 39 |
+
type: 'text',
|
| 40 |
+
text: { body: text }
|
| 41 |
+
}
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
field: 'messages'
|
| 45 |
+
}
|
| 46 |
+
]
|
| 47 |
+
}
|
| 48 |
+
]
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
try {
|
| 52 |
+
const response = await fetch(targetUrl, {
|
| 53 |
+
method: 'POST',
|
| 54 |
+
headers: {
|
| 55 |
+
'Content-Type': 'application/json',
|
| 56 |
+
// We simulate the Meta HMAC signature if needed, or disable it in dev
|
| 57 |
+
'x-hub-signature-256': 'sha256=fake-signature-for-local-test'
|
| 58 |
+
},
|
| 59 |
+
body: JSON.stringify(payload)
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
const data = await response.json();
|
| 63 |
+
console.log(`\nβ
Response (${response.status}):`, JSON.stringify(data, null, 2));
|
| 64 |
+
|
| 65 |
+
if (data.status === 'forwarded') {
|
| 66 |
+
console.log("\nπ‘ The message was successfully FORWARDED to the internal worker.");
|
| 67 |
+
} else if (data.status === 'received') {
|
| 68 |
+
console.log("\nπ₯ The message was successfully QUEUED for local processing.");
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
} catch (err: any) {
|
| 72 |
+
console.error("\nβ Simulation failed:", err.message);
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
runSimulation();
|