@telnyx/react-voice-commons-sdk 0.4.3 → 1.1.0
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/CHANGELOG.md +23 -0
- package/README.md +28 -7
- package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +74 -1
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +36 -1
- package/ios/CallKitBridge.m +20 -1
- package/ios/CallKitBridge.swift +935 -60
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +21 -1
- package/ios/VoicePnBridge.swift +62 -1
- package/lib/callkit/callkit-coordinator.d.ts +39 -3
- package/lib/callkit/callkit-coordinator.js +561 -148
- package/lib/callkit/callkit.d.ts +7 -0
- package/lib/callkit/callkit.js +83 -11
- package/lib/hooks/useAppStateHandler.js +6 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +9 -0
- package/lib/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +39 -1
- package/lib/internal/calls/call-state-controller.js +152 -64
- package/lib/internal/session/session-manager.d.ts +16 -1
- package/lib/internal/session/session-manager.js +229 -58
- package/lib/internal/voice-pn-bridge.d.ts +44 -0
- package/lib/internal/voice-pn-bridge.js +94 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +18 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +48 -5
- package/lib/telnyx-voip-client.js +276 -22
- package/package.json +7 -5
- package/src/callkit/callkit-coordinator.ts +642 -160
- package/src/callkit/callkit.ts +110 -12
- package/src/hooks/useAppStateHandler.ts +7 -2
- package/src/index.ts +3 -0
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +173 -74
- package/src/internal/session/session-manager.ts +277 -61
- package/src/internal/voice-pn-bridge.ts +111 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +18 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +260 -24
- package/src/types/telnyx-sdk.d.ts +12 -1
|
@@ -5,7 +5,9 @@ import { Call } from '../../models/call';
|
|
|
5
5
|
import { TelnyxCallState } from '../../models/call-state';
|
|
6
6
|
import { SessionManager } from '../session/session-manager';
|
|
7
7
|
import { callKitCoordinator } from '../../callkit/callkit-coordinator';
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
export type CustomHeader = { name: string; value: string };
|
|
10
|
+
export type CustomHeaders = Record<string, string> | CustomHeader[];
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Central state machine for call management.
|
|
@@ -17,11 +19,18 @@ export class CallStateController {
|
|
|
17
19
|
private readonly _calls = new BehaviorSubject<Call[]>([]);
|
|
18
20
|
private readonly _callMap = new Map<string, Call>();
|
|
19
21
|
private _disposed = false;
|
|
22
|
+
private _listenerClient?: TelnyxRTC;
|
|
20
23
|
|
|
21
24
|
// Callbacks for waiting for invite logic (used for push notifications)
|
|
22
25
|
private _isWaitingForInvite?: () => boolean;
|
|
23
26
|
private _onInviteAutoAccepted?: () => void;
|
|
24
27
|
|
|
28
|
+
// Explicitly-tracked active call ID for multi-call scenarios.
|
|
29
|
+
// When set, activeCall$/currentActiveCall prefer this call over the
|
|
30
|
+
// first-match heuristic. Cleared automatically when the call reaches
|
|
31
|
+
// a terminal state, or manually via clearActiveCall().
|
|
32
|
+
private _activeCallId: string | null = null;
|
|
33
|
+
|
|
25
34
|
constructor(private readonly _sessionManager: SessionManager) {
|
|
26
35
|
console.log('CallStateController: Constructor called - instance created');
|
|
27
36
|
// Don't set up client listeners here - client doesn't exist yet
|
|
@@ -41,18 +50,18 @@ export class CallStateController {
|
|
|
41
50
|
get activeCall$(): Observable<Call | null> {
|
|
42
51
|
return this.calls$.pipe(
|
|
43
52
|
map((calls) => {
|
|
44
|
-
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
call.currentState === TelnyxCallState.ACTIVE ||
|
|
51
|
-
call.currentState === TelnyxCallState.HELD
|
|
52
|
-
) || null
|
|
53
|
-
);
|
|
53
|
+
const call = this._selectActiveCall(calls);
|
|
54
|
+
return {
|
|
55
|
+
call,
|
|
56
|
+
callId: call?.callId ?? null,
|
|
57
|
+
state: call?.currentState ?? null,
|
|
58
|
+
};
|
|
54
59
|
}),
|
|
55
|
-
distinctUntilChanged(
|
|
60
|
+
distinctUntilChanged(
|
|
61
|
+
(previous, current) =>
|
|
62
|
+
previous.callId === current.callId && previous.state === current.state
|
|
63
|
+
),
|
|
64
|
+
map(({ call }) => call)
|
|
56
65
|
);
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -67,16 +76,7 @@ export class CallStateController {
|
|
|
67
76
|
* Current active call (synchronous access)
|
|
68
77
|
*/
|
|
69
78
|
get currentActiveCall(): Call | null {
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
calls.find(
|
|
73
|
-
(call) =>
|
|
74
|
-
call.currentState === TelnyxCallState.RINGING ||
|
|
75
|
-
call.currentState === TelnyxCallState.CONNECTING ||
|
|
76
|
-
call.currentState === TelnyxCallState.ACTIVE ||
|
|
77
|
-
call.currentState === TelnyxCallState.HELD
|
|
78
|
-
) || null
|
|
79
|
-
);
|
|
79
|
+
return this._selectActiveCall(this.currentCalls);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -105,6 +105,34 @@ export class CallStateController {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Explicitly set the active call for multi-call scenarios.
|
|
110
|
+
* When set, activeCall$ and currentActiveCall prefer this call over
|
|
111
|
+
* the first-match heuristic. The ID is cleared automatically when the
|
|
112
|
+
* call reaches a terminal state.
|
|
113
|
+
* @param callId The ID of the call to mark as active
|
|
114
|
+
*/
|
|
115
|
+
setActiveCall(callId: string): void {
|
|
116
|
+
const call = this._callMap.get(callId);
|
|
117
|
+
if (call) {
|
|
118
|
+
this._activeCallId = callId;
|
|
119
|
+
this._calls.next([...this.currentCalls]);
|
|
120
|
+
} else {
|
|
121
|
+
console.warn('CallStateController: Cannot set active call - call not found:', callId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear the explicitly-tracked active call ID, reverting to the
|
|
127
|
+
* first-match heuristic for active call selection.
|
|
128
|
+
*/
|
|
129
|
+
clearActiveCall(): void {
|
|
130
|
+
if (this._activeCallId !== null) {
|
|
131
|
+
this._activeCallId = null;
|
|
132
|
+
this._calls.next([...this.currentCalls]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
/**
|
|
109
137
|
* Find a call by its underlying Telnyx call ID
|
|
110
138
|
* @param telnyxCall The Telnyx call object to find
|
|
@@ -118,6 +146,33 @@ export class CallStateController {
|
|
|
118
146
|
return null;
|
|
119
147
|
}
|
|
120
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Select the active call, preferring the explicitly-tracked call ID
|
|
151
|
+
* over the first-match heuristic.
|
|
152
|
+
*/
|
|
153
|
+
private _selectActiveCall(calls: Call[]): Call | null {
|
|
154
|
+
if (this._activeCallId) {
|
|
155
|
+
const tracked = calls.find((c) => c.callId === this._activeCallId);
|
|
156
|
+
if (tracked && this._isNonTerminal(tracked)) {
|
|
157
|
+
return tracked;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Fall back to first non-terminal call (backward compatible)
|
|
161
|
+
return calls.find((c) => this._isNonTerminal(c)) || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if a call is in a non-terminal (active or connecting) state.
|
|
166
|
+
*/
|
|
167
|
+
private _isNonTerminal(call: Call): boolean {
|
|
168
|
+
return (
|
|
169
|
+
call.currentState === TelnyxCallState.RINGING ||
|
|
170
|
+
call.currentState === TelnyxCallState.CONNECTING ||
|
|
171
|
+
call.currentState === TelnyxCallState.ACTIVE ||
|
|
172
|
+
call.currentState === TelnyxCallState.HELD
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
121
176
|
/**
|
|
122
177
|
* Initialize client listeners when the Telnyx client becomes available
|
|
123
178
|
* This should be called by the session manager after client creation
|
|
@@ -138,7 +193,7 @@ export class CallStateController {
|
|
|
138
193
|
destination: string,
|
|
139
194
|
callerName?: string,
|
|
140
195
|
callerNumber?: string,
|
|
141
|
-
customHeaders?:
|
|
196
|
+
customHeaders?: CustomHeaders
|
|
142
197
|
): Promise<Call> {
|
|
143
198
|
if (this._disposed) {
|
|
144
199
|
throw new Error('CallStateController has been disposed');
|
|
@@ -154,7 +209,10 @@ export class CallStateController {
|
|
|
154
209
|
destinationNumber: destination,
|
|
155
210
|
callerIdName: callerName,
|
|
156
211
|
callerIdNumber: callerNumber,
|
|
157
|
-
customHeaders,
|
|
212
|
+
customHeaders: this._normalizeCustomHeaders(customHeaders),
|
|
213
|
+
peerConnectionOptions: {
|
|
214
|
+
useTrickleIce: this._sessionManager.useTrickleIce,
|
|
215
|
+
},
|
|
158
216
|
};
|
|
159
217
|
const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
|
|
160
218
|
|
|
@@ -179,6 +237,21 @@ export class CallStateController {
|
|
|
179
237
|
}
|
|
180
238
|
}
|
|
181
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Normalize public custom headers into the format expected by the underlying SDK.
|
|
242
|
+
*/
|
|
243
|
+
private _normalizeCustomHeaders(customHeaders?: CustomHeaders): CustomHeader[] | undefined {
|
|
244
|
+
if (!customHeaders) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (Array.isArray(customHeaders)) {
|
|
249
|
+
return customHeaders;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return Object.entries(customHeaders).map(([name, value]) => ({ name, value }));
|
|
253
|
+
}
|
|
254
|
+
|
|
182
255
|
/**
|
|
183
256
|
* Set callbacks for waiting for invite logic (used for push notifications)
|
|
184
257
|
*/
|
|
@@ -213,6 +286,7 @@ export class CallStateController {
|
|
|
213
286
|
}
|
|
214
287
|
}
|
|
215
288
|
this._callMap.clear();
|
|
289
|
+
this._activeCallId = null;
|
|
216
290
|
this._calls.next([]);
|
|
217
291
|
}
|
|
218
292
|
|
|
@@ -225,10 +299,12 @@ export class CallStateController {
|
|
|
225
299
|
}
|
|
226
300
|
|
|
227
301
|
this._disposed = true;
|
|
302
|
+
this._removeClientListeners();
|
|
228
303
|
|
|
229
304
|
// Dispose of all calls
|
|
230
305
|
this.currentCalls.forEach((call) => call.dispose());
|
|
231
306
|
this._callMap.clear();
|
|
307
|
+
this._activeCallId = null;
|
|
232
308
|
|
|
233
309
|
// CallKit cleanup is now handled by CallKitCoordinator automatically
|
|
234
310
|
|
|
@@ -241,40 +317,28 @@ export class CallStateController {
|
|
|
241
317
|
private _setupClientListeners(): void {
|
|
242
318
|
console.log('CallStateController: Setting up client listeners...');
|
|
243
319
|
|
|
244
|
-
|
|
320
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
321
|
+
|
|
322
|
+
if (!telnyxClient) {
|
|
245
323
|
console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
|
|
246
324
|
return;
|
|
247
325
|
}
|
|
248
326
|
|
|
327
|
+
this._removeClientListeners();
|
|
328
|
+
this._listenerClient = telnyxClient;
|
|
329
|
+
|
|
249
330
|
console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
|
|
250
|
-
console.log(
|
|
251
|
-
'CallStateController: Client instance:',
|
|
252
|
-
this._sessionManager.telnyxClient.constructor.name
|
|
253
|
-
);
|
|
331
|
+
console.log('CallStateController: Client instance:', telnyxClient.constructor.name);
|
|
254
332
|
|
|
255
333
|
// Listen for incoming calls
|
|
256
|
-
|
|
257
|
-
'telnyx.call.incoming',
|
|
258
|
-
(telnyxCall: TelnyxCall, msg: any) => {
|
|
259
|
-
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
260
|
-
this._handleIncomingCall(telnyxCall, msg, false);
|
|
261
|
-
}
|
|
262
|
-
);
|
|
334
|
+
telnyxClient.on('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
263
335
|
|
|
264
336
|
// Listen for reattached calls (after network reconnection)
|
|
265
|
-
|
|
266
|
-
'telnyx.call.reattached',
|
|
267
|
-
(telnyxCall: TelnyxCall, msg: any) => {
|
|
268
|
-
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
269
|
-
this._handleIncomingCall(telnyxCall, msg, true);
|
|
270
|
-
}
|
|
271
|
-
);
|
|
337
|
+
telnyxClient.on('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
272
338
|
|
|
273
339
|
// Verify listeners are set up
|
|
274
|
-
const incomingListeners =
|
|
275
|
-
|
|
276
|
-
const reattachedListeners =
|
|
277
|
-
this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
|
|
340
|
+
const incomingListeners = telnyxClient.listenerCount('telnyx.call.incoming');
|
|
341
|
+
const reattachedListeners = telnyxClient.listenerCount('telnyx.call.reattached');
|
|
278
342
|
console.log(
|
|
279
343
|
'CallStateController: Listeners registered - incoming:',
|
|
280
344
|
incomingListeners,
|
|
@@ -283,34 +347,57 @@ export class CallStateController {
|
|
|
283
347
|
);
|
|
284
348
|
|
|
285
349
|
// Listen for call state changes from the TelnyxRTC client (multi-call support)
|
|
286
|
-
|
|
287
|
-
'telnyx.call.stateChanged',
|
|
288
|
-
(telnyxCall: TelnyxCall, state: string) => {
|
|
289
|
-
console.log(
|
|
290
|
-
'CallStateController: Call state changed from TelnyxRTC:',
|
|
291
|
-
telnyxCall.callId,
|
|
292
|
-
state
|
|
293
|
-
);
|
|
294
|
-
// Find our wrapper call and update if needed
|
|
295
|
-
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
296
|
-
if (call) {
|
|
297
|
-
console.log(
|
|
298
|
-
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
);
|
|
350
|
+
telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
303
351
|
|
|
304
352
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
305
|
-
|
|
306
|
-
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
307
|
-
// The call cleanup is already handled by our call state subscription
|
|
308
|
-
// This event is informational for logging/debugging
|
|
309
|
-
});
|
|
353
|
+
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
310
354
|
|
|
311
355
|
console.log('CallStateController: Client listeners set up successfully');
|
|
312
356
|
}
|
|
313
357
|
|
|
358
|
+
private _removeClientListeners(): void {
|
|
359
|
+
if (!this._listenerClient) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this._listenerClient.off('telnyx.call.incoming', this._handleTelnyxIncomingCall);
|
|
364
|
+
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
365
|
+
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
366
|
+
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
367
|
+
this._listenerClient = undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private _handleTelnyxIncomingCall = (telnyxCall: TelnyxCall, msg: any): void => {
|
|
371
|
+
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
372
|
+
this._handleIncomingCall(telnyxCall, msg, false);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
private _handleTelnyxReattachedCall = (telnyxCall: TelnyxCall, msg: any): void => {
|
|
376
|
+
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
377
|
+
this._handleIncomingCall(telnyxCall, msg, true);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
private _handleTelnyxCallStateChanged = (telnyxCall: TelnyxCall, state: string): void => {
|
|
381
|
+
console.log(
|
|
382
|
+
'CallStateController: Call state changed from TelnyxRTC:',
|
|
383
|
+
telnyxCall.callId,
|
|
384
|
+
state
|
|
385
|
+
);
|
|
386
|
+
// Find our wrapper call and update if needed
|
|
387
|
+
const call = this.findCallByTelnyxCall(telnyxCall);
|
|
388
|
+
if (call) {
|
|
389
|
+
console.log(
|
|
390
|
+
'CallStateController: Found wrapper call, state sync handled by Call subscription'
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
private _handleTelnyxCallRemoved = (callId: string): void => {
|
|
396
|
+
console.log('CallStateController: Call removed from TelnyxRTC:', callId);
|
|
397
|
+
// The call cleanup is already handled by our call state subscription.
|
|
398
|
+
// This event is informational for logging/debugging.
|
|
399
|
+
};
|
|
400
|
+
|
|
314
401
|
/**
|
|
315
402
|
* Handle incoming call or reattached call
|
|
316
403
|
*/
|
|
@@ -399,6 +486,11 @@ export class CallStateController {
|
|
|
399
486
|
private _addCall(call: Call): void {
|
|
400
487
|
this._callMap.set(call.callId, call);
|
|
401
488
|
|
|
489
|
+
// Auto-track as active if no active call is currently set
|
|
490
|
+
if (this._activeCallId === null) {
|
|
491
|
+
this._activeCallId = call.callId;
|
|
492
|
+
}
|
|
493
|
+
|
|
402
494
|
const currentCalls = this.currentCalls;
|
|
403
495
|
currentCalls.push(call);
|
|
404
496
|
this._calls.next([...currentCalls]);
|
|
@@ -413,9 +505,10 @@ export class CallStateController {
|
|
|
413
505
|
|
|
414
506
|
if (existingCallKitUUID) {
|
|
415
507
|
console.log(
|
|
416
|
-
'CallStateController:
|
|
508
|
+
'CallStateController: Linking push-created CallKit call to its signaling call:',
|
|
417
509
|
existingCallKitUUID
|
|
418
510
|
);
|
|
511
|
+
callKitCoordinator.linkExistingCallKitCall(telnyxCall, existingCallKitUUID);
|
|
419
512
|
} else if (call.isIncoming) {
|
|
420
513
|
// Handle incoming call with CallKit (only if not already integrated)
|
|
421
514
|
console.log('CallStateController: Reporting incoming call to CallKitCoordinator');
|
|
@@ -432,13 +525,19 @@ export class CallStateController {
|
|
|
432
525
|
// CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
|
|
433
526
|
console.log('CallStateController: Call state changed to:', state);
|
|
434
527
|
|
|
528
|
+
// Re-emit the call list so calls$/activeCall$ subscribers see state changes.
|
|
529
|
+
// distinctUntilChanged uses reference equality; without a new array
|
|
530
|
+
// reference, non-terminal transitions leave activeCall$ stale.
|
|
531
|
+
this._calls.next([...this.currentCalls]);
|
|
532
|
+
|
|
435
533
|
// Clean up when call ends - delay to next tick so external subscribers
|
|
436
534
|
// receive the ENDED/FAILED state before the call is disposed
|
|
437
535
|
if (state === TelnyxCallState.ENDED || state === TelnyxCallState.FAILED) {
|
|
438
|
-
// Clear
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
536
|
+
// Clear active call ID if the ending call was the tracked active call
|
|
537
|
+
if (this._activeCallId === call.callId) {
|
|
538
|
+
this._activeCallId = null;
|
|
539
|
+
}
|
|
540
|
+
|
|
442
541
|
setTimeout(() => this._removeCall(call.callId), 0);
|
|
443
542
|
}
|
|
444
543
|
});
|