@cemscale-voip/voip-sdk 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,12 +43,13 @@ await voip.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
43
43
  await voip.hangup(callUuid);
44
44
  ```
45
45
 
46
- ### Three-Way Calling (Conferencia Tripartita) — GUÍA COMPLETA
46
+ ### Three-Way Calling (Conferencia Tripartita) — Conference-Based, 1-Step
47
47
 
48
- > **⚠️ LEE ESTO ANTES DE IMPLEMENTAR.** La tripartita tiene un flujo de varios pasos
49
- > y varios estados de UI. No es una sola llamada API. Si muestras el timer antes
50
- > de que la persona conteste, o llamas merge antes de que el participante esté listo,
51
- > TODO el flujo se rompe.
48
+ > The three-way calling system uses **FreeSWITCH conferences**. When you add a
49
+ > participant, both call legs are transferred into a conference immediately.
50
+ > Person A is put on hold (muted + deaf + hears MOH). Person B rings directly
51
+ > into the conference. **No separate merge step is needed** — the conference is
52
+ > active from the moment you call `addParticipant()`.
52
53
 
53
54
  ---
54
55
 
@@ -57,173 +58,143 @@ await voip.hangup(callUuid);
57
58
  ```
58
59
  PASO 0: El agente está en llamada con Persona A. currentCall.state === 'established'.
59
60
  PASO 1: El agente presiona "Agregar" → addParticipant('numero de Persona B').
60
- La API pone a Persona A en hold y origina llamada a Persona B.
61
- threeWaySession.state 'adding' ← MOSTRAR "Llamando a Persona B..."
62
- PASO 2: Persona B contesta su teléfono.
63
- threeWaySession.state 'adding' ← se mantiene igual, NO cambia solo
64
- PASO 3: El agente presiona "Fusionar"mergeThreeWay().
65
- Las 3 personas (Agente, A, B) entran a conferencia.
66
- threeWaySession.state 'active' ← AHORA inicia el timer de conferencia
61
+ La API transfiere ambas patas a conferencia, pone A en hold (MOH),
62
+ y origina llamada a B directamente dentro de la conferencia.
63
+ threeWaySession.state 'dialing' ← MOSTRAR "Llamando a Persona B..."
64
+ PASO 2: Persona B contesta entra automáticamente a la conferencia.
65
+ El SDK detecta via polling threeWaySession.state 'holding'
66
+ El agente habla con B, A sigue en hold (MOH).
67
+ PASO 3a: SWAP El agente quiere hablar con A en privado.
68
+ swapParticipant(keepUuid=A, holdUuid=B) → B va a hold (MOH), A vuelve.
69
+ PASO 3b: MERGE — El agente presiona "Conferencia" (todos hablan).
70
+ mergeThreeWay() → Todos salen de hold, los 3 se escuchan.
71
+ PASO 3c: CANCEL — Persona B no contestó.
72
+ cancelAddParticipant() → Mata a B, reanuda a A, conferencia 2 personas.
73
+ PASO 4: KICK — Sacar a alguien específico.
74
+ dropParticipant(memberUuid) → Esa persona sale de la conferencia.
67
75
  ```
68
76
 
69
77
  ---
70
78
 
71
- #### FLOW 1 — `addParticipant` + `mergeThreeWay` (WebRTC softphone, recomendado)
72
-
73
- Este es el flujo con el hook `useVoIP`. **El hook maneja TODO automáticamente:**
74
- resolución de UUID de FreeSWITCH, estados de la tripartita, y limpieza al colgar.
79
+ #### FLOW 1 — `useVoIP` hook (WebRTC softphone, recomendado)
75
80
 
76
81
  ##### 1.1 — Configuración inicial
77
82
 
78
83
  ```typescript
79
84
  import { useVoIP } from '@cemscale-voip/voip-sdk';
80
85
 
81
- // Dentro de tu componente React:
82
86
  const {
83
- // Estado del teléfono
84
87
  isRegistered, // boolean — SIP registrado?
85
- currentCall, // WebRTCCallInfo | null — datos de la llamada activa
86
- error, // string | null — último error (resetear con clearError)
87
-
88
- // Acciones de llamada básica
88
+ currentCall, // WebRTCCallInfo | null — llamada activa
89
+ error, // string | null — último error
89
90
  startPhone, // (ext, password, displayName?) => Promise<void>
90
91
  call, // (number) => Promise<WebRTCCallInfo>
91
- answer, // () => Promise<void>
92
92
  hangup, // () => Promise<void>
93
- toggleHold, // () => Promise<boolean>
94
93
  toggleMute, // () => boolean
95
94
 
96
- // Acciones de tripartita
95
+ // Three-way call
97
96
  addParticipant, // (target: string) => Promise<void>
98
- mergeThreeWay, // () => Promise<void>
99
- swapParticipant, // (keepUuid: string, holdUuid: string) => Promise<void>
100
- dropParticipant, // (memberId: string) => Promise<void>
97
+ mergeThreeWay, // () => Promise<void> — unmute/undeaf todos
98
+ swapParticipant, // (keepUuid, holdUuid) => Promise<void>
99
+ dropParticipant, // (memberUuid: string) => Promise<void>
100
+ cancelAddParticipant, // () => Promise<void> — B no contestó
101
+ endThreeWay, // () => Promise<void>
101
102
 
102
- // Estado de la tripartita
103
103
  threeWaySession, // ThreeWaySession | null
104
104
  } = useVoIP({
105
105
  apiUrl: 'https://voip-api.cemscale.com',
106
- apiKey: 'csk_live_...', // API key de tu tenant
106
+ apiKey: 'csk_live_...',
107
107
  });
108
108
  ```
109
109
 
110
- ##### 1.2 — `ThreeWaySession` (tipo completo)
111
-
112
- Este objeto describe TODO el estado de la tripartita. Tu UI debe leerlo para decidir qué mostrar.
110
+ ##### 1.2 — `ThreeWaySession` y `ThreeWayParticipant`
113
111
 
114
112
  ```typescript
115
113
  interface ThreeWaySession {
116
- conferenceName: string; // Nombre de la sala de conferencia (UUID, ej: "page_abc_123")
117
- state: 'adding' | 'active' | 'swapping';
118
- // 'adding' = Persona B está siendo llamada. MOSTRAR "Llamando..."
119
- // 'active' = Conferencia activa. AHORA iniciar timer.
120
- // 'swapping' = Intercambiando participantes (no mostrar timer propio)
121
- participants: ThreeWayParticipant[]; // Solo poblado en state='active'
122
- pendingUuid: string | undefined; // UUID de Persona B. Existe en state='adding'
123
- originalBridgedLeg: string | undefined; // UUID de la pata PSTN de Persona A
114
+ conferenceName: string; // "3way_<tenantId>_<random>"
115
+ state: 'dialing' | 'holding' | 'active' | 'swapping';
116
+ participants: ThreeWayParticipant[];
117
+ pendingUuid?: string; // UUID de Persona B mientras suena
118
+ heldMemberUuid?: string; // UUID del participante en hold
119
+ originalBridgedLeg?: string;
124
120
  }
125
121
 
126
122
  interface ThreeWayParticipant {
127
- uuid: string; // UUID del canal FreeSWITCH
128
- callerIdNumber: string; // Número de teléfono
129
- callerIdName: string; // Nombre (o número si no hay nombre)
130
- status: 'active' | 'held'; // Estado en la conferencia
131
- memberId: string; // ID de miembro en la conferencia
132
- }
133
- ```
134
-
135
- ##### 1.3 — `WebRTCCallInfo` (campos relevantes para tripartita)
136
-
137
- ```typescript
138
- interface WebRTCCallInfo {
139
- id: string; // ID local de SIP.js — ** NO USAR para API calls **
140
- fsChannelUuid: string | null; // UUID de FreeSWITCH — ** USAR ESTE para API calls **
141
- // null mientras la llamada está conectando
142
- // poblado automáticamente cuando state === 'established'
143
- state: string; // 'ringing' | 'connecting' | 'established' | 'held' | 'terminated' | ...
144
- direction: 'inbound' | 'outbound';
145
- remoteIdentity: string; // Número remoto (ej: '+17866302522')
146
- remoteDisplayName: string; // Nombre mostrado
147
- held: boolean;
148
- muted: boolean;
123
+ uuid: string; // UUID del canal FreeSWITCH
124
+ memberId: string; // ID de miembro en la conferencia
125
+ callerIdNumber: string; // "+17865551234" o "1002"
126
+ callerIdName: string; // "John Smith"
127
+ muted: boolean; // true si está muteado
128
+ deaf: boolean; // true si está ensordecido
129
+ status: 'active' | 'held'; // 'held' = muted + deaf + MOH
149
130
  }
150
131
  ```
151
132
 
152
- ##### 1.4 — Flujo COMPLETO paso a paso
133
+ ##### 1.3 — Flujo COMPLETO paso a paso
153
134
 
154
135
  ```typescript
155
136
  // ═══════════════════════════════════════════════════════════
156
137
  // PASO 0: Agente en llamada con Persona A
157
138
  // ═══════════════════════════════════════════════════════════
158
-
159
- // currentCall.state === 'established' ← CONDICIÓN para mostrar botón "Agregar"
160
- // currentCall.fsChannelUuid !== null ← resuelto automáticamente
161
- // threeWaySession === null ← no hay tripartita activa
139
+ // currentCall.state === 'established' ← condición para botón "Agregar"
140
+ // threeWaySession === null
162
141
 
163
142
  // ═══════════════════════════════════════════════════════════
164
143
  // PASO 1: Agregar Persona B
165
144
  // ═══════════════════════════════════════════════════════════
145
+ await addParticipant('+17866302522');
146
+ // La API:
147
+ // 1. Transfiere WebRTC + Persona A a conferencia (uuid_transfer -both)
148
+ // 2. Mutea + ensordece a Persona A + le pone MOH (uuid_displace)
149
+ // 3. Origina llamada a Persona B directamente a la conferencia
150
+ // 4. Retorna inmediatamente
151
+
152
+ // DESPUÉS:
153
+ // threeWaySession.state === 'dialing' ← Persona B está sonando
154
+ // threeWaySession.conferenceName !== '' ← Conferencia YA existe
155
+ // ❌ NO iniciar timer — B no ha contestado
166
156
 
167
- // UI: Botón "Agregar participante" → input para número → llama a addParticipant
168
-
169
- try {
170
- await addParticipant('+17866302522');
171
- // └─ Internamente el SDK:
172
- // 1. Toma currentCall.fsChannelUuid
173
- // 2. Llama POST /api/calls/{fsChannelUuid}/add-participant
174
- // 3. API pone Persona A en hold (escucha música)
175
- // 4. API origina llamada a Persona B vía trunk
176
- // 5. Retorna { newCallUuid, originalBridgedLeg }
177
- // 6. SDK setea threeWaySession = { state: 'adding', pendingUuid, ... }
178
-
179
- // ⚠️ DESPUÉS DE ESTA LLAMADA:
180
- // threeWaySession.state === 'adding' ← MOSTRAR "Llamando a Persona B..."
181
- // threeWaySession.pendingUuid !== undefined ← Persona B está sonando
182
- // currentCall aún existe (Persona A en hold)
183
- // ❌ NO iniciar timer — Persona B NO ha contestado todavía
184
- // ❌ NO llamar a mergeThreeWay() todavía — Persona B no ha contestado
185
-
186
- } catch (err) {
187
- // addParticipant falló. Persona A vuelve de hold automáticamente.
188
- console.error('Error al agregar participante:', err);
189
- }
157
+ // ═══════════════════════════════════════════════════════════
158
+ // PASO 2: Persona B contesta (detectado por polling automático)
159
+ // ═══════════════════════════════════════════════════════════
160
+ // El hook hace polling de getConferenceMembers() cada 2s.
161
+ // Cuando detecta que B entró a la conferencia:
162
+ // threeWaySession.state 'holding'
163
+ // threeWaySession.participants [agente(active), A(held), B(active)]
164
+ // El agente habla con B. A escucha MOH.
190
165
 
191
166
  // ═══════════════════════════════════════════════════════════
192
- // PASO 2: Persona B contesta (el agente lo escucha o ve en UI)
167
+ // PASO 3a: SWAP Hablar con A, poner B en hold
193
168
  // ═══════════════════════════════════════════════════════════
169
+ await swapParticipant(personA.uuid, personB.uuid);
170
+ // B → muted + deaf + MOH
171
+ // A → unmuted + undeaf + stop MOH
172
+ // threeWaySession.state → 'holding'
194
173
 
195
- // El SDK NO notifica automáticamente cuando Persona B contesta.
196
- // La UI debe mostrar un botón "Fusionar" inmediatamente después de
197
- // addParticipant. El agente presiona "Fusionar" cuando escucha
198
- // que Persona B contestó, o cuando ve que pasaron ~10 segundos.
174
+ // ═══════════════════════════════════════════════════════════
175
+ // PASO 3b: MERGE Todos hablan (conferencia tripartita)
176
+ // ═══════════════════════════════════════════════════════════
177
+ await mergeThreeWay();
178
+ // Desmutea + undeaf a TODOS los participantes en hold
179
+ // threeWaySession.state → 'active'
180
+ // ✅ AHORA sí iniciar timer de conferencia
199
181
 
200
182
  // ═══════════════════════════════════════════════════════════
201
- // PASO 3: Fusionar en conferencia
183
+ // PASO 3c: CANCEL B no contestó
202
184
  // ═══════════════════════════════════════════════════════════
185
+ await cancelAddParticipant();
186
+ // Mata canal de B, desmutea + undeaf a A
187
+ // Conferencia sigue con 2 personas (agente + A)
203
188
 
204
- // UI: Botón "Fusionar" — visible cuando threeWaySession.state === 'adding'
205
-
206
- try {
207
- await mergeThreeWay();
208
- // └─ Internamente el SDK:
209
- // 1. Toma currentCall.fsChannelUuid (callUuidA)
210
- // 2. Toma threeWaySession.pendingUuid (callUuidB)
211
- // 3. Toma threeWaySession.originalBridgedLeg
212
- // 4. Llama POST /api/calls/three-way/merge
213
- // 5. API mueve todos a conferencia
214
- // 6. SDK setea threeWaySession = { state: 'active', participants: [...] }
215
-
216
- // ✅ DESPUÉS DE ESTA LLAMADA:
217
- // threeWaySession.state === 'active' ← AHORA sí iniciar timer de conferencia
218
- // threeWaySession.participants.length >= 2 ← Persona A + Persona B (más el agente)
219
- // currentCall sigue activo (el agente está en la conferencia)
220
-
221
- } catch (err) {
222
- console.error('Error al fusionar:', err);
223
- }
189
+ // ═══════════════════════════════════════════════════════════
190
+ // PASO 4: KICK — Sacar a alguien de la conferencia
191
+ // ═══════════════════════════════════════════════════════════
192
+ await dropParticipant(personB.uuid);
193
+ // B sale de la conferencia (su canal cuelga)
194
+ // Si solo queda 1 participante, threeWaySession → null
224
195
  ```
225
196
 
226
- ##### 1.5 — Qué mostrar en la UI en CADA estado
197
+ ##### 1.4 — Qué mostrar en la UI en CADA estado
227
198
 
228
199
  ```typescript
229
200
  // ── SIN LLAMADA ──
@@ -238,281 +209,182 @@ if (currentCall && !threeWaySession) {
238
209
  <CallTimer startTime={currentCall.answerTime} />
239
210
  <RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
240
211
  <MuteButton active={currentCall.muted} onPress={toggleMute} />
241
- <HoldButton active={currentCall.held} onPress={toggleHold} />
242
212
  <HangupButton onPress={hangup} />
243
213
  <AddParticipantButton onPress={() => showAddParticipantInput()} />
244
- {/* ↑ Solo mostrar si currentCall.state === 'established' */}
245
214
  </InCallScreen>
246
215
  );
247
216
  }
248
217
 
249
- // ── AGREGANDO PARTICIPANTE (Persona B está sonando) ──
250
- if (threeWaySession?.state === 'adding') {
218
+ // ── DIALING Persona B está sonando ──
219
+ if (threeWaySession?.state === 'dialing') {
251
220
  return (
252
221
  <InCallScreen>
253
- {/* Persona A sigue en hold — mostrar timer de Persona A */}
254
- <CallTimer startTime={currentCall.answerTime} />
255
- <RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
256
- <HoldIndicator /> {/* Persona A está en hold */}
257
-
258
- {/* Persona B */}
259
222
  <CallingBanner>
260
223
  <Spinner />
261
224
  <Text>Llamando a Persona B...</Text>
262
- {/* ❌ NO mostrar timer para Persona B — no ha contestado */}
263
225
  </CallingBanner>
226
+ <CancelButton onPress={cancelAddParticipant} />
227
+ <HangupButton onPress={hangup} />
228
+ </InCallScreen>
229
+ );
230
+ }
264
231
 
265
- <MergeButton onPress={mergeThreeWay} />
266
- {/* El agente presiona esto cuando escucha que Persona B contestó */}
267
-
268
- <CancelButton onPress={hangup} />
269
- {/* Cancela todo: cuelga Persona A, Persona B, y al agente */}
232
+ // ── HOLDING — Un participante en hold, otro activo ──
233
+ if (threeWaySession?.state === 'holding' || threeWaySession?.state === 'swapping') {
234
+ return (
235
+ <InCallScreen>
236
+ {threeWaySession.participants.map(p => (
237
+ <ParticipantRow key={p.uuid}>
238
+ <ParticipantName name={p.callerIdName} number={p.callerIdNumber} />
239
+ <ParticipantStatus status={p.status} /> {/* 'held' o 'active' */}
240
+ {p.status === 'held' && (
241
+ <UnholdButton onPress={() => {
242
+ const other = threeWaySession.participants.find(x => x.uuid !== p.uuid && x.status === 'active');
243
+ if (other) swapParticipant(p.uuid, other.uuid);
244
+ }} />
245
+ )}
246
+ </ParticipantRow>
247
+ ))}
248
+ <MergeButton onPress={mergeThreeWay} label="Conferencia (3)" />
249
+ <HangupButton onPress={hangup} />
270
250
  </InCallScreen>
271
251
  );
272
252
  }
273
253
 
274
- // ── CONFERENCIA ACTIVA ──
254
+ // ── ACTIVE Conferencia tripartita (todos hablan) ──
275
255
  if (threeWaySession?.state === 'active') {
276
256
  return (
277
257
  <ConferenceScreen>
278
- <ConferenceHeader>
279
- <ConferenceIcon />
280
- <Text>Conferencia — 3 participantes</Text>
281
- <ConferenceTimer startTime={conferenceStartTime} />
282
- {/* ↑ AHORA sí iniciar timer */}
283
- </ConferenceHeader>
284
-
258
+ <ConferenceTimer startTime={conferenceStartTime} />
285
259
  {threeWaySession.participants.map(p => (
286
260
  <ParticipantRow key={p.uuid}>
287
261
  <ParticipantName name={p.callerIdName} number={p.callerIdNumber} />
288
- <ParticipantStatus status={p.status} />
289
- {/* p.status === 'held' → mostrar "En espera" */}
290
- <SwapButton onPress={() => swapParticipant(p.uuid, getOtherActiveUuid(p))} />
291
- {/* ↑ Solo mostrar si hay >1 participante active */}
292
- <DropButton onPress={() => dropParticipant(p.memberId)} />
262
+ <DropButton onPress={() => dropParticipant(p.uuid)} />
293
263
  </ParticipantRow>
294
264
  ))}
295
-
296
- <MuteButton active={currentCall.muted} onPress={toggleMute} />
297
265
  <EndConferenceButton onPress={hangup} />
298
- {/* ↑ Cuelga a TODOS y destruye la conferencia */}
299
266
  </ConferenceScreen>
300
267
  );
301
268
  }
302
269
  ```
303
270
 
304
- ##### 1.6 — Errores comunes y cómo manejarlos
271
+ ##### 1.5 — Errores comunes
305
272
 
306
273
  | Error | Causa | Solución |
307
274
  |-------|-------|----------|
308
- | `"FreeSWITCH UUID not resolved yet"` | Llamaste `addParticipant` antes de que la llamada conecte | Espera a `currentCall.state === 'established'` y `currentCall.fsChannelUuid !== null` |
309
- | `"No active call"` | `currentCall` es null | Verifica que haya una llamada activa antes de mostrar el botón |
310
- | `"No pending participant to merge"` | `threeWaySession.pendingUuid` es undefined | Solo mostrar botón "Fusionar" cuando `threeWaySession?.state === 'adding'` y `threeWaySession?.pendingUuid` existe |
311
- | Timer se inicia antes de contestar | Tu UI inicia el timer en state='adding' | Solo iniciar timer cuando `threeWaySession.state === 'active'` |
312
- | La llamada se cuelga al hacer merge | Llamaste merge antes de que Persona B conteste | Asegúrate de que Persona B ya contestó antes de fusionar |
313
- | `addParticipant` lanza error 500 | El backend no pudo originar a Persona B | Mostrar el mensaje de error al agente. La llamada con Persona A se reanuda sola. |
275
+ | `"FreeSWITCH UUID not resolved yet"` | Llamaste `addParticipant` antes de que la llamada conecte | Espera `currentCall.state === 'established'` y `fsChannelUuid !== null` |
276
+ | `"No active call"` | `currentCall` es null | Verifica que haya una llamada activa |
277
+ | Timer se inicia antes de contestar | Iniciaste timer en state='dialing' | Solo iniciar timer cuando `state === 'active'` |
278
+ | `addParticipant` lanza error 500 | El backend no pudo originar a Persona B | La llamada con A se reanuda automáticamente |
314
279
 
315
280
  ---
316
281
 
317
- #### FLOW 2 — `mergeDirectCalls` (teléfono físico / desk phone)
318
-
319
- Para cuando el agente usa un teléfono físico (Yealink, Grandstream) y manualmente:
320
- 1. Llama a Persona A
321
- 2. Pone en hold
322
- 3. Marca a Persona B
323
- 4. Quiere fusionar ambas
324
-
325
- **NO usa `addParticipant`.** Solo detecta las 2 llamadas activas y las fusiona.
282
+ #### FLOW 2 — VoIPClient directo (sin hook)
326
283
 
327
284
  ```typescript
328
285
  import { VoIPClient } from '@cemscale-voip/voip-sdk';
329
286
 
330
- const voip = new VoIPClient({
331
- apiUrl: 'https://voip-api.cemscale.com',
332
- apiKey: 'csk_live_...',
287
+ const voip = new VoIPClient({ apiUrl: '...', apiKey: '...' });
288
+
289
+ // PASO 1: Agregar participante (conferencia se crea inmediatamente)
290
+ const result = await voip.addCallParticipant(fsUuid, {
291
+ toNumber: '+17866302522',
333
292
  });
334
293
 
335
- // ═══════════════════════════════════════════════════════
336
- // PASO 1: Detectar 2+ llamadas activas de la misma extensión
337
- // ═══════════════════════════════════════════════════════
294
+ // result = {
295
+ // conferenceName: "3way_<tenantId>_<hex>",
296
+ // state: "dialing",
297
+ // participants: [
298
+ // { uuid: "webrtc-uuid", callerIdNumber: "1001", status: "active", ... },
299
+ // { uuid: "personA-uuid", callerIdNumber: "+1786...", status: "held", muted: true, deaf: true },
300
+ // ],
301
+ // newCallUuid: "personB-uuid",
302
+ // heldMemberUuid: "personA-uuid",
303
+ // originalBridgedLeg: "personA-uuid",
304
+ // }
338
305
 
339
- // Hacer polling cada 5 segundos
306
+ // PASO 2: Detectar cuando B contesta (polling)
340
307
  const pollInterval = setInterval(async () => {
341
- const { activeCalls } = await voip.getActiveCalls();
342
-
343
- // Filtrar llamadas de la extensión del agente
344
- const agentExt = '1001';
345
- const myCalls = activeCalls.filter(c =>
346
- c.caller_id_number === agentExt || c.destination === agentExt
347
- );
348
-
349
- // ¿Hay 2 o más llamadas activas?
350
- if (myCalls.length >= 2) {
351
- // Mostrar botón "Fusionar" con opción de seleccionar cuáles 2
352
- showMergeButton(myCalls);
353
- } else {
354
- hideMergeButton();
355
- }
356
- }, 5000);
357
-
358
- // ═══════════════════════════════════════════════════════
359
- // PASO 2: Fusionar las 2 llamadas seleccionadas
360
- // ═══════════════════════════════════════════════════════
361
-
362
- async function handleMerge(callA: ActiveCall, callB: ActiveCall) {
363
- try {
364
- const result = await voip.mergeDirectCalls({
365
- callUuidA: callA.uuid, // ⚠️ uuid de FreeSWITCH, NO call_uuid
366
- callUuidB: callB.uuid,
367
- });
368
-
369
- console.log('Conferencia creada:', result.conferenceName);
370
- // result.participants → Array con los 3 participantes
371
- // La conferencia se destruye sola cuando el agente cuelga
372
-
373
- } catch (err) {
374
- console.error('Error al fusionar:', err);
308
+ const { conference } = await voip.getConferenceMembers(result.conferenceName);
309
+ const activeCount = conference.members.filter(m => !m.muted || !m.deaf).length;
310
+ if (activeCount >= 2) {
311
+ clearInterval(pollInterval);
312
+ // B contestó ahora puedes swap/merge
375
313
  }
376
- }
377
-
378
- // ═══════════════════════════════════════════════════════
379
- // LIMPIEZA
380
- // ═══════════════════════════════════════════════════════
314
+ }, 2000);
381
315
 
382
- // Cuando el componente se desmonta:
383
- clearInterval(pollInterval);
384
- ```
385
-
386
- ##### 2.1 Formato de `ActiveCall` (de `getActiveCalls()`)
387
-
388
- ```typescript
389
- interface ActiveCall {
390
- uuid: string; // UUID del canal FreeSWITCH — USAR ESTE
391
- call_uuid: string; // UUID de la sesión de llamada (compartido entre A-leg y B-leg)
392
- caller_id_number: string; // Quién llama (extensión o número externo)
393
- caller_id_name: string; // Nombre del caller
394
- destination: string; // Número marcado
395
- direction: string; // 'inbound' | 'outbound'
396
- callstate: string; // 'ACTIVE' | 'EARLY' | 'RINGING' | 'HANGUP'
397
- answered: boolean;
398
- on_hold: boolean;
399
- }
400
- ```
316
+ // PASO 3a: Swap (hablar con A, poner B en hold)
317
+ await voip.swapCallParticipant({
318
+ conferenceName: result.conferenceName,
319
+ keepUuid: personA_uuid, // persona con quien hablar
320
+ holdUuid: personB_uuid, // persona que va a hold
321
+ });
401
322
 
402
- ##### 2.2 — IMPORTANTE: No confundir los UUIDs
323
+ // PASO 3b: Merge (todos hablan)
324
+ await voip.mergeThreeWay({
325
+ conferenceName: result.conferenceName,
326
+ });
403
327
 
404
- ```
405
- activeCall.uuid → "a1b2c3d4-..." ← USAR ESTE para mergeDirectCalls
406
- activeCall.call_uuid → "a1b2c3d4-..." NO USAR (es el mismo que el de la otra pata)
328
+ // PASO 3c: Cancel (B no contestó)
329
+ await voip.cancelAddParticipant({
330
+ conferenceName: result.conferenceName,
331
+ newCallUuid: result.newCallUuid,
332
+ heldMemberUuid: result.heldMemberUuid,
333
+ });
407
334
 
408
- Una llamada tiene 2 canales (A-leg y B-leg) que comparten el MISMO call_uuid
409
- pero tienen DIFERENTE uuid. mergeDirectCalls necesita los uuid de canal.
335
+ // PASO 4: Kick (sacar a alguien)
336
+ await voip.kickParticipant({
337
+ conferenceName: result.conferenceName,
338
+ memberUuid: personB_uuid,
339
+ });
410
340
  ```
411
341
 
412
342
  ---
413
343
 
414
- #### FLOW 3 `addCallParticipant` + `mergeCalls` sin hook (VoIPClient directo)
415
-
416
- Si NO estás usando `useVoIP` (estás usando `VoIPClient` + `WebRTCPhone` manualmente):
417
-
418
- ```typescript
419
- import { VoIPClient, WebRTCPhone } from '@cemscale-voip/voip-sdk';
420
-
421
- const voip = new VoIPClient({ apiUrl: '...', apiKey: '...' });
422
- const phone = new WebRTCPhone({ ... });
423
-
424
- // ════════ PASO 1: Hacer llamada a Persona A ════════
425
- const callInfo = await phone.call('+17863402240');
426
- // Esperar: callInfo.state === 'established'
427
-
428
- // ════════ PASO 2: Resolver el UUID de FreeSWITCH ════════
429
- const fsUuid = await voip.resolveCallFsUuid(
430
- callInfo.remoteIdentity.includes('100') ? callInfo.remoteIdentity : '1001', // extensión
431
- callInfo.remoteIdentity, // número remoto
432
- callInfo.direction // 'outbound' | 'inbound'
433
- );
434
-
435
- if (!fsUuid) {
436
- throw new Error('No se pudo resolver el UUID de FreeSWITCH. ¿La llamada sigue activa?');
437
- }
438
- // fsUuid → "53c4b1ef-487c-438a-8254-c99ef17a3a26" (UUID real de FreeSWITCH)
439
-
440
- // ════════ PASO 3: Agregar Persona B ════════
441
- const { newCallUuid, originalBridgedLeg } = await voip.addCallParticipant(fsUuid, {
442
- toNumber: '+17866302522', // Número externo (E.164)
443
- // toExtension: '1004', // O extensión interna
444
- });
445
-
446
- // ⚠️ Persona B está SONANDO (no ha contestado)
447
- // newCallUuid → UUID del canal de Persona B
448
- // originalBridgedLeg → UUID de la pata PSTN de Persona A
449
-
450
- // ════════ PASO 4: Esperar a que Persona B conteste ════════
451
- // El agente escucha que Persona B contestó, o la UI muestra el botón "Fusionar"
452
-
453
- // ════════ PASO 5: Fusionar ════════
454
- const result = await voip.mergeCalls({
455
- callUuidA: fsUuid, // UUID de FreeSWITCH de la llamada original
456
- callUuidB: newCallUuid, // UUID de Persona B (de addCallParticipant)
457
- bridgedLegA: originalBridgedLeg, // UUID de la pata PSTN de Persona A
458
- conferenceName: undefined, // Auto-generado si no se especifica
459
- });
344
+ #### MÁQUINA DE ESTADOS de threeWaySession
460
345
 
461
- // result.conferenceName → "page_15d0851a_..."
462
- // result.participants → [{ callerIdNumber, callerIdName, uuid, status, memberId }, ...]
463
- // result.state → 'active'
346
+ ```
347
+ null ──addParticipant()──> { state: 'dialing', pendingUuid: '...' }
348
+ │ │
349
+ │ B contesta (polling automático)
350
+ │ │
351
+ │ ▼
352
+ │ { state: 'holding', participants: [...] }
353
+ │ │ │ │
354
+ │ swapParticipant() mergeThreeWay() dropParticipant()
355
+ │ │ │ │
356
+ │ ▼ ▼ │
357
+ │ { state: 'holding' } { state: 'active' }
358
+ │ │ │
359
+ │ swapParticipant() dropParticipant() (≤1 left)
360
+ │ │ │
361
+ │ ▼ ▼
362
+ │ { state: 'holding' } null
363
+
364
+ │──cancelAddParticipant()──> null (B killed, A unheld, 2-person conference)
365
+
366
+ │──currentCall === null──> null (cleanup automático)
464
367
  ```
465
368
 
466
369
  ---
467
370
 
468
- #### RESUMEN de estados y UUIDs
371
+ #### Métodos de tres vías — Referencia completa
469
372
 
470
- | Contexto | Qué UUID usar | De dónde viene |
471
- |----------|--------------|----------------|
472
- | `addParticipant` | `currentCall.fsChannelUuid` | Resuelto automático por useVoIP al conectar |
473
- | `mergeThreeWay` (callUuidA) | `currentCall.fsChannelUuid` | Mismo que arriba |
474
- | `mergeThreeWay` (callUuidB) | `threeWaySession.pendingUuid` | Retornado por `addParticipant` |
475
- | `mergeThreeWay` (bridgedLegA) | `threeWaySession.originalBridgedLeg` | Retornado por `addParticipant` |
476
- | `mergeDirectCalls` (callUuidA/B) | `activeCall.uuid` | De `getActiveCalls()` |
477
- | `resolveCallFsUuid` (manual) | Retorna `string \| null` | Lo usas para `addCallParticipant` |
478
- | `swapParticipant` (keepUuid/holdUuid) | `participant.uuid` | De `threeWaySession.participants[i].uuid` |
479
- | `dropParticipant` (memberId) | `participant.memberId` | De `threeWaySession.participants[i].memberId` |
373
+ | Método | Parámetros | Qué hace |
374
+ |--------|-----------|----------|
375
+ | `addCallParticipant(fsUuid, { toNumber?, toExtension? })` | UUID de FS, destino | Transfiere ambas patas a conferencia, hold A, originate B |
376
+ | `cancelAddParticipant({ conferenceName, newCallUuid, heldMemberUuid })` | Datos de addCallParticipant | Mata B, desmutea A, conferencia 2 personas |
377
+ | `swapCallParticipant({ conferenceName, keepUuid, holdUuid })` | Conference, UUIDs | Mute+deaf+MOH holdUuid, unmute+undeaf keepUuid |
378
+ | `mergeThreeWay({ conferenceName })` | Conference | Desmutea+undeaf TODOS los en hold |
379
+ | `kickParticipant({ conferenceName, memberUuid })` | Conference, UUID | Saca participante de conferencia (cuelga) |
380
+ | `getConferenceMembers(conferenceName)` | Conference | Polling: miembros actuales de la conferencia |
480
381
 
481
- | NO USAR NUNCA | Por qué |
482
- |------------------|---------|
483
- | `currentCall.id` | Es el ID local de SIP.js, no el UUID de FreeSWITCH |
484
- | `activeCall.call_uuid` | Es compartido entre A-leg y B-leg, no identifica un canal único |
382
+ #### Deprecated methods
485
383
 
486
- ---
487
-
488
- #### MÁQUINA DE ESTADOS de threeWaySession
489
-
490
- ```
491
- null ──addParticipant()──> { state: 'adding', pendingUuid: '...' }
492
-
493
- mergeThreeWay()
494
-
495
-
496
- { state: 'active', participants: [...] }
497
-
498
- swapParticipant()
499
-
500
-
501
- { state: 'swapping', ... }
502
- │ (automático al completar)
503
-
504
- { state: 'active', participants: [...] }
505
-
506
- dropParticipant() (si quedan ≤1)
507
-
508
-
509
- null
510
-
511
- currentCall === null
512
-
513
-
514
- null (limpieza automática)
515
- ```
384
+ | Método | Reemplazo |
385
+ |--------|-----------|
386
+ | `mergeCalls(params)` | `mergeThreeWay({ conferenceName })` |
387
+ | `mergeDirectCalls(params)` | `addCallParticipant()` + `mergeThreeWay()` |
516
388
 
517
389
  ### Extensions
518
390