@telnyx/react-voice-commons-sdk 1.1.1 → 1.2.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 +45 -1
- package/TelnyxVoiceCommons.podspec +3 -3
- package/ios/CallKitBridge.swift +126 -44
- package/lib/callkit/callkit-coordinator.d.ts +12 -0
- package/lib/callkit/callkit-coordinator.js +32 -4
- package/lib/index.d.ts +4 -1
- package/lib/index.js +9 -0
- package/lib/internal/calls/call-state-controller.d.ts +1 -0
- package/lib/internal/calls/call-state-controller.js +35 -1
- package/lib/internal/ice-servers-storage.d.ts +9 -0
- package/lib/internal/ice-servers-storage.js +37 -0
- package/lib/internal/sdk-identity.d.ts +4 -0
- package/lib/internal/sdk-identity.js +60 -0
- package/lib/internal/session/session-manager.d.ts +7 -2
- package/lib/internal/session/session-manager.js +35 -4
- package/lib/internal/signaling-environment.d.ts +13 -0
- package/lib/internal/signaling-environment.js +18 -0
- package/lib/models/call.d.ts +23 -2
- package/lib/models/call.js +41 -3
- package/lib/models/config.d.ts +28 -0
- package/lib/telnyx-voip-client.d.ts +4 -2
- package/lib/telnyx-voip-client.js +60 -2
- package/package.json +3 -4
- package/src/callkit/callkit-coordinator.ts +49 -6
- package/src/index.ts +6 -1
- package/src/internal/calls/call-state-controller.ts +41 -2
- package/src/internal/ice-servers-storage.ts +37 -0
- package/src/internal/sdk-identity.ts +6 -0
- package/src/internal/session/session-manager.ts +38 -4
- package/src/internal/signaling-environment.ts +20 -0
- package/src/models/call.ts +48 -3
- package/src/models/config.ts +30 -0
- package/src/telnyx-voip-client.ts +60 -3
- package/src/types/telnyx-sdk.d.ts +67 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
# CHANGELOG.md
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## [1.2.1] (2026-10-30)
|
|
4
|
+
|
|
5
|
+
### Bug Fixing
|
|
6
|
+
|
|
7
|
+
- Fix iOS call waiting. In 1.2.0, answering a second call failed while another call was active or still being answered, so the iOS Hold & Accept and End & Accept options did not connect the new call. Answering the same call twice is still ignored.
|
|
8
|
+
- `Call.answer()` on iOS now rejects when CallKit fails to answer the call, as `hold()` and `resume()` already do. It previously resolved even though the call was not answered, so apps that await `answer()` should handle the rejection.
|
|
9
|
+
|
|
10
|
+
## [1.2.0] (2026-10-28)
|
|
11
|
+
|
|
12
|
+
### Enhancement
|
|
13
|
+
|
|
14
|
+
- Expose call-quality metrics through `Call.qualityMetrics$`, `currentQualityMetrics`, and the exported metrics and quality-level types.
|
|
15
|
+
- Add automatic active-call recovery through the Voice SDK, including signaling-health monitoring, ICE restart, RTP verification, and reconnect/reattach fallback.
|
|
16
|
+
- Add `iceServers` to credential and token configurations so applications can provide custom STUN/TURN servers; omitted values use the Voice SDK's Telnyx production defaults.
|
|
17
|
+
- Identify Commons signaling as `webrtc-react-native-commons` with the Commons package version while retaining per-client identity isolation and the existing User-Agent format.
|
|
18
|
+
|
|
19
|
+
### Dependencies
|
|
20
|
+
|
|
21
|
+
- Require `react-native-webrtc ^155.0.1` (WebRTC M155), matching the Voice SDK's media dependency. Applications must target iOS 14 or newer; the Android minimum remains API 24.
|
|
22
|
+
- Require `@telnyx/react-native-voice-sdk ^1.2.0` when publishing so Commons uses the matching signaling security, call recovery, metrics, and ICE-server behavior while staying within the compatible 1.x line.
|
|
23
|
+
- Remove the unused direct `expo` dependency. Commons does not require Expo,
|
|
24
|
+
so this avoids installing Expo SDK 53 alongside an application's Expo SDK.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- Use the Voice SDK's platform-default TLS certificate and hostname validation. On iOS, signaling uses `URLSessionWebSocketTask`; Android retains React Native's OkHttp-backed WebSocket.
|
|
29
|
+
|
|
30
|
+
### Bug Fixing
|
|
31
|
+
|
|
32
|
+
- Handle BYE and missed-call pushes that arrive before the incoming WebRTC call exists, dismiss the matching CallKit call, clear pending push state, and show the missed-call notification without treating the original UUID as a collision.
|
|
33
|
+
- Serialize CallKit registration completion with missed-call invalidation so a delayed completion cannot restore an already-ended incoming call; PushKit completion remains exactly once.
|
|
34
|
+
- Keep the active CallKit UUID claimed until WebRTC answering finishes, share concurrent answer work, and reject a second call while another call owns the answering/active slot.
|
|
35
|
+
- Prevent duplicate incoming and reattached-call listeners when the same client-ready callback fires more than once during connection or reconnection.
|
|
36
|
+
|
|
37
|
+
## [1.1.1] (2026-09-11)
|
|
38
|
+
|
|
39
|
+
### Bug Fixing
|
|
40
|
+
|
|
41
|
+
- Improve iOS CallKit audio reliability by routing activation and connected-call verification through a single idempotent recovery path.
|
|
42
|
+
- Recover connected calls when CallKit owns an active play-and-record audio session but the WebRTC audio device remains disabled.
|
|
43
|
+
- Prevent duplicate CallKit setup from disabling WebRTC audio after a call is already active, and harden failure handling when audio recovery does not succeed.
|
|
44
|
+
|
|
45
|
+
### Dependencies
|
|
46
|
+
|
|
47
|
+
- Continues to require `@telnyx/react-native-voice-sdk >= 1.1.0`; no new base Voice SDK release is required.
|
|
4
48
|
|
|
5
49
|
## [1.1.0] (2026-08-07)
|
|
6
50
|
|
|
@@ -9,7 +9,7 @@ Pod::Spec.new do |s|
|
|
|
9
9
|
s.description = package['description']
|
|
10
10
|
s.homepage = package['homepage']
|
|
11
11
|
s.license = package['license']
|
|
12
|
-
s.platforms = { :ios => '
|
|
12
|
+
s.platforms = { :ios => '14.0' }
|
|
13
13
|
s.swift_version = '5.4'
|
|
14
14
|
s.author = package['author']
|
|
15
15
|
s.source = { :git => package['repository']['url'], :tag => "#{s.version}" }
|
|
@@ -22,11 +22,11 @@ Pod::Spec.new do |s|
|
|
|
22
22
|
s.dependency 'React-Core'
|
|
23
23
|
|
|
24
24
|
# WebRTC Framework dependency
|
|
25
|
-
s.dependency 'JitsiWebRTC', '~>
|
|
25
|
+
s.dependency 'JitsiWebRTC', '~> 155.0.0'
|
|
26
26
|
|
|
27
27
|
# iOS Frameworks
|
|
28
28
|
s.frameworks = 'CallKit', 'PushKit', 'AVFoundation'
|
|
29
29
|
|
|
30
30
|
# Compiler flags for Swift
|
|
31
31
|
s.compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'
|
|
32
|
-
end
|
|
32
|
+
end
|
package/ios/CallKitBridge.swift
CHANGED
|
@@ -156,7 +156,11 @@ import React
|
|
|
156
156
|
"handle": handle,
|
|
157
157
|
"uuid": uuid.uuidString,
|
|
158
158
|
"direction": "outgoing",
|
|
159
|
-
|
|
159
|
+
// VSDK-771 AFK review: align internal source tag with the
|
|
160
|
+
// new prefixed convention so grep-based family completeness
|
|
161
|
+
// (webrtc-react-native / webrtc-react-native-commons) is
|
|
162
|
+
// clean across the SDK. Not consumed by the wire contract.
|
|
163
|
+
"source": "webrtc-react-native",
|
|
160
164
|
]
|
|
161
165
|
|
|
162
166
|
callController.request(transaction) { error in
|
|
@@ -197,7 +201,9 @@ import React
|
|
|
197
201
|
"handle": handle,
|
|
198
202
|
"uuid": uuid.uuidString,
|
|
199
203
|
"direction": "incoming",
|
|
200
|
-
|
|
204
|
+
// VSDK-771 AFK review: align internal source tag with the
|
|
205
|
+
// new prefixed convention. See outgoing branch above.
|
|
206
|
+
"source": "webrtc-react-native",
|
|
201
207
|
"isCallKitRegistered": false,
|
|
202
208
|
]
|
|
203
209
|
|
|
@@ -741,6 +747,10 @@ import React
|
|
|
741
747
|
public var callKitProvider: CXProvider?
|
|
742
748
|
public var callKitController: CXCallController?
|
|
743
749
|
private let stateLock = NSRecursiveLock()
|
|
750
|
+
// Keep terminal missed-call invalidation atomic with registration side effects so a
|
|
751
|
+
// delayed CallKit completion cannot restore an already-ended incoming call.
|
|
752
|
+
private let incomingRegistrationQueue = DispatchQueue(
|
|
753
|
+
label: "com.telnyx.react-voice-commons.incoming-registration")
|
|
744
754
|
public lazy var activeCalls = LockedDictionary<UUID, [String: Any]>(lock: stateLock)
|
|
745
755
|
public lazy var pendingAnswerActions = LockedDictionary<UUID, CXAnswerCallAction>(
|
|
746
756
|
lock: stateLock)
|
|
@@ -782,6 +792,35 @@ import React
|
|
|
782
792
|
return true
|
|
783
793
|
}
|
|
784
794
|
|
|
795
|
+
@discardableResult
|
|
796
|
+
fileprivate func performIncomingRegistrationIfOwned(
|
|
797
|
+
_ callUUID: UUID,
|
|
798
|
+
token: String,
|
|
799
|
+
effects: () -> Void
|
|
800
|
+
) -> Bool {
|
|
801
|
+
return incomingRegistrationQueue.sync {
|
|
802
|
+
guard let callData = activeCalls[callUUID],
|
|
803
|
+
callData["incomingRegistrationToken"] as? String == token
|
|
804
|
+
else {
|
|
805
|
+
return false
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
effects()
|
|
809
|
+
return true
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
fileprivate func invalidateIncomingRegistration(_ callUUID: UUID) {
|
|
814
|
+
incomingRegistrationQueue.sync {
|
|
815
|
+
guard var callData = activeCalls[callUUID] else {
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
callData.removeValue(forKey: "incomingRegistrationToken")
|
|
820
|
+
activeCalls[callUUID] = callData
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
785
824
|
private override init() {
|
|
786
825
|
super.init()
|
|
787
826
|
// Only setup if explicitly requested via setup() method
|
|
@@ -1064,7 +1103,36 @@ import React
|
|
|
1064
1103
|
}
|
|
1065
1104
|
|
|
1066
1105
|
if let activeCall = activeCalls[callUUID] {
|
|
1067
|
-
|
|
1106
|
+
let activeSource = activeCall["source"] as? String
|
|
1107
|
+
|
|
1108
|
+
// A missed-call push normally reuses the original incoming
|
|
1109
|
+
// push's call_id. End that still-ringing CallKit call and show
|
|
1110
|
+
// the notification instead of treating it as a UUID collision.
|
|
1111
|
+
if activeSource == "push_notification" || activeSource == "terminated_app_push"
|
|
1112
|
+
|| activeSource == "push"
|
|
1113
|
+
{
|
|
1114
|
+
NSLog(
|
|
1115
|
+
"TelnyxVoice: Missed call matched active incoming push UUID: \(callUUID)"
|
|
1116
|
+
)
|
|
1117
|
+
// Stop the original report callback from persisting or emitting
|
|
1118
|
+
// this call while the missed-call watchdog report is pending.
|
|
1119
|
+
invalidateIncomingRegistration(callUUID)
|
|
1120
|
+
// This is a second PushKit delivery, so report it to CallKit
|
|
1121
|
+
// with a fresh UUID before ending the original ringing call.
|
|
1122
|
+
reportAndEndWatchdogCall(
|
|
1123
|
+
callUUID: UUID(),
|
|
1124
|
+
callerName: callerName,
|
|
1125
|
+
callerNumber: callerNumber,
|
|
1126
|
+
payload: payload,
|
|
1127
|
+
source: "missed_call_matched_watchdog",
|
|
1128
|
+
endedReason: .remoteEnded
|
|
1129
|
+
) {
|
|
1130
|
+
finishMissedCall()
|
|
1131
|
+
}
|
|
1132
|
+
return true
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
guard activeSource == "missed_call_push" else {
|
|
1068
1136
|
NSLog("TelnyxVoice: Suppressing missed call notification because UUID belongs to an active call: \(callUUID)")
|
|
1069
1137
|
markMissedCallProcessed(id: missedCallId)
|
|
1070
1138
|
reportAndEndWatchdogCall(
|
|
@@ -1723,6 +1791,7 @@ import React
|
|
|
1723
1791
|
|
|
1724
1792
|
// Store call data manually and report to CallKit
|
|
1725
1793
|
let isAppRunning = CallKitBridge.shared != nil
|
|
1794
|
+
let registrationToken = UUID().uuidString
|
|
1726
1795
|
callKitManager.activeCalls[callUUID] = [
|
|
1727
1796
|
"caller": callerName,
|
|
1728
1797
|
"handle": callerNumber,
|
|
@@ -1731,6 +1800,7 @@ import React
|
|
|
1731
1800
|
"direction": "incoming",
|
|
1732
1801
|
"source": isAppRunning ? "push_notification" : "terminated_app_push",
|
|
1733
1802
|
"isCallKitRegistered": false,
|
|
1803
|
+
"incomingRegistrationToken": registrationToken,
|
|
1734
1804
|
]
|
|
1735
1805
|
|
|
1736
1806
|
// Report to CallKit immediately
|
|
@@ -1742,56 +1812,68 @@ import React
|
|
|
1742
1812
|
callUpdate.supportsHolding = true
|
|
1743
1813
|
|
|
1744
1814
|
callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
|
|
1745
|
-
|
|
1746
|
-
let nsError = error as NSError
|
|
1747
|
-
let reason =
|
|
1748
|
-
nsError.domain == CXErrorDomainIncomingCall && nsError.code == 3
|
|
1749
|
-
? "filteredByDoNotDisturb" : "registrationRejected"
|
|
1750
|
-
NSLog(
|
|
1751
|
-
"[TelnyxVoipPushHandler] CallKit incoming registration rejected: domain=\(nsError.domain), code=\(nsError.code), reason=\(reason)"
|
|
1752
|
-
)
|
|
1753
|
-
callKitManager.activeCalls.removeValue(forKey: callUUID)
|
|
1754
|
-
callKitManager.clearPendingPushData(for: callUUID.uuidString)
|
|
1755
|
-
} else {
|
|
1756
|
-
NSLog(
|
|
1757
|
-
"[TelnyxVoipPushHandler] ✅ CallKit call reported successfully via unified path"
|
|
1758
|
-
)
|
|
1759
|
-
|
|
1760
|
-
callKitManager.markCallKitRegistered(callUUID)
|
|
1761
|
-
self.storePendingVoipPush(payload.dictionaryPayload)
|
|
1815
|
+
defer { completion() }
|
|
1762
1816
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
// Try to emit push received event if CallKit bridge is available
|
|
1772
|
-
if let callKitBridge = CallKitBridge.shared {
|
|
1773
|
-
let source = isAppRunning ? "push_notification" : "terminated_app_push"
|
|
1774
|
-
callKitBridge.emitCallEvent(
|
|
1775
|
-
"CallKitDidReceivePush", callUUID: callUUID,
|
|
1776
|
-
callData: [
|
|
1777
|
-
"caller": callerName,
|
|
1778
|
-
"handle": callerNumber,
|
|
1779
|
-
"payload": payload.dictionaryPayload,
|
|
1780
|
-
"uuid": callUUID.uuidString,
|
|
1781
|
-
"direction": "incoming",
|
|
1782
|
-
"source": source,
|
|
1783
|
-
])
|
|
1817
|
+
let handled = callKitManager.performIncomingRegistrationIfOwned(
|
|
1818
|
+
callUUID, token: registrationToken
|
|
1819
|
+
) {
|
|
1820
|
+
if let error = error {
|
|
1821
|
+
let nsError = error as NSError
|
|
1822
|
+
let reason =
|
|
1823
|
+
nsError.domain == CXErrorDomainIncomingCall && nsError.code == 3
|
|
1824
|
+
? "filteredByDoNotDisturb" : "registrationRejected"
|
|
1784
1825
|
NSLog(
|
|
1785
|
-
"[TelnyxVoipPushHandler]
|
|
1826
|
+
"[TelnyxVoipPushHandler] CallKit incoming registration rejected: domain=\(nsError.domain), code=\(nsError.code), reason=\(reason)"
|
|
1786
1827
|
)
|
|
1828
|
+
callKitManager.activeCalls.removeValue(forKey: callUUID)
|
|
1829
|
+
callKitManager.clearPendingPushData(for: callUUID.uuidString)
|
|
1787
1830
|
} else {
|
|
1788
1831
|
NSLog(
|
|
1789
|
-
"[TelnyxVoipPushHandler]
|
|
1832
|
+
"[TelnyxVoipPushHandler] ✅ CallKit call reported successfully via unified path"
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
callKitManager.markCallKitRegistered(callUUID)
|
|
1836
|
+
self.storePendingVoipPush(payload.dictionaryPayload)
|
|
1837
|
+
|
|
1838
|
+
// CRITICAL: Store the CallKit UUID for the React Native CallKitCoordinator
|
|
1839
|
+
// This allows the coordinator to find the existing CallKit call instead of creating a duplicate
|
|
1840
|
+
UserDefaults.standard.set(
|
|
1841
|
+
callUUID.uuidString, forKey: "@pending_callkit_uuid")
|
|
1842
|
+
UserDefaults.standard.synchronize()
|
|
1843
|
+
NSLog(
|
|
1844
|
+
"[TelnyxVoipPushHandler] ✅ Stored CallKit UUID for React Native coordinator: \(callUUID.uuidString)"
|
|
1790
1845
|
)
|
|
1846
|
+
|
|
1847
|
+
// Try to emit push received event if CallKit bridge is available
|
|
1848
|
+
if let callKitBridge = CallKitBridge.shared {
|
|
1849
|
+
let source =
|
|
1850
|
+
isAppRunning ? "push_notification" : "terminated_app_push"
|
|
1851
|
+
callKitBridge.emitCallEvent(
|
|
1852
|
+
"CallKitDidReceivePush", callUUID: callUUID,
|
|
1853
|
+
callData: [
|
|
1854
|
+
"caller": callerName,
|
|
1855
|
+
"handle": callerNumber,
|
|
1856
|
+
"payload": payload.dictionaryPayload,
|
|
1857
|
+
"uuid": callUUID.uuidString,
|
|
1858
|
+
"direction": "incoming",
|
|
1859
|
+
"source": source,
|
|
1860
|
+
])
|
|
1861
|
+
NSLog(
|
|
1862
|
+
"[TelnyxVoipPushHandler] ✅ Emitted CallKitDidReceivePush event to React Native"
|
|
1863
|
+
)
|
|
1864
|
+
} else {
|
|
1865
|
+
NSLog(
|
|
1866
|
+
"[TelnyxVoipPushHandler] ⚠️ React Native bridge not ready - event will be emitted when bridge becomes available"
|
|
1867
|
+
)
|
|
1868
|
+
}
|
|
1791
1869
|
}
|
|
1792
1870
|
}
|
|
1793
1871
|
|
|
1794
|
-
|
|
1872
|
+
if !handled {
|
|
1873
|
+
NSLog(
|
|
1874
|
+
"[TelnyxVoipPushHandler] Ignoring stale incoming registration completion for ended call: \(callUUID.uuidString)"
|
|
1875
|
+
)
|
|
1876
|
+
}
|
|
1795
1877
|
}
|
|
1796
1878
|
}
|
|
1797
1879
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Call } from '@telnyx/react-native-voice-sdk';
|
|
2
2
|
import type { TelnyxVoipClient } from '../telnyx-voip-client';
|
|
3
|
+
export type RemoteCallEndedReason = {
|
|
4
|
+
cause?: string;
|
|
5
|
+
causeCode?: number;
|
|
6
|
+
sipCode?: number;
|
|
7
|
+
sipReason?: string;
|
|
8
|
+
sipCallId?: string;
|
|
9
|
+
};
|
|
3
10
|
/**
|
|
4
11
|
* CallKit Coordinator - Manages the proper CallKit-first flow for iOS
|
|
5
12
|
*
|
|
@@ -80,6 +87,11 @@ declare class CallKitCoordinator {
|
|
|
80
87
|
* before calling this method.
|
|
81
88
|
*/
|
|
82
89
|
handleCallKitPushReceived(callKitUUID: string, event?: any): Promise<boolean>;
|
|
90
|
+
/** End a still-pending PushKit CallKit call after a remote hangup. */
|
|
91
|
+
handlePendingPushCallRemoteEnded(
|
|
92
|
+
callKitUUID: string,
|
|
93
|
+
reason?: RemoteCallEndedReason
|
|
94
|
+
): Promise<boolean>;
|
|
83
95
|
/**
|
|
84
96
|
* Handle push notification answer - when user answers from CallKit but we don't have a WebRTC call yet
|
|
85
97
|
* This is the iOS equivalent of the Android FCM handler
|
|
@@ -76,6 +76,8 @@ class CallKitCoordinator {
|
|
|
76
76
|
// Tracks action+UUID pairs so an end action is not dropped merely because
|
|
77
77
|
// an answer or held action for the same call is still completing.
|
|
78
78
|
this.processingCalls = new Set();
|
|
79
|
+
// CallKit/the app owns cross-call hold and swap policy. Only serialize
|
|
80
|
+
// duplicate actions for the same UUID; held calls must not block a new answer.
|
|
79
81
|
// Tracks calls that have already been ended in CallKit to prevent duplicate reports
|
|
80
82
|
this.endedCalls = new Set();
|
|
81
83
|
// Tracks calls that have already been reported as connected to prevent duplicate reports
|
|
@@ -675,6 +677,31 @@ class CallKitCoordinator {
|
|
|
675
677
|
return false;
|
|
676
678
|
}
|
|
677
679
|
}
|
|
680
|
+
/** End a still-pending PushKit CallKit call after a remote hangup. */
|
|
681
|
+
async handlePendingPushCallRemoteEnded(callKitUUID, reason) {
|
|
682
|
+
callKitUUID = this.normalizeUUID(callKitUUID);
|
|
683
|
+
// Do not let an unrelated/stale socket BYE terminate another CallKit call.
|
|
684
|
+
// The UUID is added here only after native CallKit registration succeeds.
|
|
685
|
+
if (!this.pendingPushCallUUIDs.has(callKitUUID)) {
|
|
686
|
+
console.log('CallKitCoordinator: Ignoring remote-ended event for unknown push UUID', {
|
|
687
|
+
callKitUUID,
|
|
688
|
+
reason,
|
|
689
|
+
});
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
console.log('CallKitCoordinator: Ending pending push call after remote hangup', {
|
|
693
|
+
callKitUUID,
|
|
694
|
+
reason,
|
|
695
|
+
});
|
|
696
|
+
if (!this.endedCalls.has(callKitUUID)) {
|
|
697
|
+
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.RemoteEnded);
|
|
698
|
+
this.endedCalls.add(callKitUUID);
|
|
699
|
+
}
|
|
700
|
+
this.cleanupCall(callKitUUID);
|
|
701
|
+
await this.clearMatchingPendingVoipPush(callKitUUID);
|
|
702
|
+
await this.checkBackgroundDisconnection();
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
678
705
|
/**
|
|
679
706
|
* Handle push notification answer - when user answers from CallKit but we don't have a WebRTC call yet
|
|
680
707
|
* This is the iOS equivalent of the Android FCM handler
|
|
@@ -698,7 +725,7 @@ class CallKitCoordinator {
|
|
|
698
725
|
// checkForInitialPushNotification() will run after setVoipClient()
|
|
699
726
|
// and will find the push data still intact, call handleCallKitPushReceived()
|
|
700
727
|
// which queues the matching auto-answer.
|
|
701
|
-
return;
|
|
728
|
+
return false;
|
|
702
729
|
}
|
|
703
730
|
// Queue one UUID-targeted answer. If the push is already being
|
|
704
731
|
// processed, its eventual INVITE will consume this action. Otherwise
|
|
@@ -706,19 +733,20 @@ class CallKitCoordinator {
|
|
|
706
733
|
voipClient.queueAnswerFromCallKit(callKitUUID);
|
|
707
734
|
if (this.pendingPushCallUUIDs.has(callKitUUID)) {
|
|
708
735
|
this.autoAnswerCallUUIDs.delete(callKitUUID);
|
|
709
|
-
return;
|
|
736
|
+
return true;
|
|
710
737
|
}
|
|
711
|
-
await this.handleCallKitPushReceived(callKitUUID, event);
|
|
712
|
-
return;
|
|
738
|
+
return await this.handleCallKitPushReceived(callKitUUID, event);
|
|
713
739
|
}
|
|
714
740
|
// For other platforms (shouldn't happen on iOS)
|
|
715
741
|
console.error('CallKitCoordinator: ❌ Unsupported platform for push notification handling');
|
|
716
742
|
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
743
|
+
return false;
|
|
717
744
|
} catch (error) {
|
|
718
745
|
console.error('CallKitCoordinator: ❌ Error handling push notification answer:', error);
|
|
719
746
|
// Report the call as failed to CallKit
|
|
720
747
|
await callkit_1.default.reportCallEnded(callKitUUID, callkit_1.CallEndReason.Failed);
|
|
721
748
|
this.cleanupCall(callKitUUID);
|
|
749
|
+
return false;
|
|
722
750
|
}
|
|
723
751
|
}
|
|
724
752
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -38,8 +38,11 @@ export {
|
|
|
38
38
|
createCredentialConfig,
|
|
39
39
|
createTokenConfig,
|
|
40
40
|
} from './models/config';
|
|
41
|
-
export type { Config, CredentialConfig, TokenConfig } from './models/config';
|
|
41
|
+
export type { Config, CredentialConfig, TokenConfig, TelnyxEnvironment } from './models/config';
|
|
42
|
+
export type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
42
43
|
export type { Call as TelnyxCall } from '@telnyx/react-native-voice-sdk';
|
|
44
|
+
export { CallQualityLevel } from '@telnyx/react-native-voice-sdk';
|
|
45
|
+
export type { CallQualityMetrics, QualityStatsRecord } from '@telnyx/react-native-voice-sdk';
|
|
43
46
|
export * from './callkit';
|
|
44
47
|
export { VoicePnBridge } from './internal/voice-pn-bridge';
|
|
45
48
|
export { useAppReadyNotifier } from './hooks/useAppReadyNotifier';
|
package/lib/index.js
CHANGED
|
@@ -39,6 +39,7 @@ var __exportStar =
|
|
|
39
39
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
40
40
|
exports.useAppReadyNotifier =
|
|
41
41
|
exports.VoicePnBridge =
|
|
42
|
+
exports.CallQualityLevel =
|
|
42
43
|
exports.createTokenConfig =
|
|
43
44
|
exports.createCredentialConfig =
|
|
44
45
|
exports.validateTokenConfig =
|
|
@@ -214,6 +215,14 @@ Object.defineProperty(exports, 'createTokenConfig', {
|
|
|
214
215
|
return config_1.createTokenConfig;
|
|
215
216
|
},
|
|
216
217
|
});
|
|
218
|
+
// Re-export call quality metrics types from the underlying SDK
|
|
219
|
+
var react_native_voice_sdk_1 = require('@telnyx/react-native-voice-sdk');
|
|
220
|
+
Object.defineProperty(exports, 'CallQualityLevel', {
|
|
221
|
+
enumerable: true,
|
|
222
|
+
get: function () {
|
|
223
|
+
return react_native_voice_sdk_1.CallQualityLevel;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
217
226
|
// Export CallKit functionality
|
|
218
227
|
__exportStar(require('./callkit'), exports);
|
|
219
228
|
// Native push notification helpers
|
|
@@ -122,6 +122,7 @@ export declare class CallStateController {
|
|
|
122
122
|
private _removeClientListeners;
|
|
123
123
|
private _handleTelnyxIncomingCall;
|
|
124
124
|
private _handleTelnyxReattachedCall;
|
|
125
|
+
private _handleRemoteCallEnded;
|
|
125
126
|
private _handleTelnyxCallStateChanged;
|
|
126
127
|
private _handleTelnyxCallRemoved;
|
|
127
128
|
/**
|
|
@@ -31,6 +31,19 @@ class CallStateController {
|
|
|
31
31
|
console.log('CallStateController: Reattached call received:', telnyxCall.callId);
|
|
32
32
|
this._handleIncomingCall(telnyxCall, msg, true);
|
|
33
33
|
};
|
|
34
|
+
this._handleRemoteCallEnded = (callId, reason) => {
|
|
35
|
+
console.log('CallStateController: Pending push call ended remotely', {
|
|
36
|
+
callId,
|
|
37
|
+
reason,
|
|
38
|
+
});
|
|
39
|
+
// A tracked call can also reach the existing state-listener teardown path.
|
|
40
|
+
// Both paths are intentionally idempotent so pre-INVITE calls are covered too.
|
|
41
|
+
void callkit_coordinator_1.callKitCoordinator
|
|
42
|
+
.handlePendingPushCallRemoteEnded(callId, reason)
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
console.error('CallStateController: Failed to end pending push CallKit call:', error);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
34
47
|
this._handleTelnyxCallStateChanged = (telnyxCall, state) => {
|
|
35
48
|
console.log(
|
|
36
49
|
'CallStateController: Call state changed from TelnyxRTC:',
|
|
@@ -184,8 +197,24 @@ class CallStateController {
|
|
|
184
197
|
* This should be called by the session manager after client creation
|
|
185
198
|
*/
|
|
186
199
|
initializeClientListeners() {
|
|
200
|
+
const telnyxClient = this._sessionManager.telnyxClient;
|
|
187
201
|
console.log('CallStateController: initializeClientListeners called');
|
|
188
|
-
console.log('CallStateController: Current client exists:', !!
|
|
202
|
+
console.log('CallStateController: Current client exists:', !!telnyxClient);
|
|
203
|
+
if (!telnyxClient) {
|
|
204
|
+
console.log('CallStateController: No telnyxClient available, skipping listener setup');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Skip redundant listener setup — SessionManager calls _onClientReady
|
|
208
|
+
// twice during connect/reconnect (once before connection, once on
|
|
209
|
+
// telnyx.client.ready event). _setupClientListeners() already
|
|
210
|
+
// removes old listeners before re-adding, so this guard skips the
|
|
211
|
+
// redundant off→on churn rather than preventing actual duplicates.
|
|
212
|
+
if (this._listenerClient === telnyxClient) {
|
|
213
|
+
console.log(
|
|
214
|
+
'CallStateController: Listeners already registered for this client instance, skipping'
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
189
218
|
this._setupClientListeners();
|
|
190
219
|
// CallKit integration now handled by CallKitCoordinator
|
|
191
220
|
console.log('CallStateController: Using CallKitCoordinator for CallKit integration');
|
|
@@ -320,6 +349,10 @@ class CallStateController {
|
|
|
320
349
|
telnyxClient.on('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
321
350
|
// Listen for call removal events from TelnyxRTC (multi-call support)
|
|
322
351
|
telnyxClient.on('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
352
|
+
// A caller can hang up while PushKit is reconnecting the socket, before
|
|
353
|
+
// an INVITE creates a Call object. Forward that terminal event so the
|
|
354
|
+
// already-presented CallKit call is dismissed.
|
|
355
|
+
telnyxClient.on('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
|
|
323
356
|
console.log('CallStateController: Client listeners set up successfully');
|
|
324
357
|
}
|
|
325
358
|
_removeClientListeners() {
|
|
@@ -330,6 +363,7 @@ class CallStateController {
|
|
|
330
363
|
this._listenerClient.off('telnyx.call.reattached', this._handleTelnyxReattachedCall);
|
|
331
364
|
this._listenerClient.off('telnyx.call.stateChanged', this._handleTelnyxCallStateChanged);
|
|
332
365
|
this._listenerClient.off('telnyx.call.removed', this._handleTelnyxCallRemoved);
|
|
366
|
+
this._listenerClient.off('telnyx.call.remoteEnded', this._handleRemoteCallEnded);
|
|
333
367
|
this._listenerClient = undefined;
|
|
334
368
|
}
|
|
335
369
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TelnyxIceServer } from '@telnyx/react-native-voice-sdk';
|
|
2
|
+
export declare const ICE_SERVERS_STORAGE_KEY = '@ice_servers';
|
|
3
|
+
/**
|
|
4
|
+
* Parse the ICE servers persisted by a previous login. A missing, malformed,
|
|
5
|
+
* or structurally invalid value yields `undefined` (the core SDK then uses the
|
|
6
|
+
* Telnyx defaults) rather than throwing, so a bad stored list never discards
|
|
7
|
+
* otherwise valid stored authentication.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseStoredIceServers(stored: string | null): TelnyxIceServer[] | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.ICE_SERVERS_STORAGE_KEY = void 0;
|
|
4
|
+
exports.parseStoredIceServers = parseStoredIceServers;
|
|
5
|
+
exports.ICE_SERVERS_STORAGE_KEY = '@ice_servers';
|
|
6
|
+
/**
|
|
7
|
+
* Parse the ICE servers persisted by a previous login. A missing, malformed,
|
|
8
|
+
* or structurally invalid value yields `undefined` (the core SDK then uses the
|
|
9
|
+
* Telnyx defaults) rather than throwing, so a bad stored list never discards
|
|
10
|
+
* otherwise valid stored authentication.
|
|
11
|
+
*/
|
|
12
|
+
function parseStoredIceServers(stored) {
|
|
13
|
+
if (!stored) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(stored);
|
|
18
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(isIceServer)) {
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Fall through to the warning below.
|
|
23
|
+
}
|
|
24
|
+
console.warn('Ignoring invalid stored ICE servers; using the SDK defaults');
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function isIceServer(value) {
|
|
28
|
+
if (!value || typeof value !== 'object') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const server = value;
|
|
32
|
+
return (
|
|
33
|
+
typeof server.urls === 'string' &&
|
|
34
|
+
(server.username === undefined || typeof server.username === 'string') &&
|
|
35
|
+
(server.credential === undefined || typeof server.credential === 'string')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var __createBinding =
|
|
3
|
+
(this && this.__createBinding) ||
|
|
4
|
+
(Object.create
|
|
5
|
+
? function (o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () {
|
|
12
|
+
return m[k];
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}
|
|
18
|
+
: function (o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
});
|
|
22
|
+
var __setModuleDefault =
|
|
23
|
+
(this && this.__setModuleDefault) ||
|
|
24
|
+
(Object.create
|
|
25
|
+
? function (o, v) {
|
|
26
|
+
Object.defineProperty(o, 'default', { enumerable: true, value: v });
|
|
27
|
+
}
|
|
28
|
+
: function (o, v) {
|
|
29
|
+
o['default'] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar =
|
|
32
|
+
(this && this.__importStar) ||
|
|
33
|
+
(function () {
|
|
34
|
+
var ownKeys = function (o) {
|
|
35
|
+
ownKeys =
|
|
36
|
+
Object.getOwnPropertyNames ||
|
|
37
|
+
function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null)
|
|
48
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++)
|
|
49
|
+
if (k[i] !== 'default') __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
55
|
+
exports.REACT_NATIVE_COMMONS_SDK_IDENTITY = void 0;
|
|
56
|
+
const pkg = __importStar(require('../../package.json'));
|
|
57
|
+
exports.REACT_NATIVE_COMMONS_SDK_IDENTITY = Object.freeze({
|
|
58
|
+
name: 'webrtc-react-native-commons',
|
|
59
|
+
version: pkg.version,
|
|
60
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import * as TelnyxSDK from '@telnyx/react-native-voice-sdk';
|
|
3
3
|
import { TelnyxConnectionState } from '../../models/connection-state';
|
|
4
|
-
import { Config, CredentialConfig, TokenConfig } from '../../models/config';
|
|
4
|
+
import { Config, CredentialConfig, TelnyxEnvironment, TokenConfig } from '../../models/config';
|
|
5
|
+
export interface SessionManagerOptions {
|
|
6
|
+
/** Environment used when a config or stored restore does not specify one */
|
|
7
|
+
defaultEnvironment?: TelnyxEnvironment;
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* Manages the connection lifecycle to the Telnyx platform.
|
|
7
11
|
*
|
|
@@ -20,7 +24,8 @@ export declare class SessionManager {
|
|
|
20
24
|
private _connectPromise?;
|
|
21
25
|
private _onClientReady?;
|
|
22
26
|
private _onDisconnect?;
|
|
23
|
-
|
|
27
|
+
private readonly _defaultEnvironment;
|
|
28
|
+
constructor(options?: SessionManagerOptions);
|
|
24
29
|
/**
|
|
25
30
|
* Observable stream of connection state changes
|
|
26
31
|
*/
|