@telnyx/react-voice-commons-sdk 1.0.0 → 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/TelnyxNotificationHelper.kt +345 -24
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +19 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +19 -0
- package/ios/CallKitBridge.m +20 -1
- package/ios/CallKitBridge.swift +527 -78
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +6 -0
- package/ios/VoicePnBridge.swift +15 -0
- 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/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +33 -1
- package/lib/internal/calls/call-state-controller.js +100 -28
- package/lib/internal/session/session-manager.d.ts +15 -1
- package/lib/internal/session/session-manager.js +214 -62
- package/lib/internal/voice-pn-bridge.d.ts +18 -0
- package/lib/internal/voice-pn-bridge.js +39 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +10 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +46 -5
- package/lib/telnyx-voip-client.js +238 -22
- package/package.json +6 -4
- package/src/callkit/callkit-coordinator.ts +642 -160
- package/src/callkit/callkit.ts +110 -12
- package/src/hooks/useAppStateHandler.ts +7 -2
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +112 -29
- package/src/internal/session/session-manager.ts +260 -65
- package/src/internal/voice-pn-bridge.ts +47 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +10 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +216 -24
- package/src/types/telnyx-sdk.d.ts +2 -1
package/lib/callkit/callkit.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare enum CallEndReason {
|
|
|
7
7
|
}
|
|
8
8
|
export interface CallKitEvent {
|
|
9
9
|
callUUID: string;
|
|
10
|
+
isOnHold?: boolean;
|
|
10
11
|
[key: string]: any;
|
|
11
12
|
}
|
|
12
13
|
declare class CallKitManager {
|
|
@@ -33,7 +34,11 @@ declare class CallKitManager {
|
|
|
33
34
|
startOutgoingCall(callUUID: string, handle: string, displayName: string): Promise<boolean>;
|
|
34
35
|
reportIncomingCall(callUUID: string, handle: string, displayName: string): Promise<boolean>;
|
|
35
36
|
answerCall(callUUID: string): Promise<boolean>;
|
|
37
|
+
isCallRegistered(callUUID: string): Promise<boolean>;
|
|
36
38
|
endCall(callUUID: string): Promise<boolean>;
|
|
39
|
+
completeHeldCallAction(callUUID: string, success: boolean): Promise<boolean>;
|
|
40
|
+
setCallHeld(callUUID: string, isOnHold: boolean): Promise<boolean>;
|
|
41
|
+
swapCalls(activeCallUUID: string, heldCallUUID: string): Promise<boolean>;
|
|
37
42
|
reportCallConnected(callUUID: string): Promise<boolean>;
|
|
38
43
|
reportCallEnded(callUUID: string, reason?: CallEndReason): Promise<boolean>;
|
|
39
44
|
updateCall(callUUID: string, displayName: string, handle: string): Promise<boolean>;
|
|
@@ -41,7 +46,9 @@ declare class CallKitManager {
|
|
|
41
46
|
onStartCall(listener: (event: CallKitEvent) => void): () => void;
|
|
42
47
|
onAnswerCall(listener: (event: CallKitEvent) => void): () => void;
|
|
43
48
|
onEndCall(listener: (event: CallKitEvent) => void): () => void;
|
|
49
|
+
onHeldCall(listener: (event: CallKitEvent) => void): () => void;
|
|
44
50
|
onReceivePush(listener: (event: CallKitEvent) => void): () => void;
|
|
51
|
+
private addListener;
|
|
45
52
|
generateCallUUID(): string;
|
|
46
53
|
isAvailable(): boolean;
|
|
47
54
|
}
|
package/lib/callkit/callkit.js
CHANGED
|
@@ -68,6 +68,11 @@ class CallKitManager {
|
|
|
68
68
|
console.log('CallKit: Received end call action', normalizedEvent);
|
|
69
69
|
this.notifyListeners('endCall', normalizedEvent);
|
|
70
70
|
});
|
|
71
|
+
this.eventEmitter.addListener('CallKitDidPerformHeldCallAction', (event) => {
|
|
72
|
+
const normalizedEvent = this.normalizeEvent(event);
|
|
73
|
+
console.log('CallKit: Received held call action', normalizedEvent);
|
|
74
|
+
this.notifyListeners('heldCall', normalizedEvent);
|
|
75
|
+
});
|
|
71
76
|
this.eventEmitter.addListener('CallKitDidReceivePush', (event) => {
|
|
72
77
|
const normalizedEvent = this.normalizeEvent(event);
|
|
73
78
|
console.log('CallKit: Received push notification event', normalizedEvent);
|
|
@@ -75,9 +80,11 @@ class CallKitManager {
|
|
|
75
80
|
});
|
|
76
81
|
}
|
|
77
82
|
notifyListeners(eventType, event) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
listener
|
|
83
|
+
const listeners = this.listeners.get(eventType);
|
|
84
|
+
if (listeners) {
|
|
85
|
+
for (const listener of listeners) {
|
|
86
|
+
listener(event);
|
|
87
|
+
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
// Public API methods
|
|
@@ -140,6 +147,21 @@ class CallKitManager {
|
|
|
140
147
|
return false;
|
|
141
148
|
}
|
|
142
149
|
}
|
|
150
|
+
async isCallRegistered(callUUID) {
|
|
151
|
+
if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const result = await this.bridge.isCallRegistered(this.denormalizeUUID(callUUID));
|
|
156
|
+
return result.registered;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('CallKit: Failed to check incoming call registration', {
|
|
159
|
+
callUUID,
|
|
160
|
+
error,
|
|
161
|
+
});
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
143
165
|
async endCall(callUUID) {
|
|
144
166
|
if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
|
|
145
167
|
console.warn('CallKit: Not available on this platform');
|
|
@@ -157,6 +179,46 @@ class CallKitManager {
|
|
|
157
179
|
return false;
|
|
158
180
|
}
|
|
159
181
|
}
|
|
182
|
+
async completeHeldCallAction(callUUID, success) {
|
|
183
|
+
if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const uppercaseUUID = this.denormalizeUUID(callUUID);
|
|
188
|
+
const result = await this.bridge.completeHeldCallAction(uppercaseUUID, success);
|
|
189
|
+
return result.success;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('CallKit: Failed to complete held call action', error);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async setCallHeld(callUUID, isOnHold) {
|
|
196
|
+
if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const result = await this.bridge.setCallHeld(this.denormalizeUUID(callUUID), isOnHold);
|
|
201
|
+
return result.success;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('CallKit: Failed to change held state', { callUUID, isOnHold, error });
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async swapCalls(activeCallUUID, heldCallUUID) {
|
|
208
|
+
if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const result = await this.bridge.swapCalls(
|
|
213
|
+
this.denormalizeUUID(activeCallUUID),
|
|
214
|
+
this.denormalizeUUID(heldCallUUID)
|
|
215
|
+
);
|
|
216
|
+
return result.success;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error('CallKit: Failed to swap calls', error);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
160
222
|
async reportCallConnected(callUUID) {
|
|
161
223
|
if (!this.bridge || react_native_1.Platform.OS !== 'ios') {
|
|
162
224
|
return false;
|
|
@@ -220,20 +282,30 @@ class CallKitManager {
|
|
|
220
282
|
}
|
|
221
283
|
// Event listener management
|
|
222
284
|
onStartCall(listener) {
|
|
223
|
-
this.
|
|
224
|
-
return () => this.listeners.delete('startCall');
|
|
285
|
+
return this.addListener('startCall', listener);
|
|
225
286
|
}
|
|
226
287
|
onAnswerCall(listener) {
|
|
227
|
-
this.
|
|
228
|
-
return () => this.listeners.delete('answerCall');
|
|
288
|
+
return this.addListener('answerCall', listener);
|
|
229
289
|
}
|
|
230
290
|
onEndCall(listener) {
|
|
231
|
-
this.
|
|
232
|
-
|
|
291
|
+
return this.addListener('endCall', listener);
|
|
292
|
+
}
|
|
293
|
+
onHeldCall(listener) {
|
|
294
|
+
return this.addListener('heldCall', listener);
|
|
233
295
|
}
|
|
234
296
|
onReceivePush(listener) {
|
|
235
|
-
this.
|
|
236
|
-
|
|
297
|
+
return this.addListener('receivePush', listener);
|
|
298
|
+
}
|
|
299
|
+
addListener(eventType, listener) {
|
|
300
|
+
const listeners = this.listeners.get(eventType) ?? new Set();
|
|
301
|
+
listeners.add(listener);
|
|
302
|
+
this.listeners.set(eventType, listeners);
|
|
303
|
+
return () => {
|
|
304
|
+
listeners.delete(listener);
|
|
305
|
+
if (listeners.size === 0) {
|
|
306
|
+
this.listeners.delete(eventType);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
237
309
|
}
|
|
238
310
|
// Utility methods
|
|
239
311
|
generateCallUUID() {
|
|
@@ -30,8 +30,12 @@ const useAppStateHandler = ({
|
|
|
30
30
|
if (appState.current.match(/active/) && nextAppState === 'background') {
|
|
31
31
|
log('AppStateHandler: App went to background, checking for active calls...');
|
|
32
32
|
const connectionState = voipClient.currentConnectionState;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const shouldHandleBackgroundConnection =
|
|
34
|
+
connectionState === connection_state_1.TelnyxConnectionState.CONNECTED ||
|
|
35
|
+
connectionState === connection_state_1.TelnyxConnectionState.CONNECTING ||
|
|
36
|
+
connectionState === connection_state_1.TelnyxConnectionState.RECONNECTING;
|
|
37
|
+
if (!shouldHandleBackgroundConnection) {
|
|
38
|
+
log('AppStateHandler: Not connected or connecting, skipping background handling');
|
|
35
39
|
appState.current = nextAppState;
|
|
36
40
|
return;
|
|
37
41
|
}
|
|
@@ -11,6 +11,9 @@ interface CallKitHandlerProps {
|
|
|
11
11
|
* Internal CallKit handler for iOS push notifications
|
|
12
12
|
* This component is automatically included in TelnyxVoiceApp
|
|
13
13
|
*
|
|
14
|
+
* The active instance removes only its own DeviceEventEmitter subscriptions on
|
|
15
|
+
* unmount via each listener's `.remove()` method.
|
|
16
|
+
*
|
|
14
17
|
* @internal - Users should not use this component directly
|
|
15
18
|
*/
|
|
16
19
|
export declare const CallKitHandler: React.FC<CallKitHandlerProps>;
|
|
@@ -9,19 +9,68 @@ exports.CallKitHandler = void 0;
|
|
|
9
9
|
const react_1 = require('react');
|
|
10
10
|
const react_native_1 = require('react-native');
|
|
11
11
|
const async_storage_1 = __importDefault(require('@react-native-async-storage/async-storage'));
|
|
12
|
-
|
|
13
|
-
// Global flag to ensure only one CallKitHandler is active
|
|
12
|
+
// Global flag to ensure only one CallKitHandler is active.
|
|
14
13
|
let isCallKitHandlerActive = false;
|
|
15
14
|
/**
|
|
16
15
|
* Internal CallKit handler for iOS push notifications
|
|
17
16
|
* This component is automatically included in TelnyxVoiceApp
|
|
18
17
|
*
|
|
18
|
+
* The active instance removes only its own DeviceEventEmitter subscriptions on
|
|
19
|
+
* unmount via each listener's `.remove()` method.
|
|
20
|
+
*
|
|
19
21
|
* @internal - Users should not use this component directly
|
|
20
22
|
*/
|
|
21
23
|
const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack }) => {
|
|
22
|
-
const { voipClient } = (0, TelnyxVoiceContext_1.useTelnyxVoice)();
|
|
23
24
|
// Store active calls by CallKit UUID for coordination
|
|
24
25
|
const activeCallsRef = (0, react_1.useRef)(new Map());
|
|
26
|
+
// DeviceEventEmitter listeners are registered once below, so read callbacks
|
|
27
|
+
// from refs that are updated during every render rather than stale closures.
|
|
28
|
+
const onLoginRequiredRef = (0, react_1.useRef)(onLoginRequired);
|
|
29
|
+
const onNavigateToDialerRef = (0, react_1.useRef)(onNavigateToDialer);
|
|
30
|
+
const onNavigateBackRef = (0, react_1.useRef)(onNavigateBack);
|
|
31
|
+
onLoginRequiredRef.current = onLoginRequired;
|
|
32
|
+
onNavigateToDialerRef.current = onNavigateToDialer;
|
|
33
|
+
onNavigateBackRef.current = onNavigateBack;
|
|
34
|
+
const handleIncomingCall = async (eventData) => {
|
|
35
|
+
console.log('CallKitHandler: Handling incoming call', {
|
|
36
|
+
callUUID: eventData.callUUID,
|
|
37
|
+
});
|
|
38
|
+
// Store the push notification payload
|
|
39
|
+
await async_storage_1.default.setItem(
|
|
40
|
+
'@push_notification_payload',
|
|
41
|
+
JSON.stringify(eventData.payload)
|
|
42
|
+
);
|
|
43
|
+
// Mark this call as being processed
|
|
44
|
+
activeCallsRef.current.set(eventData.callUUID, {
|
|
45
|
+
processing: true,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
});
|
|
48
|
+
// Trigger login required callback if provided
|
|
49
|
+
if (onLoginRequiredRef.current) {
|
|
50
|
+
onLoginRequiredRef.current(eventData.payload);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const handleAnswerCall = async (eventData) => {
|
|
54
|
+
console.log('CallKitHandler: User answered call via CallKit', {
|
|
55
|
+
callUUID: eventData.callUUID,
|
|
56
|
+
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
57
|
+
});
|
|
58
|
+
if (onNavigateToDialerRef.current) {
|
|
59
|
+
onNavigateToDialerRef.current();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const handleEndCall = async (eventData) => {
|
|
63
|
+
console.log('CallKitHandler: User ended call via CallKit', {
|
|
64
|
+
callUUID: eventData.callUUID,
|
|
65
|
+
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
66
|
+
});
|
|
67
|
+
// Clean up our local tracking info
|
|
68
|
+
activeCallsRef.current.delete(eventData.callUUID);
|
|
69
|
+
await async_storage_1.default.removeItem('@push_notification_payload');
|
|
70
|
+
if (onNavigateBackRef.current) {
|
|
71
|
+
onNavigateBackRef.current();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
25
74
|
(0, react_1.useEffect)(() => {
|
|
26
75
|
if (react_native_1.Platform.OS !== 'ios') return;
|
|
27
76
|
if (isCallKitHandlerActive) {
|
|
@@ -30,8 +79,6 @@ const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack })
|
|
|
30
79
|
}
|
|
31
80
|
isCallKitHandlerActive = true;
|
|
32
81
|
console.log('CallKitHandler: Setting up DeviceEventEmitter listeners (singleton instance)...');
|
|
33
|
-
react_native_1.DeviceEventEmitter.removeAllListeners('incomingVoIPCall');
|
|
34
|
-
react_native_1.DeviceEventEmitter.removeAllListeners('callKitAction');
|
|
35
82
|
const incomingCallListener = react_native_1.DeviceEventEmitter.addListener(
|
|
36
83
|
'incomingVoIPCall',
|
|
37
84
|
async (eventData) => {
|
|
@@ -63,47 +110,6 @@ const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack })
|
|
|
63
110
|
isCallKitHandlerActive = false;
|
|
64
111
|
};
|
|
65
112
|
}, []);
|
|
66
|
-
const handleIncomingCall = async (eventData) => {
|
|
67
|
-
console.log('CallKitHandler: Handling incoming call', {
|
|
68
|
-
callUUID: eventData.callUUID,
|
|
69
|
-
hasClient: !!voipClient,
|
|
70
|
-
});
|
|
71
|
-
// Store the push notification payload
|
|
72
|
-
await async_storage_1.default.setItem(
|
|
73
|
-
'@push_notification_payload',
|
|
74
|
-
JSON.stringify(eventData.payload)
|
|
75
|
-
);
|
|
76
|
-
// Mark this call as being processed
|
|
77
|
-
activeCallsRef.current.set(eventData.callUUID, {
|
|
78
|
-
processing: true,
|
|
79
|
-
timestamp: Date.now(),
|
|
80
|
-
});
|
|
81
|
-
// Trigger login required callback if provided
|
|
82
|
-
if (onLoginRequired) {
|
|
83
|
-
onLoginRequired(eventData.payload);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const handleAnswerCall = async (eventData) => {
|
|
87
|
-
console.log('CallKitHandler: User answered call via CallKit', {
|
|
88
|
-
callUUID: eventData.callUUID,
|
|
89
|
-
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
90
|
-
});
|
|
91
|
-
if (onNavigateToDialer) {
|
|
92
|
-
onNavigateToDialer();
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
const handleEndCall = async (eventData) => {
|
|
96
|
-
console.log('CallKitHandler: User ended call via CallKit', {
|
|
97
|
-
callUUID: eventData.callUUID,
|
|
98
|
-
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
99
|
-
});
|
|
100
|
-
// Clean up our local tracking info
|
|
101
|
-
activeCallsRef.current.delete(eventData.callUUID);
|
|
102
|
-
await async_storage_1.default.removeItem('@push_notification_payload');
|
|
103
|
-
if (onNavigateBack) {
|
|
104
|
-
onNavigateBack();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
113
|
// This component doesn't render anything, it just handles events
|
|
108
114
|
return null;
|
|
109
115
|
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import { Call } from '../../models/call';
|
|
3
3
|
import { SessionManager } from '../session/session-manager';
|
|
4
|
+
export type CustomHeader = {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
export type CustomHeaders = Record<string, string> | CustomHeader[];
|
|
4
9
|
/**
|
|
5
10
|
* Central state machine for call management.
|
|
6
11
|
*
|
|
@@ -15,6 +20,7 @@ export declare class CallStateController {
|
|
|
15
20
|
private _listenerClient?;
|
|
16
21
|
private _isWaitingForInvite?;
|
|
17
22
|
private _onInviteAutoAccepted?;
|
|
23
|
+
private _activeCallId;
|
|
18
24
|
constructor(_sessionManager: SessionManager);
|
|
19
25
|
/**
|
|
20
26
|
* Observable stream of all current calls
|
|
@@ -46,11 +52,33 @@ export declare class CallStateController {
|
|
|
46
52
|
* @param callId The ID of the call to set to connecting state
|
|
47
53
|
*/
|
|
48
54
|
setCallConnecting(callId: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Explicitly set the active call for multi-call scenarios.
|
|
57
|
+
* When set, activeCall$ and currentActiveCall prefer this call over
|
|
58
|
+
* the first-match heuristic. The ID is cleared automatically when the
|
|
59
|
+
* call reaches a terminal state.
|
|
60
|
+
* @param callId The ID of the call to mark as active
|
|
61
|
+
*/
|
|
62
|
+
setActiveCall(callId: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Clear the explicitly-tracked active call ID, reverting to the
|
|
65
|
+
* first-match heuristic for active call selection.
|
|
66
|
+
*/
|
|
67
|
+
clearActiveCall(): void;
|
|
49
68
|
/**
|
|
50
69
|
* Find a call by its underlying Telnyx call ID
|
|
51
70
|
* @param telnyxCall The Telnyx call object to find
|
|
52
71
|
*/
|
|
53
72
|
findCallByTelnyxCall(telnyxCall: any): Call | null;
|
|
73
|
+
/**
|
|
74
|
+
* Select the active call, preferring the explicitly-tracked call ID
|
|
75
|
+
* over the first-match heuristic.
|
|
76
|
+
*/
|
|
77
|
+
private _selectActiveCall;
|
|
78
|
+
/**
|
|
79
|
+
* Check if a call is in a non-terminal (active or connecting) state.
|
|
80
|
+
*/
|
|
81
|
+
private _isNonTerminal;
|
|
54
82
|
/**
|
|
55
83
|
* Initialize client listeners when the Telnyx client becomes available
|
|
56
84
|
* This should be called by the session manager after client creation
|
|
@@ -63,8 +91,12 @@ export declare class CallStateController {
|
|
|
63
91
|
destination: string,
|
|
64
92
|
callerName?: string,
|
|
65
93
|
callerNumber?: string,
|
|
66
|
-
customHeaders?:
|
|
94
|
+
customHeaders?: CustomHeaders
|
|
67
95
|
): Promise<Call>;
|
|
96
|
+
/**
|
|
97
|
+
* Normalize public custom headers into the format expected by the underlying SDK.
|
|
98
|
+
*/
|
|
99
|
+
private _normalizeCustomHeaders;
|
|
68
100
|
/**
|
|
69
101
|
* Set callbacks for waiting for invite logic (used for push notifications)
|
|
70
102
|
*/
|
|
@@ -6,7 +6,6 @@ const operators_1 = require('rxjs/operators');
|
|
|
6
6
|
const call_1 = require('../../models/call');
|
|
7
7
|
const call_state_1 = require('../../models/call-state');
|
|
8
8
|
const callkit_coordinator_1 = require('../../callkit/callkit-coordinator');
|
|
9
|
-
const voice_pn_bridge_1 = require('../voice-pn-bridge');
|
|
10
9
|
/**
|
|
11
10
|
* Central state machine for call management.
|
|
12
11
|
*
|
|
@@ -19,6 +18,11 @@ class CallStateController {
|
|
|
19
18
|
this._calls = new rxjs_1.BehaviorSubject([]);
|
|
20
19
|
this._callMap = new Map();
|
|
21
20
|
this._disposed = false;
|
|
21
|
+
// Explicitly-tracked active call ID for multi-call scenarios.
|
|
22
|
+
// When set, activeCall$/currentActiveCall prefer this call over the
|
|
23
|
+
// first-match heuristic. Cleared automatically when the call reaches
|
|
24
|
+
// a terminal state, or manually via clearActiveCall().
|
|
25
|
+
this._activeCallId = null;
|
|
22
26
|
this._handleTelnyxIncomingCall = (telnyxCall, msg) => {
|
|
23
27
|
console.log('CallStateController: Incoming call received:', telnyxCall.callId);
|
|
24
28
|
this._handleIncomingCall(telnyxCall, msg, false);
|
|
@@ -62,18 +66,18 @@ class CallStateController {
|
|
|
62
66
|
get activeCall$() {
|
|
63
67
|
return this.calls$.pipe(
|
|
64
68
|
(0, operators_1.map)((calls) => {
|
|
65
|
-
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
72
|
-
call.currentState === call_state_1.TelnyxCallState.HELD
|
|
73
|
-
) || null
|
|
74
|
-
);
|
|
69
|
+
const call = this._selectActiveCall(calls);
|
|
70
|
+
return {
|
|
71
|
+
call,
|
|
72
|
+
callId: call?.callId ?? null,
|
|
73
|
+
state: call?.currentState ?? null,
|
|
74
|
+
};
|
|
75
75
|
}),
|
|
76
|
-
(0, operators_1.distinctUntilChanged)(
|
|
76
|
+
(0, operators_1.distinctUntilChanged)(
|
|
77
|
+
(previous, current) =>
|
|
78
|
+
previous.callId === current.callId && previous.state === current.state
|
|
79
|
+
),
|
|
80
|
+
(0, operators_1.map)(({ call }) => call)
|
|
77
81
|
);
|
|
78
82
|
}
|
|
79
83
|
/**
|
|
@@ -86,16 +90,7 @@ class CallStateController {
|
|
|
86
90
|
* Current active call (synchronous access)
|
|
87
91
|
*/
|
|
88
92
|
get currentActiveCall() {
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
calls.find(
|
|
92
|
-
(call) =>
|
|
93
|
-
call.currentState === call_state_1.TelnyxCallState.RINGING ||
|
|
94
|
-
call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
|
|
95
|
-
call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
96
|
-
call.currentState === call_state_1.TelnyxCallState.HELD
|
|
97
|
-
) || null
|
|
98
|
-
);
|
|
93
|
+
return this._selectActiveCall(this.currentCalls);
|
|
99
94
|
}
|
|
100
95
|
/**
|
|
101
96
|
* Access any active call tracked by the client.
|
|
@@ -121,6 +116,32 @@ class CallStateController {
|
|
|
121
116
|
console.warn('CallStateController: Could not find call to set connecting:', callId);
|
|
122
117
|
}
|
|
123
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Explicitly set the active call for multi-call scenarios.
|
|
121
|
+
* When set, activeCall$ and currentActiveCall prefer this call over
|
|
122
|
+
* the first-match heuristic. The ID is cleared automatically when the
|
|
123
|
+
* call reaches a terminal state.
|
|
124
|
+
* @param callId The ID of the call to mark as active
|
|
125
|
+
*/
|
|
126
|
+
setActiveCall(callId) {
|
|
127
|
+
const call = this._callMap.get(callId);
|
|
128
|
+
if (call) {
|
|
129
|
+
this._activeCallId = callId;
|
|
130
|
+
this._calls.next([...this.currentCalls]);
|
|
131
|
+
} else {
|
|
132
|
+
console.warn('CallStateController: Cannot set active call - call not found:', callId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Clear the explicitly-tracked active call ID, reverting to the
|
|
137
|
+
* first-match heuristic for active call selection.
|
|
138
|
+
*/
|
|
139
|
+
clearActiveCall() {
|
|
140
|
+
if (this._activeCallId !== null) {
|
|
141
|
+
this._activeCallId = null;
|
|
142
|
+
this._calls.next([...this.currentCalls]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
124
145
|
/**
|
|
125
146
|
* Find a call by its underlying Telnyx call ID
|
|
126
147
|
* @param telnyxCall The Telnyx call object to find
|
|
@@ -133,6 +154,31 @@ class CallStateController {
|
|
|
133
154
|
}
|
|
134
155
|
return null;
|
|
135
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Select the active call, preferring the explicitly-tracked call ID
|
|
159
|
+
* over the first-match heuristic.
|
|
160
|
+
*/
|
|
161
|
+
_selectActiveCall(calls) {
|
|
162
|
+
if (this._activeCallId) {
|
|
163
|
+
const tracked = calls.find((c) => c.callId === this._activeCallId);
|
|
164
|
+
if (tracked && this._isNonTerminal(tracked)) {
|
|
165
|
+
return tracked;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Fall back to first non-terminal call (backward compatible)
|
|
169
|
+
return calls.find((c) => this._isNonTerminal(c)) || null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if a call is in a non-terminal (active or connecting) state.
|
|
173
|
+
*/
|
|
174
|
+
_isNonTerminal(call) {
|
|
175
|
+
return (
|
|
176
|
+
call.currentState === call_state_1.TelnyxCallState.RINGING ||
|
|
177
|
+
call.currentState === call_state_1.TelnyxCallState.CONNECTING ||
|
|
178
|
+
call.currentState === call_state_1.TelnyxCallState.ACTIVE ||
|
|
179
|
+
call.currentState === call_state_1.TelnyxCallState.HELD
|
|
180
|
+
);
|
|
181
|
+
}
|
|
136
182
|
/**
|
|
137
183
|
* Initialize client listeners when the Telnyx client becomes available
|
|
138
184
|
* This should be called by the session manager after client creation
|
|
@@ -160,7 +206,7 @@ class CallStateController {
|
|
|
160
206
|
destinationNumber: destination,
|
|
161
207
|
callerIdName: callerName,
|
|
162
208
|
callerIdNumber: callerNumber,
|
|
163
|
-
customHeaders,
|
|
209
|
+
customHeaders: this._normalizeCustomHeaders(customHeaders),
|
|
164
210
|
peerConnectionOptions: {
|
|
165
211
|
useTrickleIce: this._sessionManager.useTrickleIce,
|
|
166
212
|
},
|
|
@@ -184,6 +230,18 @@ class CallStateController {
|
|
|
184
230
|
throw error;
|
|
185
231
|
}
|
|
186
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Normalize public custom headers into the format expected by the underlying SDK.
|
|
235
|
+
*/
|
|
236
|
+
_normalizeCustomHeaders(customHeaders) {
|
|
237
|
+
if (!customHeaders) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(customHeaders)) {
|
|
241
|
+
return customHeaders;
|
|
242
|
+
}
|
|
243
|
+
return Object.entries(customHeaders).map(([name, value]) => ({ name, value }));
|
|
244
|
+
}
|
|
187
245
|
/**
|
|
188
246
|
* Set callbacks for waiting for invite logic (used for push notifications)
|
|
189
247
|
*/
|
|
@@ -212,6 +270,7 @@ class CallStateController {
|
|
|
212
270
|
}
|
|
213
271
|
}
|
|
214
272
|
this._callMap.clear();
|
|
273
|
+
this._activeCallId = null;
|
|
215
274
|
this._calls.next([]);
|
|
216
275
|
}
|
|
217
276
|
/**
|
|
@@ -226,6 +285,7 @@ class CallStateController {
|
|
|
226
285
|
// Dispose of all calls
|
|
227
286
|
this.currentCalls.forEach((call) => call.dispose());
|
|
228
287
|
this._callMap.clear();
|
|
288
|
+
this._activeCallId = null;
|
|
229
289
|
// CallKit cleanup is now handled by CallKitCoordinator automatically
|
|
230
290
|
this._calls.complete();
|
|
231
291
|
}
|
|
@@ -346,6 +406,10 @@ class CallStateController {
|
|
|
346
406
|
*/
|
|
347
407
|
_addCall(call) {
|
|
348
408
|
this._callMap.set(call.callId, call);
|
|
409
|
+
// Auto-track as active if no active call is currently set
|
|
410
|
+
if (this._activeCallId === null) {
|
|
411
|
+
this._activeCallId = call.callId;
|
|
412
|
+
}
|
|
349
413
|
const currentCalls = this.currentCalls;
|
|
350
414
|
currentCalls.push(call);
|
|
351
415
|
this._calls.next([...currentCalls]);
|
|
@@ -358,7 +422,11 @@ class CallStateController {
|
|
|
358
422
|
callkit_coordinator_1.callKitCoordinator.getCallKitUUID(telnyxCall);
|
|
359
423
|
if (existingCallKitUUID) {
|
|
360
424
|
console.log(
|
|
361
|
-
'CallStateController:
|
|
425
|
+
'CallStateController: Linking push-created CallKit call to its signaling call:',
|
|
426
|
+
existingCallKitUUID
|
|
427
|
+
);
|
|
428
|
+
callkit_coordinator_1.callKitCoordinator.linkExistingCallKitCall(
|
|
429
|
+
telnyxCall,
|
|
362
430
|
existingCallKitUUID
|
|
363
431
|
);
|
|
364
432
|
} else if (call.isIncoming) {
|
|
@@ -383,16 +451,20 @@ class CallStateController {
|
|
|
383
451
|
call.callState$.subscribe((state) => {
|
|
384
452
|
// CallKitCoordinator automatically updates CallKit via setupWebRTCCallListeners
|
|
385
453
|
console.log('CallStateController: Call state changed to:', state);
|
|
454
|
+
// Re-emit the call list so calls$/activeCall$ subscribers see state changes.
|
|
455
|
+
// distinctUntilChanged uses reference equality; without a new array
|
|
456
|
+
// reference, non-terminal transitions leave activeCall$ stale.
|
|
457
|
+
this._calls.next([...this.currentCalls]);
|
|
386
458
|
// Clean up when call ends - delay to next tick so external subscribers
|
|
387
459
|
// receive the ENDED/FAILED state before the call is disposed
|
|
388
460
|
if (
|
|
389
461
|
state === call_state_1.TelnyxCallState.ENDED ||
|
|
390
462
|
state === call_state_1.TelnyxCallState.FAILED
|
|
391
463
|
) {
|
|
392
|
-
// Clear
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
464
|
+
// Clear active call ID if the ending call was the tracked active call
|
|
465
|
+
if (this._activeCallId === call.callId) {
|
|
466
|
+
this._activeCallId = null;
|
|
467
|
+
}
|
|
396
468
|
setTimeout(() => this._removeCall(call.callId), 0);
|
|
397
469
|
}
|
|
398
470
|
});
|
|
@@ -14,6 +14,10 @@ export declare class SessionManager {
|
|
|
14
14
|
private _currentConfig?;
|
|
15
15
|
private _sessionId;
|
|
16
16
|
private _disposed;
|
|
17
|
+
private _disposing;
|
|
18
|
+
private _connectionGeneration;
|
|
19
|
+
private _disposePromise?;
|
|
20
|
+
private _connectPromise?;
|
|
17
21
|
private _onClientReady?;
|
|
18
22
|
private _onDisconnect?;
|
|
19
23
|
constructor();
|
|
@@ -76,14 +80,17 @@ export declare class SessionManager {
|
|
|
76
80
|
* Handle push notification (async version)
|
|
77
81
|
*/
|
|
78
82
|
handlePushNotification(payload: Record<string, any>): Promise<void>;
|
|
83
|
+
private _hasActiveOrHeldCall;
|
|
79
84
|
/**
|
|
80
85
|
* Dispose of the session manager and clean up resources
|
|
81
86
|
*/
|
|
82
|
-
dispose(): void
|
|
87
|
+
dispose(): Promise<void>;
|
|
83
88
|
/**
|
|
84
89
|
* Internal method to establish connection with or without push notification handling
|
|
85
90
|
*/
|
|
86
91
|
private _connect;
|
|
92
|
+
private _dispose;
|
|
93
|
+
private _runConnect;
|
|
87
94
|
/**
|
|
88
95
|
* Set up event listeners for the Telnyx client
|
|
89
96
|
*/
|
|
@@ -96,4 +103,11 @@ export declare class SessionManager {
|
|
|
96
103
|
* Generate a unique session ID
|
|
97
104
|
*/
|
|
98
105
|
private _generateSessionId;
|
|
106
|
+
private _assertCanStartConnection;
|
|
107
|
+
private _isTeardownActive;
|
|
108
|
+
private _isConnectCanceled;
|
|
109
|
+
private _throwIfConnectCanceled;
|
|
110
|
+
private _createConnectCanceledError;
|
|
111
|
+
private _disconnectClient;
|
|
112
|
+
private _disconnectAndForgetClient;
|
|
99
113
|
}
|