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 accessToken = response.authResponse.accessToken;
73
- // We wait for the WA_EMBEDDED_SIGNUP_EVENT to get the IDs
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
- // 1. Encrypt and store Meta credentials
149
- await prisma.organization.update({
150
- where: { id },
151
- data: encryptSecrets({
152
- systemUserToken: accessToken,
153
- wabaId
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();