@telnyx/react-voice-commons-sdk 0.2.1 → 0.3.1
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 +27 -2
- package/README.md +24 -0
- package/ios/CallKitBridge.swift +0 -10
- package/ios/VoicePnBridge.m +0 -6
- package/ios/VoicePnBridge.swift +0 -15
- package/lib/callkit/callkit-coordinator.js +0 -23
- package/lib/hooks/useAppStateHandler.js +0 -12
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/internal/CallKitHandler.js +0 -8
- package/lib/internal/calls/call-state-controller.d.ts +7 -0
- package/lib/internal/calls/call-state-controller.js +23 -0
- package/lib/internal/session/session-manager.d.ts +14 -1
- package/lib/internal/session/session-manager.js +23 -2
- package/lib/internal/voice-pn-bridge.d.ts +0 -12
- package/lib/internal/voice-pn-bridge.js +0 -26
- package/lib/telnyx-voip-client.js +5 -0
- package/package.json +5 -8
- package/src/callkit/callkit-coordinator.ts +1 -26
- package/src/hooks/useAppStateHandler.ts +0 -13
- package/src/index.ts +2 -0
- package/src/internal/CallKitHandler.tsx +0 -8
- package/src/internal/calls/call-state-controller.ts +26 -0
- package/src/internal/session/session-manager.ts +26 -3
- package/src/internal/voice-pn-bridge.ts +0 -32
- package/src/telnyx-voip-client.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# CHANGELOG.md
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.3.1] (2026-04-16)
|
|
4
4
|
|
|
5
5
|
### Bug Fixing
|
|
6
6
|
|
|
7
|
-
- Fixed
|
|
7
|
+
- Fixed stale `CONNECTED` state during background disconnect: `SessionManager.disconnect()` now emits `DISCONNECTED` before awaiting the underlying client teardown. Previously, observers (including the auto-reconnect logic in `TelnyxVoiceApp`) could read a stale `CONNECTED` value while the socket was being torn down, causing auto-reconnection to be skipped and subsequent `newCall()` attempts to fail with `Cannot make call when connection state is: DISCONNECTED` or `No connection exists. Please connect first.`
|
|
8
|
+
- Tracked calls are now cleared on disconnect. Previously, calls left in non-terminal states when the socket was torn down would accumulate across background/foreground cycles, since a dead socket never emits the `ENDED`/`FAILED` events that normally trigger per-call cleanup.
|
|
9
|
+
|
|
10
|
+
## [0.3.0] (2026-04-15)
|
|
11
|
+
|
|
12
|
+
### ⚠️ Breaking changes
|
|
13
|
+
|
|
14
|
+
- **`expo-router` is no longer a dependency of the SDK.** The SDK previously navigated the host app in a few places (`useAppStateHandler` on background disconnect, `CallKitHandler` after CallKit answer/end). Those calls have been removed — navigation is now exclusively the host app's responsibility.
|
|
15
|
+
- **Migration for Expo consumers:** subscribe to `voipClient.connectionState$` and `voipClient.activeCall$` in your app and navigate there. Example:
|
|
16
|
+
```tsx
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const sub = voipClient.connectionState$.subscribe((state) => {
|
|
19
|
+
if (state === TelnyxConnectionState.DISCONNECTED) {
|
|
20
|
+
router.replace('/');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return () => sub.unsubscribe();
|
|
24
|
+
}, []);
|
|
25
|
+
```
|
|
26
|
+
- `CallKitHandler` already exposed `onNavigateToDialer` / `onNavigateBack` callback props; those are now the only way to wire navigation.
|
|
27
|
+
- The `navigateToLoginOnDisconnect` option on `useAppStateHandler` is retained in the type signature for source compatibility but no longer has any effect.
|
|
28
|
+
- **Bare React Native (non-Expo) projects are now supported.** Metro bundling no longer fails on missing `expo-router`; SDK-level behavior is identical for Expo and bare RN consumers.
|
|
29
|
+
|
|
30
|
+
### Enhancement
|
|
31
|
+
|
|
32
|
+
- **`react-native-url-polyfill` is now a direct dependency and auto-loaded** from the SDK entry point. Previously, apps running Hermes crashed on the second login attempt with `URLSearchParams.set is not implemented` because the SDK constructs the WebSocket URL via `URLSearchParams`, which is incomplete in Hermes. No action required from consumers.
|
|
8
33
|
|
|
9
34
|
## [0.2.0](https://github.com/team-telnyx/react-native-voice-commons/releases/tag/commons-sdk-v0.2.0) (2026-04-01)
|
|
10
35
|
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK tha
|
|
|
11
11
|
- **Reactive State Management**: RxJS-based state streams for real-time UI updates
|
|
12
12
|
- **TypeScript Support**: Full TypeScript definitions for better developer experience
|
|
13
13
|
- **Cross-Platform**: Built for both iOS and Android with React Native
|
|
14
|
+
- **Framework-agnostic**: Works in both Expo and bare React Native projects. See the [bare RN reference demo](https://github.com/team-telnyx/telnyx-react-native-bare-demo) for non-Expo integration.
|
|
14
15
|
|
|
15
16
|
## About @telnyx/react-voice-commons-sdk
|
|
16
17
|
|
|
@@ -115,6 +116,29 @@ call.callState$.subscribe((state) => {
|
|
|
115
116
|
});
|
|
116
117
|
```
|
|
117
118
|
|
|
119
|
+
### Navigation
|
|
120
|
+
|
|
121
|
+
As of **v0.3.0**, the SDK no longer navigates the host app. Routing on state transitions (e.g. redirecting to a login screen on disconnect, surfacing a dialer screen after answering a call via CallKit) is entirely the host app's responsibility. Subscribe to `connectionState$` and `activeCall$` and invoke your own navigator.
|
|
122
|
+
|
|
123
|
+
Example using `expo-router`:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { router } from 'expo-router';
|
|
127
|
+
import { useEffect } from 'react';
|
|
128
|
+
import { TelnyxConnectionState } from '@telnyx/react-voice-commons-sdk';
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const sub = voipClient.connectionState$.subscribe((state) => {
|
|
132
|
+
if (state === TelnyxConnectionState.DISCONNECTED) {
|
|
133
|
+
router.replace('/');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return () => sub.unsubscribe();
|
|
137
|
+
}, []);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The same pattern works with `react-navigation`, React Router, or any other navigator — the SDK is agnostic.
|
|
141
|
+
|
|
118
142
|
### 4. Call Management
|
|
119
143
|
|
|
120
144
|
```tsx
|
package/ios/CallKitBridge.swift
CHANGED
|
@@ -533,12 +533,6 @@ import React
|
|
|
533
533
|
NSLog("📞 TelnyxVoice: CALLKIT ANSWER ACTION - Provider: \(provider), Action: \(action)")
|
|
534
534
|
NSLog("TelnyxVoice: User answered call with UUID: \(action.callUUID)")
|
|
535
535
|
|
|
536
|
-
// Always persist the answer action in UserDefaults so the JS side can detect it
|
|
537
|
-
// even when the RCTEventEmitter bridge is not yet ready (app cold-launched from push).
|
|
538
|
-
UserDefaults.standard.set(action.callUUID.uuidString, forKey: "pending_callkit_answer")
|
|
539
|
-
UserDefaults.standard.synchronize()
|
|
540
|
-
NSLog("TelnyxVoice: Stored pending CallKit answer for UUID: \(action.callUUID)")
|
|
541
|
-
|
|
542
536
|
// Check if this is a programmatic answer (call already answered in WebRTC)
|
|
543
537
|
// vs a user answer from CallKit UI
|
|
544
538
|
if let callData = activeCalls[action.callUUID],
|
|
@@ -561,10 +555,6 @@ import React
|
|
|
561
555
|
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
|
562
556
|
NSLog("TelnyxVoice: User ended call with UUID: \(action.callUUID)")
|
|
563
557
|
|
|
564
|
-
// Clear any pending CallKit answer (prevents stale auto-answer on next call)
|
|
565
|
-
UserDefaults.standard.removeObject(forKey: "pending_callkit_answer")
|
|
566
|
-
UserDefaults.standard.synchronize()
|
|
567
|
-
|
|
568
558
|
// Notify React Native via CallKit bridge
|
|
569
559
|
CallKitBridge.shared?.emitCallEvent(
|
|
570
560
|
"CallKitDidPerformEndCallAction", callUUID: action.callUUID,
|
package/ios/VoicePnBridge.m
CHANGED
|
@@ -22,12 +22,6 @@ RCT_EXTERN_METHOD(getPendingVoipPush:(RCTPromiseResolveBlock)resolve
|
|
|
22
22
|
RCT_EXTERN_METHOD(clearPendingVoipPush:(RCTPromiseResolveBlock)resolve
|
|
23
23
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
24
24
|
|
|
25
|
-
RCT_EXTERN_METHOD(getPendingCallKitAnswer:(RCTPromiseResolveBlock)resolve
|
|
26
|
-
rejecter:(RCTPromiseRejectBlock)reject)
|
|
27
|
-
|
|
28
|
-
RCT_EXTERN_METHOD(clearPendingCallKitAnswer:(RCTPromiseResolveBlock)resolve
|
|
29
|
-
rejecter:(RCTPromiseRejectBlock)reject)
|
|
30
|
-
|
|
31
25
|
RCT_EXTERN_METHOD(getPendingVoipAction:(RCTPromiseResolveBlock)resolve
|
|
32
26
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
33
27
|
|
package/ios/VoicePnBridge.swift
CHANGED
|
@@ -70,21 +70,6 @@ class VoicePnBridge: NSObject {
|
|
|
70
70
|
resolve(true)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
@objc
|
|
74
|
-
func getPendingCallKitAnswer(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
75
|
-
let answer = UserDefaults.standard.string(forKey: "pending_callkit_answer")
|
|
76
|
-
NSLog("[VoicePnBridge] getPendingCallKitAnswer - answer: \(answer ?? "nil")")
|
|
77
|
-
resolve(answer)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
@objc
|
|
81
|
-
func clearPendingCallKitAnswer(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
82
|
-
UserDefaults.standard.removeObject(forKey: "pending_callkit_answer")
|
|
83
|
-
UserDefaults.standard.synchronize()
|
|
84
|
-
NSLog("[VoicePnBridge] clearPendingCallKitAnswer - cleared")
|
|
85
|
-
resolve(true)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
73
|
@objc
|
|
89
74
|
func getPendingVoipAction(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
90
75
|
let pending = UserDefaults.standard.string(forKey: "pending_voip_action")
|
|
@@ -258,10 +258,6 @@ class CallKitCoordinator {
|
|
|
258
258
|
console.log('CallKitCoordinator: Answer action already being processed, skipping duplicate');
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
|
-
// Clear native-side pending answer since JS received the event normally
|
|
262
|
-
try {
|
|
263
|
-
await voice_pn_bridge_1.VoicePnBridge.clearPendingCallKitAnswer();
|
|
264
|
-
} catch (_) {}
|
|
265
261
|
const call = this.callMap.get(callKitUUID);
|
|
266
262
|
if (!call) {
|
|
267
263
|
console.warn('CallKitCoordinator: No WebRTC call found for CallKit answer action', {
|
|
@@ -444,21 +440,6 @@ class CallKitCoordinator {
|
|
|
444
440
|
...realPushData.metadata,
|
|
445
441
|
from_callkit: true,
|
|
446
442
|
};
|
|
447
|
-
// Check if the user answered from CallKit before JS listeners were ready.
|
|
448
|
-
// The native CXAnswerCallAction handler persists the answer UUID in UserDefaults
|
|
449
|
-
// so we can detect it here even when the JS event was dropped.
|
|
450
|
-
try {
|
|
451
|
-
const pendingAnswer = await voice_pn_bridge_1.VoicePnBridge.getPendingCallKitAnswer();
|
|
452
|
-
if (pendingAnswer) {
|
|
453
|
-
console.log(
|
|
454
|
-
'CallKitCoordinator: Found pending CallKit answer from native (JS event was missed), setting auto-answer flag'
|
|
455
|
-
);
|
|
456
|
-
this.shouldAutoAnswerNextCall = true;
|
|
457
|
-
await voice_pn_bridge_1.VoicePnBridge.clearPendingCallKitAnswer();
|
|
458
|
-
}
|
|
459
|
-
} catch (e) {
|
|
460
|
-
// Ignore - method may not exist on older native versions
|
|
461
|
-
}
|
|
462
443
|
// Check if auto-answer is set and add from_notification flag
|
|
463
444
|
const shouldAddFromNotification = this.shouldAutoAnswerNextCall;
|
|
464
445
|
let pushData;
|
|
@@ -736,10 +717,6 @@ class CallKitCoordinator {
|
|
|
736
717
|
async cleanupPushNotificationState() {
|
|
737
718
|
console.log('CallKitCoordinator: ✅ Cleared auto-answer flag');
|
|
738
719
|
this.shouldAutoAnswerNextCall = false;
|
|
739
|
-
// Also clear native-side pending answer to prevent stale auto-answer
|
|
740
|
-
try {
|
|
741
|
-
await voice_pn_bridge_1.VoicePnBridge.clearPendingCallKitAnswer();
|
|
742
|
-
} catch (_) {}
|
|
743
720
|
}
|
|
744
721
|
/**
|
|
745
722
|
* Get reference to the SDK client (for queuing actions when call doesn't exist yet)
|
|
@@ -8,7 +8,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
8
8
|
exports.useAppStateHandler = void 0;
|
|
9
9
|
const react_1 = require('react');
|
|
10
10
|
const react_native_1 = require('react-native');
|
|
11
|
-
const expo_router_1 = require('expo-router');
|
|
12
11
|
const async_storage_1 = __importDefault(require('@react-native-async-storage/async-storage'));
|
|
13
12
|
const connection_state_1 = require('../models/connection-state');
|
|
14
13
|
const call_state_1 = require('../models/call-state');
|
|
@@ -70,9 +69,6 @@ const useAppStateHandler = ({
|
|
|
70
69
|
if (!stillInProgress) {
|
|
71
70
|
log('AppStateHandler: Push notification call completed, now disconnecting socket');
|
|
72
71
|
await voipClient.logout();
|
|
73
|
-
if (navigateToLoginOnDisconnect) {
|
|
74
|
-
expo_router_1.router.replace('/');
|
|
75
|
-
}
|
|
76
72
|
}
|
|
77
73
|
}, 5000); // Wait 5 seconds
|
|
78
74
|
appState.current = nextAppState;
|
|
@@ -83,14 +79,6 @@ const useAppStateHandler = ({
|
|
|
83
79
|
// Disconnect the socket with background reason
|
|
84
80
|
await voipClient.logout();
|
|
85
81
|
log('AppStateHandler: Socket disconnected successfully');
|
|
86
|
-
// Navigate to login screen
|
|
87
|
-
if (navigateToLoginOnDisconnect) {
|
|
88
|
-
// Use a small delay to ensure the disconnect completes
|
|
89
|
-
setTimeout(() => {
|
|
90
|
-
log('AppStateHandler: Navigating to login screen');
|
|
91
|
-
expo_router_1.router.replace('/');
|
|
92
|
-
}, 100);
|
|
93
|
-
}
|
|
94
82
|
} catch (error) {
|
|
95
83
|
console.error('AppStateHandler: Error during background disconnect:', error);
|
|
96
84
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -62,6 +62,7 @@ exports.useAppReadyNotifier =
|
|
|
62
62
|
exports.createTelnyxVoipClient =
|
|
63
63
|
exports.TelnyxVoipClient =
|
|
64
64
|
void 0;
|
|
65
|
+
require('react-native-url-polyfill/auto');
|
|
65
66
|
// Main client
|
|
66
67
|
var telnyx_voip_client_1 = require('./telnyx-voip-client');
|
|
67
68
|
Object.defineProperty(exports, 'TelnyxVoipClient', {
|
|
@@ -8,7 +8,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
8
8
|
exports.CallKitHandler = void 0;
|
|
9
9
|
const react_1 = require('react');
|
|
10
10
|
const react_native_1 = require('react-native');
|
|
11
|
-
const expo_router_1 = require('expo-router');
|
|
12
11
|
const async_storage_1 = __importDefault(require('@react-native-async-storage/async-storage'));
|
|
13
12
|
const TelnyxVoiceContext_1 = require('../context/TelnyxVoiceContext');
|
|
14
13
|
// Global flag to ensure only one CallKitHandler is active
|
|
@@ -20,7 +19,6 @@ let isCallKitHandlerActive = false;
|
|
|
20
19
|
* @internal - Users should not use this component directly
|
|
21
20
|
*/
|
|
22
21
|
const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack }) => {
|
|
23
|
-
const router = (0, expo_router_1.useRouter)();
|
|
24
22
|
const { voipClient } = (0, TelnyxVoiceContext_1.useTelnyxVoice)();
|
|
25
23
|
// Store active calls by CallKit UUID for coordination
|
|
26
24
|
const activeCallsRef = (0, react_1.useRef)(new Map());
|
|
@@ -90,11 +88,8 @@ const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack })
|
|
|
90
88
|
callUUID: eventData.callUUID,
|
|
91
89
|
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
92
90
|
});
|
|
93
|
-
// Navigate to dialer after answering
|
|
94
91
|
if (onNavigateToDialer) {
|
|
95
92
|
onNavigateToDialer();
|
|
96
|
-
} else {
|
|
97
|
-
router.replace('/dialer');
|
|
98
93
|
}
|
|
99
94
|
};
|
|
100
95
|
const handleEndCall = async (eventData) => {
|
|
@@ -105,11 +100,8 @@ const CallKitHandler = ({ onLoginRequired, onNavigateToDialer, onNavigateBack })
|
|
|
105
100
|
// Clean up our local tracking info
|
|
106
101
|
activeCallsRef.current.delete(eventData.callUUID);
|
|
107
102
|
await async_storage_1.default.removeItem('@push_notification_payload');
|
|
108
|
-
// Navigate back after call ends
|
|
109
103
|
if (onNavigateBack) {
|
|
110
104
|
onNavigateBack();
|
|
111
|
-
} else {
|
|
112
|
-
router.replace('/dialer');
|
|
113
105
|
}
|
|
114
106
|
};
|
|
115
107
|
// This component doesn't render anything, it just handles events
|
|
@@ -71,6 +71,13 @@ export declare class CallStateController {
|
|
|
71
71
|
isWaitingForInvite: () => boolean;
|
|
72
72
|
onInviteAutoAccepted: () => void;
|
|
73
73
|
}): void;
|
|
74
|
+
/**
|
|
75
|
+
* Clear all tracked calls. Called when the session disconnects so that
|
|
76
|
+
* calls left in non-terminal states (because the socket died before
|
|
77
|
+
* their ENDED/FAILED events could arrive) don't accumulate as ghosts
|
|
78
|
+
* across reconnect cycles.
|
|
79
|
+
*/
|
|
80
|
+
clearAllCalls(): void;
|
|
74
81
|
/**
|
|
75
82
|
* Dispose of the controller and clean up resources
|
|
76
83
|
*/
|
|
@@ -161,6 +161,29 @@ class CallStateController {
|
|
|
161
161
|
this._isWaitingForInvite = callbacks.isWaitingForInvite;
|
|
162
162
|
this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Clear all tracked calls. Called when the session disconnects so that
|
|
166
|
+
* calls left in non-terminal states (because the socket died before
|
|
167
|
+
* their ENDED/FAILED events could arrive) don't accumulate as ghosts
|
|
168
|
+
* across reconnect cycles.
|
|
169
|
+
*/
|
|
170
|
+
clearAllCalls() {
|
|
171
|
+
if (this._callMap.size === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log(
|
|
175
|
+
`CallStateController: Clearing ${this._callMap.size} tracked call(s) on disconnect`
|
|
176
|
+
);
|
|
177
|
+
for (const call of this._callMap.values()) {
|
|
178
|
+
try {
|
|
179
|
+
call.dispose();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn('CallStateController: Error disposing call during clear:', error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this._callMap.clear();
|
|
185
|
+
this._calls.next([]);
|
|
186
|
+
}
|
|
164
187
|
/**
|
|
165
188
|
* Dispose of the controller and clean up resources
|
|
166
189
|
*/
|
|
@@ -15,6 +15,7 @@ export declare class SessionManager {
|
|
|
15
15
|
private _sessionId;
|
|
16
16
|
private _disposed;
|
|
17
17
|
private _onClientReady?;
|
|
18
|
+
private _onDisconnect?;
|
|
18
19
|
constructor();
|
|
19
20
|
/**
|
|
20
21
|
* Observable stream of connection state changes
|
|
@@ -24,6 +25,11 @@ export declare class SessionManager {
|
|
|
24
25
|
* Set callback to be called when the Telnyx client is ready
|
|
25
26
|
*/
|
|
26
27
|
setOnClientReady(callback: () => void): void;
|
|
28
|
+
/**
|
|
29
|
+
* Set callback to be called when the session disconnects, so dependent
|
|
30
|
+
* subsystems (e.g. the call state controller) can clear their state.
|
|
31
|
+
*/
|
|
32
|
+
setOnDisconnect(callback: () => void): void;
|
|
27
33
|
/**
|
|
28
34
|
* Current connection state (synchronous access)
|
|
29
35
|
*/
|
|
@@ -45,7 +51,14 @@ export declare class SessionManager {
|
|
|
45
51
|
*/
|
|
46
52
|
connectWithToken(config: TokenConfig): Promise<void>;
|
|
47
53
|
/**
|
|
48
|
-
* Disconnect from the Telnyx platform
|
|
54
|
+
* Disconnect from the Telnyx platform.
|
|
55
|
+
*
|
|
56
|
+
* The DISCONNECTED state is emitted BEFORE awaiting the underlying
|
|
57
|
+
* client teardown so that observers (including the auto-reconnect logic
|
|
58
|
+
* in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
|
|
59
|
+
* short window while the socket is being torn down. Tracked calls are
|
|
60
|
+
* cleared here too, since a torn-down socket will never emit the
|
|
61
|
+
* ENDED/FAILED events that normally trigger per-call cleanup.
|
|
49
62
|
*/
|
|
50
63
|
disconnect(): Promise<void>;
|
|
51
64
|
/**
|
|
@@ -85,6 +85,13 @@ class SessionManager {
|
|
|
85
85
|
setOnClientReady(callback) {
|
|
86
86
|
this._onClientReady = callback;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Set callback to be called when the session disconnects, so dependent
|
|
90
|
+
* subsystems (e.g. the call state controller) can clear their state.
|
|
91
|
+
*/
|
|
92
|
+
setOnDisconnect(callback) {
|
|
93
|
+
this._onDisconnect = callback;
|
|
94
|
+
}
|
|
88
95
|
/**
|
|
89
96
|
* Current connection state (synchronous access)
|
|
90
97
|
*/
|
|
@@ -124,13 +131,28 @@ class SessionManager {
|
|
|
124
131
|
await this._connect();
|
|
125
132
|
}
|
|
126
133
|
/**
|
|
127
|
-
* Disconnect from the Telnyx platform
|
|
134
|
+
* Disconnect from the Telnyx platform.
|
|
135
|
+
*
|
|
136
|
+
* The DISCONNECTED state is emitted BEFORE awaiting the underlying
|
|
137
|
+
* client teardown so that observers (including the auto-reconnect logic
|
|
138
|
+
* in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
|
|
139
|
+
* short window while the socket is being torn down. Tracked calls are
|
|
140
|
+
* cleared here too, since a torn-down socket will never emit the
|
|
141
|
+
* ENDED/FAILED events that normally trigger per-call cleanup.
|
|
128
142
|
*/
|
|
129
143
|
async disconnect() {
|
|
130
144
|
if (this._disposed) {
|
|
131
145
|
return;
|
|
132
146
|
}
|
|
133
147
|
this._currentConfig = undefined;
|
|
148
|
+
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
149
|
+
if (this._onDisconnect) {
|
|
150
|
+
try {
|
|
151
|
+
this._onDisconnect();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error in onDisconnect callback:', error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
134
156
|
if (this._telnyxClient) {
|
|
135
157
|
try {
|
|
136
158
|
await this._telnyxClient.disconnect();
|
|
@@ -138,7 +160,6 @@ class SessionManager {
|
|
|
138
160
|
console.error('Error during disconnect:', error);
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
|
-
this._connectionState.next(connection_state_1.TelnyxConnectionState.DISCONNECTED);
|
|
142
163
|
}
|
|
143
164
|
/**
|
|
144
165
|
* Disable push notifications for the current session.
|
|
@@ -25,8 +25,6 @@ export interface VoicePnBridgeInterface {
|
|
|
25
25
|
): Promise<boolean>;
|
|
26
26
|
hideOngoingCallNotification(): Promise<boolean>;
|
|
27
27
|
hideIncomingCallNotification(): Promise<boolean>;
|
|
28
|
-
getPendingCallKitAnswer(): Promise<string | null>;
|
|
29
|
-
clearPendingCallKitAnswer(): Promise<boolean>;
|
|
30
28
|
getVoipToken(): Promise<string | null>;
|
|
31
29
|
getPendingVoipPush(): Promise<string | null>;
|
|
32
30
|
clearPendingVoipPush(): Promise<boolean>;
|
|
@@ -89,16 +87,6 @@ export declare class VoicePnBridge {
|
|
|
89
87
|
* Useful for dismissing notifications when call is answered/rejected in app
|
|
90
88
|
*/
|
|
91
89
|
static hideIncomingCallNotification(): Promise<boolean>;
|
|
92
|
-
/**
|
|
93
|
-
* Get pending CallKit answer UUID from native storage (iOS only).
|
|
94
|
-
* When the user answers a CallKit call before JS listeners are ready,
|
|
95
|
-
* the native side persists the answer UUID in UserDefaults so JS can detect it.
|
|
96
|
-
*/
|
|
97
|
-
static getPendingCallKitAnswer(): Promise<string | null>;
|
|
98
|
-
/**
|
|
99
|
-
* Clear pending CallKit answer from native storage (iOS only)
|
|
100
|
-
*/
|
|
101
|
-
static clearPendingCallKitAnswer(): Promise<boolean>;
|
|
102
90
|
/**
|
|
103
91
|
* Get VoIP token from native storage
|
|
104
92
|
*/
|
|
@@ -124,32 +124,6 @@ class VoicePnBridge {
|
|
|
124
124
|
return false;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Get pending CallKit answer UUID from native storage (iOS only).
|
|
129
|
-
* When the user answers a CallKit call before JS listeners are ready,
|
|
130
|
-
* the native side persists the answer UUID in UserDefaults so JS can detect it.
|
|
131
|
-
*/
|
|
132
|
-
static async getPendingCallKitAnswer() {
|
|
133
|
-
if (react_native_1.Platform.OS !== 'ios') return null;
|
|
134
|
-
try {
|
|
135
|
-
return await NativeBridge.getPendingCallKitAnswer();
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Clear pending CallKit answer from native storage (iOS only)
|
|
143
|
-
*/
|
|
144
|
-
static async clearPendingCallKitAnswer() {
|
|
145
|
-
if (react_native_1.Platform.OS !== 'ios') return true;
|
|
146
|
-
try {
|
|
147
|
-
return await NativeBridge.clearPendingCallKitAnswer();
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
127
|
/**
|
|
154
128
|
* Get VoIP token from native storage
|
|
155
129
|
*/
|
|
@@ -66,6 +66,11 @@ class TelnyxVoipClient {
|
|
|
66
66
|
);
|
|
67
67
|
this._callStateController.initializeClientListeners();
|
|
68
68
|
});
|
|
69
|
+
// Clear any tracked calls when the session disconnects, so ghosts
|
|
70
|
+
// don't accumulate across background → foreground reconnect cycles.
|
|
71
|
+
this._sessionManager.setOnDisconnect(() => {
|
|
72
|
+
this._callStateController.clearAllCalls();
|
|
73
|
+
});
|
|
69
74
|
if (this._options.debug) {
|
|
70
75
|
console.log('TelnyxVoipClient initialized with options:', this._options);
|
|
71
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -59,14 +59,13 @@
|
|
|
59
59
|
"homepage": "https://github.com/team-telnyx/react-native-voice-commons",
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
62
|
-
"url": "
|
|
62
|
+
"url": "https://github.com/team-telnyx/react-native-voice-commons.git"
|
|
63
63
|
},
|
|
64
64
|
"bugs": {
|
|
65
65
|
"url": "https://github.com/team-telnyx/react-native-voice-commons/issues"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
69
|
-
"expo-router": "^5.1.0",
|
|
70
69
|
"react": ">=19.0.0 <20.0.0",
|
|
71
70
|
"react-native": ">=0.79.0 <1.0.0",
|
|
72
71
|
"react-native-webrtc": "^124.0.5"
|
|
@@ -75,9 +74,6 @@
|
|
|
75
74
|
"@react-native-async-storage/async-storage": {
|
|
76
75
|
"optional": false
|
|
77
76
|
},
|
|
78
|
-
"expo-router": {
|
|
79
|
-
"optional": false
|
|
80
|
-
},
|
|
81
77
|
"react": {
|
|
82
78
|
"optional": false
|
|
83
79
|
},
|
|
@@ -93,6 +89,7 @@
|
|
|
93
89
|
"@telnyx/react-native-voice-sdk": ">=0.4.1",
|
|
94
90
|
"eventemitter3": "^5.0.1",
|
|
95
91
|
"expo": "~53.0.22",
|
|
92
|
+
"react-native-url-polyfill": "^3.0.0",
|
|
96
93
|
"react-native-voip-push-notification": "^3.3.3",
|
|
97
94
|
"rxjs": "^7.8.2"
|
|
98
95
|
},
|
|
@@ -104,7 +101,6 @@
|
|
|
104
101
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
105
102
|
"@typescript-eslint/parser": "^6.0.0",
|
|
106
103
|
"eslint": "^8.0.0",
|
|
107
|
-
"expo-router": "^5.1.0",
|
|
108
104
|
"jest": "^29.5.0",
|
|
109
105
|
"prettier": "^3.0.0",
|
|
110
106
|
"ts-jest": "^29.1.0",
|
|
@@ -116,6 +112,7 @@
|
|
|
116
112
|
"node": ">=16"
|
|
117
113
|
},
|
|
118
114
|
"publishConfig": {
|
|
119
|
-
"access": "public"
|
|
115
|
+
"access": "public",
|
|
116
|
+
"provenance": true
|
|
120
117
|
}
|
|
121
118
|
}
|
|
@@ -2,9 +2,9 @@ import { Platform, AppState } from 'react-native';
|
|
|
2
2
|
import CallKit, { CallEndReason } from './callkit';
|
|
3
3
|
import { Call } from '@telnyx/react-native-voice-sdk';
|
|
4
4
|
import { VoicePnBridge } from '../internal/voice-pn-bridge';
|
|
5
|
-
import { router } from 'expo-router';
|
|
6
5
|
import { TelnyxVoipClient } from '../telnyx-voip-client';
|
|
7
6
|
import { TelnyxConnectionState } from '../models/connection-state';
|
|
7
|
+
import { act } from 'react';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* CallKit Coordinator - Manages the proper CallKit-first flow for iOS
|
|
@@ -253,11 +253,6 @@ class CallKitCoordinator {
|
|
|
253
253
|
return;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// Clear native-side pending answer since JS received the event normally
|
|
257
|
-
try {
|
|
258
|
-
await VoicePnBridge.clearPendingCallKitAnswer();
|
|
259
|
-
} catch (_) {}
|
|
260
|
-
|
|
261
256
|
const call = this.callMap.get(callKitUUID);
|
|
262
257
|
|
|
263
258
|
if (!call) {
|
|
@@ -473,22 +468,6 @@ class CallKitCoordinator {
|
|
|
473
468
|
from_callkit: true,
|
|
474
469
|
};
|
|
475
470
|
|
|
476
|
-
// Check if the user answered from CallKit before JS listeners were ready.
|
|
477
|
-
// The native CXAnswerCallAction handler persists the answer UUID in UserDefaults
|
|
478
|
-
// so we can detect it here even when the JS event was dropped.
|
|
479
|
-
try {
|
|
480
|
-
const pendingAnswer = await VoicePnBridge.getPendingCallKitAnswer();
|
|
481
|
-
if (pendingAnswer) {
|
|
482
|
-
console.log(
|
|
483
|
-
'CallKitCoordinator: Found pending CallKit answer from native (JS event was missed), setting auto-answer flag'
|
|
484
|
-
);
|
|
485
|
-
this.shouldAutoAnswerNextCall = true;
|
|
486
|
-
await VoicePnBridge.clearPendingCallKitAnswer();
|
|
487
|
-
}
|
|
488
|
-
} catch (e) {
|
|
489
|
-
// Ignore - method may not exist on older native versions
|
|
490
|
-
}
|
|
491
|
-
|
|
492
471
|
// Check if auto-answer is set and add from_notification flag
|
|
493
472
|
const shouldAddFromNotification = this.shouldAutoAnswerNextCall;
|
|
494
473
|
|
|
@@ -809,10 +788,6 @@ class CallKitCoordinator {
|
|
|
809
788
|
private async cleanupPushNotificationState(): Promise<void> {
|
|
810
789
|
console.log('CallKitCoordinator: ✅ Cleared auto-answer flag');
|
|
811
790
|
this.shouldAutoAnswerNextCall = false;
|
|
812
|
-
// Also clear native-side pending answer to prevent stale auto-answer
|
|
813
|
-
try {
|
|
814
|
-
await VoicePnBridge.clearPendingCallKitAnswer();
|
|
815
|
-
} catch (_) {}
|
|
816
791
|
}
|
|
817
792
|
|
|
818
793
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { AppState, AppStateStatus } from 'react-native';
|
|
3
|
-
import { router } from 'expo-router';
|
|
4
3
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
5
4
|
import { TelnyxVoipClient } from '../telnyx-voip-client';
|
|
6
5
|
import { TelnyxConnectionState } from '../models/connection-state';
|
|
@@ -76,9 +75,6 @@ export const useAppStateHandler = ({
|
|
|
76
75
|
if (!stillInProgress) {
|
|
77
76
|
log('AppStateHandler: Push notification call completed, now disconnecting socket');
|
|
78
77
|
await voipClient.logout();
|
|
79
|
-
if (navigateToLoginOnDisconnect) {
|
|
80
|
-
router.replace('/');
|
|
81
|
-
}
|
|
82
78
|
}
|
|
83
79
|
}, 5000); // Wait 5 seconds
|
|
84
80
|
appState.current = nextAppState;
|
|
@@ -92,15 +88,6 @@ export const useAppStateHandler = ({
|
|
|
92
88
|
await voipClient.logout();
|
|
93
89
|
|
|
94
90
|
log('AppStateHandler: Socket disconnected successfully');
|
|
95
|
-
|
|
96
|
-
// Navigate to login screen
|
|
97
|
-
if (navigateToLoginOnDisconnect) {
|
|
98
|
-
// Use a small delay to ensure the disconnect completes
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
log('AppStateHandler: Navigating to login screen');
|
|
101
|
-
router.replace('/');
|
|
102
|
-
}, 100);
|
|
103
|
-
}
|
|
104
91
|
} catch (error) {
|
|
105
92
|
console.error('AppStateHandler: Error during background disconnect:', error);
|
|
106
93
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { Platform, DeviceEventEmitter } from 'react-native';
|
|
3
|
-
import { useRouter } from 'expo-router';
|
|
4
3
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
5
4
|
import { useTelnyxVoice } from '../context/TelnyxVoiceContext';
|
|
6
5
|
import { callKitCoordinator } from '../callkit';
|
|
@@ -36,7 +35,6 @@ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
|
36
35
|
onNavigateToDialer,
|
|
37
36
|
onNavigateBack,
|
|
38
37
|
}) => {
|
|
39
|
-
const router = useRouter();
|
|
40
38
|
const { voipClient } = useTelnyxVoice();
|
|
41
39
|
|
|
42
40
|
// Store active calls by CallKit UUID for coordination
|
|
@@ -118,11 +116,8 @@ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
|
118
116
|
isTrackedCall: activeCallsRef.current.has(eventData.callUUID),
|
|
119
117
|
});
|
|
120
118
|
|
|
121
|
-
// Navigate to dialer after answering
|
|
122
119
|
if (onNavigateToDialer) {
|
|
123
120
|
onNavigateToDialer();
|
|
124
|
-
} else {
|
|
125
|
-
router.replace('/dialer');
|
|
126
121
|
}
|
|
127
122
|
};
|
|
128
123
|
|
|
@@ -136,11 +131,8 @@ export const CallKitHandler: React.FC<CallKitHandlerProps> = ({
|
|
|
136
131
|
activeCallsRef.current.delete(eventData.callUUID);
|
|
137
132
|
await AsyncStorage.removeItem('@push_notification_payload');
|
|
138
133
|
|
|
139
|
-
// Navigate back after call ends
|
|
140
134
|
if (onNavigateBack) {
|
|
141
135
|
onNavigateBack();
|
|
142
|
-
} else {
|
|
143
|
-
router.replace('/dialer');
|
|
144
136
|
}
|
|
145
137
|
};
|
|
146
138
|
|
|
@@ -190,6 +190,32 @@ export class CallStateController {
|
|
|
190
190
|
this._onInviteAutoAccepted = callbacks.onInviteAutoAccepted;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Clear all tracked calls. Called when the session disconnects so that
|
|
195
|
+
* calls left in non-terminal states (because the socket died before
|
|
196
|
+
* their ENDED/FAILED events could arrive) don't accumulate as ghosts
|
|
197
|
+
* across reconnect cycles.
|
|
198
|
+
*/
|
|
199
|
+
clearAllCalls(): void {
|
|
200
|
+
if (this._callMap.size === 0) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(
|
|
205
|
+
`CallStateController: Clearing ${this._callMap.size} tracked call(s) on disconnect`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
for (const call of this._callMap.values()) {
|
|
209
|
+
try {
|
|
210
|
+
call.dispose();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn('CallStateController: Error disposing call during clear:', error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this._callMap.clear();
|
|
216
|
+
this._calls.next([]);
|
|
217
|
+
}
|
|
218
|
+
|
|
193
219
|
/**
|
|
194
220
|
* Dispose of the controller and clean up resources
|
|
195
221
|
*/
|
|
@@ -26,6 +26,7 @@ export class SessionManager {
|
|
|
26
26
|
private _sessionId: string;
|
|
27
27
|
private _disposed = false;
|
|
28
28
|
private _onClientReady?: () => void;
|
|
29
|
+
private _onDisconnect?: () => void;
|
|
29
30
|
|
|
30
31
|
constructor() {
|
|
31
32
|
this._sessionId = this._generateSessionId();
|
|
@@ -45,6 +46,14 @@ export class SessionManager {
|
|
|
45
46
|
this._onClientReady = callback;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Set callback to be called when the session disconnects, so dependent
|
|
51
|
+
* subsystems (e.g. the call state controller) can clear their state.
|
|
52
|
+
*/
|
|
53
|
+
setOnDisconnect(callback: () => void): void {
|
|
54
|
+
this._onDisconnect = callback;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
/**
|
|
49
58
|
* Current connection state (synchronous access)
|
|
50
59
|
*/
|
|
@@ -91,7 +100,14 @@ export class SessionManager {
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
|
-
* Disconnect from the Telnyx platform
|
|
103
|
+
* Disconnect from the Telnyx platform.
|
|
104
|
+
*
|
|
105
|
+
* The DISCONNECTED state is emitted BEFORE awaiting the underlying
|
|
106
|
+
* client teardown so that observers (including the auto-reconnect logic
|
|
107
|
+
* in TelnyxVoiceApp) cannot read a stale CONNECTED value during the
|
|
108
|
+
* short window while the socket is being torn down. Tracked calls are
|
|
109
|
+
* cleared here too, since a torn-down socket will never emit the
|
|
110
|
+
* ENDED/FAILED events that normally trigger per-call cleanup.
|
|
95
111
|
*/
|
|
96
112
|
async disconnect(): Promise<void> {
|
|
97
113
|
if (this._disposed) {
|
|
@@ -99,6 +115,15 @@ export class SessionManager {
|
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
this._currentConfig = undefined;
|
|
118
|
+
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
119
|
+
|
|
120
|
+
if (this._onDisconnect) {
|
|
121
|
+
try {
|
|
122
|
+
this._onDisconnect();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error in onDisconnect callback:', error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
if (this._telnyxClient) {
|
|
104
129
|
try {
|
|
@@ -107,8 +132,6 @@ export class SessionManager {
|
|
|
107
132
|
console.error('Error during disconnect:', error);
|
|
108
133
|
}
|
|
109
134
|
}
|
|
110
|
-
|
|
111
|
-
this._connectionState.next(TelnyxConnectionState.DISCONNECTED);
|
|
112
135
|
}
|
|
113
136
|
|
|
114
137
|
/**
|
|
@@ -32,10 +32,6 @@ export interface VoicePnBridgeInterface {
|
|
|
32
32
|
hideOngoingCallNotification(): Promise<boolean>;
|
|
33
33
|
hideIncomingCallNotification(): Promise<boolean>;
|
|
34
34
|
|
|
35
|
-
// CallKit answer persistence (iOS only)
|
|
36
|
-
getPendingCallKitAnswer(): Promise<string | null>;
|
|
37
|
-
clearPendingCallKitAnswer(): Promise<boolean>;
|
|
38
|
-
|
|
39
35
|
// Additional UserDefaults methods
|
|
40
36
|
getVoipToken(): Promise<string | null>;
|
|
41
37
|
getPendingVoipPush(): Promise<string | null>;
|
|
@@ -183,34 +179,6 @@ export class VoicePnBridge {
|
|
|
183
179
|
}
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
/**
|
|
187
|
-
* Get pending CallKit answer UUID from native storage (iOS only).
|
|
188
|
-
* When the user answers a CallKit call before JS listeners are ready,
|
|
189
|
-
* the native side persists the answer UUID in UserDefaults so JS can detect it.
|
|
190
|
-
*/
|
|
191
|
-
static async getPendingCallKitAnswer(): Promise<string | null> {
|
|
192
|
-
if (Platform.OS !== 'ios') return null;
|
|
193
|
-
try {
|
|
194
|
-
return await NativeBridge.getPendingCallKitAnswer();
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error('VoicePnBridge: Error getting pending CallKit answer:', error);
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Clear pending CallKit answer from native storage (iOS only)
|
|
203
|
-
*/
|
|
204
|
-
static async clearPendingCallKitAnswer(): Promise<boolean> {
|
|
205
|
-
if (Platform.OS !== 'ios') return true;
|
|
206
|
-
try {
|
|
207
|
-
return await NativeBridge.clearPendingCallKitAnswer();
|
|
208
|
-
} catch (error) {
|
|
209
|
-
console.error('VoicePnBridge: Error clearing pending CallKit answer:', error);
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
182
|
/**
|
|
215
183
|
* Get VoIP token from native storage
|
|
216
184
|
*/
|
|
@@ -82,6 +82,12 @@ export class TelnyxVoipClient {
|
|
|
82
82
|
this._callStateController.initializeClientListeners();
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
// Clear any tracked calls when the session disconnects, so ghosts
|
|
86
|
+
// don't accumulate across background → foreground reconnect cycles.
|
|
87
|
+
this._sessionManager.setOnDisconnect(() => {
|
|
88
|
+
this._callStateController.clearAllCalls();
|
|
89
|
+
});
|
|
90
|
+
|
|
85
91
|
if (this._options.debug) {
|
|
86
92
|
console.log('TelnyxVoipClient initialized with options:', this._options);
|
|
87
93
|
}
|