@telnyx/react-voice-commons-sdk 0.1.5 → 0.1.7-beta.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.
Files changed (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +24 -24
  3. package/android/build.gradle +45 -0
  4. package/android/src/main/AndroidManifest.xml +38 -0
  5. package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +83 -0
  6. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +179 -0
  7. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxMainActivity.kt +216 -0
  8. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationActionReceiver.kt +177 -0
  9. package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +277 -0
  10. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +247 -0
  11. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgePackage.kt +17 -0
  12. package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +154 -0
  13. package/ios/CallKitBridge.m +1 -1
  14. package/ios/CallKitBridge.swift +1 -11
  15. package/ios/README.md +2 -2
  16. package/lib/callkit/callkit-coordinator.js +6 -2
  17. package/lib/internal/calls/call-state-controller.d.ts +9 -0
  18. package/lib/internal/calls/call-state-controller.js +51 -24
  19. package/lib/telnyx-voice-app.js +127 -151
  20. package/lib/telnyx-voip-client.d.ts +21 -0
  21. package/lib/telnyx-voip-client.js +30 -0
  22. package/package.json +4 -1
  23. package/src/callkit/callkit-coordinator.ts +8 -2
  24. package/src/internal/calls/call-state-controller.ts +56 -24
  25. package/src/telnyx-voice-app.tsx +154 -170
  26. package/src/telnyx-voip-client.ts +31 -0
@@ -0,0 +1,247 @@
1
+ package com.telnyx.react_voice_commons
2
+
3
+ import android.content.Intent
4
+ import android.os.Build
5
+ import android.util.Log
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
8
+ import com.facebook.react.bridge.ReactMethod
9
+ import com.facebook.react.bridge.Promise
10
+ import com.facebook.react.bridge.WritableMap
11
+ import com.facebook.react.bridge.Arguments
12
+ import com.facebook.react.modules.core.DeviceEventManagerModule
13
+
14
+ class VoicePnBridgeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
15
+
16
+ companion object {
17
+ private const val TAG = "VoicePnBridgeModule"
18
+ }
19
+
20
+ override fun getName(): String {
21
+ return "VoicePnBridge"
22
+ }
23
+
24
+ @ReactMethod
25
+ fun getPendingPushAction(promise: Promise) {
26
+ try {
27
+ val (action, metadata) = VoicePnManager.getPendingPushAction(reactApplicationContext)
28
+
29
+ Log.d(TAG, "Retrieved pending action: '$action'")
30
+ Log.d(TAG, "Retrieved metadata: '$metadata'")
31
+ Log.d(TAG, "Metadata length: ${metadata?.length ?: 0}")
32
+
33
+ val result = Arguments.createMap()
34
+ result.putString("action", action)
35
+ result.putString("metadata", metadata)
36
+
37
+ promise.resolve(result)
38
+ } catch (e: Exception) {
39
+ Log.e(TAG, "Error getting pending push action", e)
40
+ promise.reject("GET_PENDING_PUSH_ACTION_ERROR", e.message, e)
41
+ }
42
+ }
43
+
44
+ @ReactMethod
45
+ fun setPendingPushAction(action: String, metadata: String, promise: Promise) {
46
+ try {
47
+ Log.d(TAG, "Setting pending push action: '$action' with metadata length: ${metadata.length}")
48
+ val result = VoicePnManager.setPendingPushAction(reactApplicationContext, action, metadata)
49
+ promise.resolve(result)
50
+ } catch (e: Exception) {
51
+ Log.e(TAG, "Error setting pending push action", e)
52
+ promise.reject("SET_PENDING_PUSH_ACTION_ERROR", e.message, e)
53
+ }
54
+ }
55
+
56
+ @ReactMethod
57
+ fun clearPendingPushAction(promise: Promise) {
58
+ try {
59
+ val result = VoicePnManager.clearPendingPushAction(reactApplicationContext)
60
+ promise.resolve(result)
61
+ } catch (e: Exception) {
62
+ promise.reject("CLEAR_PENDING_PUSH_ACTION_ERROR", e.message, e)
63
+ }
64
+ }
65
+
66
+ /**
67
+ * React Native → Android: Get any pending call action from notification buttons
68
+ * Uses the same reliable pattern as getPendingPushAction
69
+ */
70
+ @ReactMethod
71
+ fun getPendingCallAction(promise: Promise) {
72
+ try {
73
+ val (action, callId, timestamp) = VoicePnManager.getPendingCallAction(reactApplicationContext)
74
+
75
+ Log.d(TAG, "Retrieved pending call action: '$action' for callId: '$callId'")
76
+
77
+ val result = Arguments.createMap()
78
+ result.putString("action", action)
79
+ result.putString("callId", callId)
80
+ result.putDouble("timestamp", timestamp?.toDouble() ?: 0.0)
81
+
82
+ promise.resolve(result)
83
+ } catch (e: Exception) {
84
+ Log.e(TAG, "Error getting pending call action", e)
85
+ promise.reject("GET_PENDING_CALL_ACTION_ERROR", e.message, e)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * React Native → Android: Clear any pending call action
91
+ */
92
+ @ReactMethod
93
+ fun clearPendingCallAction(promise: Promise) {
94
+ try {
95
+ val result = VoicePnManager.clearPendingCallAction(reactApplicationContext)
96
+ promise.resolve(result)
97
+ } catch (e: Exception) {
98
+ Log.e(TAG, "Error clearing pending call action", e)
99
+ promise.reject("CLEAR_PENDING_CALL_ACTION_ERROR", e.message, e)
100
+ }
101
+ }
102
+
103
+ /**
104
+ * React Native → Android: End/hang up the current call
105
+ */
106
+ @ReactMethod
107
+ fun endCall(callId: String?, promise: Promise) {
108
+ try {
109
+ Log.d(TAG, "endCall called from React Native for callId: $callId")
110
+
111
+ // Hide any ongoing call notification and stop the foreground service
112
+ TelnyxNotificationHelper.hideOngoingCallNotificationFromContext(reactApplicationContext)
113
+
114
+ // Stop the foreground service that's keeping the notification alive
115
+ try {
116
+ val serviceIntent = Intent(reactApplicationContext, CallForegroundService::class.java).apply {
117
+ setAction(CallForegroundService.ACTION_STOP_FOREGROUND_SERVICE)
118
+ }
119
+ reactApplicationContext.startService(serviceIntent)
120
+ Log.d(TAG, "Stopped CallForegroundService from endCall")
121
+ } catch (e: Exception) {
122
+ Log.e(TAG, "Failed to stop CallForegroundService from endCall", e)
123
+ // Don't fail the call end if service stop fails
124
+ }
125
+
126
+ // Store the call action for any listeners (using same pattern)
127
+ VoicePnManager.setPendingCallAction(reactApplicationContext, "call_ended_from_rn", callId, System.currentTimeMillis())
128
+
129
+ promise.resolve(true)
130
+ } catch (e: Exception) {
131
+ Log.e(TAG, "Error ending call", e)
132
+ promise.reject("END_CALL_ERROR", e.message, e)
133
+ }
134
+ }
135
+
136
+ /**
137
+ * React Native → Android: Show ongoing call notification
138
+ */
139
+ @ReactMethod
140
+ fun showOngoingCallNotification(callerName: String?, callerNumber: String?, callId: String?, promise: Promise) {
141
+ try {
142
+ Log.d(TAG, "showOngoingCallNotification called from React Native")
143
+
144
+ val notificationHelper = TelnyxNotificationHelper(reactApplicationContext)
145
+ notificationHelper.showOngoingCallNotification(
146
+ callerName ?: "Unknown Caller",
147
+ callerNumber ?: "",
148
+ callId ?: "unknown"
149
+ )
150
+
151
+ // Also start the foreground service to keep the WebRTC connection alive
152
+ try {
153
+ val serviceIntent = Intent(reactApplicationContext, CallForegroundService::class.java).apply {
154
+ setAction(CallForegroundService.ACTION_START_FOREGROUND_SERVICE)
155
+ putExtra(CallForegroundService.EXTRA_CALLER_NAME, callerName ?: "Unknown Caller")
156
+ putExtra(CallForegroundService.EXTRA_CALLER_NUMBER, callerNumber ?: "")
157
+ putExtra(CallForegroundService.EXTRA_CALL_ID, callId ?: "unknown")
158
+ }
159
+
160
+ // Use startForegroundService for API 26+, startService for older versions
161
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
162
+ reactApplicationContext.startForegroundService(serviceIntent)
163
+ } else {
164
+ reactApplicationContext.startService(serviceIntent)
165
+ }
166
+ Log.d(TAG, "Started CallForegroundService for ongoing call")
167
+ } catch (e: Exception) {
168
+ Log.e(TAG, "Failed to start CallForegroundService", e)
169
+ // Don't fail the notification if service start fails
170
+ }
171
+
172
+ promise.resolve(true)
173
+ } catch (e: Exception) {
174
+ Log.e(TAG, "Error showing ongoing call notification", e)
175
+ promise.reject("SHOW_ONGOING_NOTIFICATION_ERROR", e.message, e)
176
+ }
177
+ }
178
+
179
+ /**
180
+ * React Native → Android: Hide ongoing call notification
181
+ */
182
+ @ReactMethod
183
+ fun hideOngoingCallNotification(promise: Promise) {
184
+ try {
185
+ Log.d(TAG, "hideOngoingCallNotification called from React Native")
186
+
187
+ TelnyxNotificationHelper.hideOngoingCallNotificationFromContext(reactApplicationContext)
188
+
189
+ // Also stop the foreground service
190
+ try {
191
+ val serviceIntent = Intent(reactApplicationContext, CallForegroundService::class.java).apply {
192
+ setAction(CallForegroundService.ACTION_STOP_FOREGROUND_SERVICE)
193
+ }
194
+ reactApplicationContext.startService(serviceIntent)
195
+ Log.d(TAG, "Stopped CallForegroundService")
196
+ } catch (e: Exception) {
197
+ Log.e(TAG, "Failed to stop CallForegroundService", e)
198
+ // Don't fail the notification hiding if service stop fails
199
+ }
200
+
201
+ promise.resolve(true)
202
+ } catch (e: Exception) {
203
+ Log.e(TAG, "Error hiding ongoing call notification", e)
204
+ promise.reject("HIDE_ONGOING_NOTIFICATION_ERROR", e.message, e)
205
+ }
206
+ }
207
+
208
+ /**
209
+ * React Native → Android: Hide any incoming call notification
210
+ */
211
+ @ReactMethod
212
+ fun hideIncomingCallNotification(promise: Promise) {
213
+ try {
214
+ Log.d(TAG, "hideIncomingCallNotification called from React Native")
215
+
216
+ TelnyxNotificationHelper.hideNotificationFromContext(reactApplicationContext)
217
+
218
+ promise.resolve(true)
219
+ } catch (e: Exception) {
220
+ Log.e(TAG, "Error hiding incoming call notification", e)
221
+ promise.reject("HIDE_INCOMING_NOTIFICATION_ERROR", e.message, e)
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Emit call action event to React Native (for immediate notification)
227
+ * Called from TelnyxMainActivity when user taps notification action
228
+ */
229
+ fun emitCallActionEvent(action: String, callId: String) {
230
+ try {
231
+ Log.d(TAG, "Emitting call action event to React Native: action=$action, callId=$callId")
232
+
233
+ val params = Arguments.createMap()
234
+ params.putString("action", action)
235
+ params.putString("callId", callId)
236
+ params.putDouble("timestamp", System.currentTimeMillis().toDouble())
237
+
238
+ reactApplicationContext
239
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
240
+ ?.emit("TelnyxCallAction", params)
241
+
242
+ Log.d(TAG, "Call action event emitted successfully")
243
+ } catch (e: Exception) {
244
+ Log.e(TAG, "Error emitting call action event", e)
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,17 @@
1
+ package com.telnyx.react_voice_commons
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class VoicePnBridgePackage : ReactPackage {
9
+
10
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
11
+ return listOf(VoicePnBridgeModule(reactContext))
12
+ }
13
+
14
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
+ return emptyList()
16
+ }
17
+ }
@@ -0,0 +1,154 @@
1
+ package com.telnyx.react_voice_commons
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.util.Log
6
+
7
+ object VoicePnManager {
8
+
9
+ private const val TAG = "VoicePnManager"
10
+ private const val PREFS_NAME = "telnyx_voice_prefs"
11
+
12
+ private fun getSharedPreferences(context: Context): SharedPreferences {
13
+ return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
14
+ }
15
+
16
+ // UserDefaults equivalent functions
17
+ fun getVoipToken(context: Context): String? {
18
+ Log.d(TAG, "getVoipToken called")
19
+ return getSharedPreferences(context).getString("voip_token", null)
20
+ }
21
+
22
+ fun setVoipToken(context: Context, token: String?) {
23
+ Log.d(TAG, "setVoipToken called with: $token")
24
+ val editor = getSharedPreferences(context).edit()
25
+ if (token != null) {
26
+ editor.putString("voip_token", token)
27
+ } else {
28
+ editor.remove("voip_token")
29
+ }
30
+ editor.apply()
31
+ }
32
+
33
+ fun getPendingVoipPush(context: Context): String? {
34
+ Log.d(TAG, "getPendingVoipPush called")
35
+ return getSharedPreferences(context).getString("pending_voip_push", null)
36
+ }
37
+
38
+ fun setPendingVoipPush(context: Context, push: String?) {
39
+ Log.d(TAG, "setPendingVoipPush called with: $push")
40
+ val editor = getSharedPreferences(context).edit()
41
+ if (push != null) {
42
+ editor.putString("pending_voip_push", push)
43
+ } else {
44
+ editor.remove("pending_voip_push")
45
+ }
46
+ editor.apply()
47
+ }
48
+
49
+ fun clearPendingVoipPush(context: Context): Boolean {
50
+ Log.d(TAG, "clearPendingVoipPush called")
51
+ val editor = getSharedPreferences(context).edit()
52
+ editor.remove("pending_voip_push")
53
+ editor.apply()
54
+ return true
55
+ }
56
+
57
+ fun getPendingVoipAction(context: Context): String? {
58
+ Log.d(TAG, "getPendingVoipAction called")
59
+ return getSharedPreferences(context).getString("pending_voip_action", null)
60
+ }
61
+
62
+ fun setPendingVoipAction(context: Context, action: String?) {
63
+ Log.d(TAG, "setPendingVoipAction called with: $action")
64
+ val editor = getSharedPreferences(context).edit()
65
+ if (action != null) {
66
+ editor.putString("pending_voip_action", action)
67
+ } else {
68
+ editor.remove("pending_voip_action")
69
+ }
70
+ editor.apply()
71
+ }
72
+
73
+ fun clearPendingVoipAction(context: Context): Boolean {
74
+ Log.d(TAG, "clearPendingVoipAction called")
75
+ val editor = getSharedPreferences(context).edit()
76
+ editor.remove("pending_voip_action")
77
+ editor.apply()
78
+ return true
79
+ }
80
+
81
+ // PnModule functions
82
+ fun setPendingPushAction(context: Context, action: String, metadata: String) {
83
+ Log.d(TAG, "setPendingPushAction called with action: $action")
84
+ val editor = getSharedPreferences(context).edit()
85
+ editor.putString("pending_push_action", action)
86
+ editor.putString("pending_push_metadata", metadata)
87
+ editor.apply()
88
+ }
89
+
90
+ fun getPendingPushAction(context: Context): Pair<String?, String?> {
91
+ Log.d(TAG, "getPendingPushAction called")
92
+ val prefs = getSharedPreferences(context)
93
+ val action = prefs.getString("pending_push_action", null)
94
+ val metadata = prefs.getString("pending_push_metadata", null)
95
+ return Pair(action, metadata)
96
+ }
97
+
98
+ fun clearPendingPushAction(context: Context): Boolean {
99
+ Log.d(TAG, "clearPendingPushAction called")
100
+ val editor = getSharedPreferences(context).edit()
101
+ editor.remove("pending_push_action")
102
+ editor.remove("pending_push_metadata")
103
+ editor.apply()
104
+ return true
105
+ }
106
+
107
+ fun checkPendingPushAction(context: Context): Boolean {
108
+ Log.d(TAG, "checkPendingPushAction called")
109
+ val (pendingAction, pendingMetadata) = getPendingPushAction(context)
110
+
111
+ if (pendingAction != null && pendingMetadata != null) {
112
+ Log.d(TAG, "Found pending action: $pendingAction")
113
+ // Clear after checking
114
+ clearPendingPushAction(context)
115
+ return true
116
+ } else {
117
+ Log.d(TAG, "No pending action found")
118
+ return false
119
+ }
120
+ }
121
+
122
+ // Call action management (using same pattern as push actions)
123
+ fun setPendingCallAction(context: Context, action: String, callId: String?, timestamp: Long) {
124
+ Log.d(TAG, "setPendingCallAction called with action: $action, callId: $callId")
125
+ val editor = getSharedPreferences(context).edit()
126
+ editor.putString("pending_call_action", action)
127
+ editor.putString("pending_call_id", callId)
128
+ editor.putLong("pending_call_timestamp", timestamp)
129
+ editor.apply()
130
+ }
131
+
132
+ fun getPendingCallAction(context: Context): Triple<String?, String?, Long?> {
133
+ Log.d(TAG, "getPendingCallAction called")
134
+ val prefs = getSharedPreferences(context)
135
+ val action = prefs.getString("pending_call_action", null)
136
+ val callId = prefs.getString("pending_call_id", null)
137
+ val timestamp = if (prefs.contains("pending_call_timestamp")) {
138
+ prefs.getLong("pending_call_timestamp", 0)
139
+ } else {
140
+ null
141
+ }
142
+ return Triple(action, callId, timestamp)
143
+ }
144
+
145
+ fun clearPendingCallAction(context: Context): Boolean {
146
+ Log.d(TAG, "clearPendingCallAction called")
147
+ val editor = getSharedPreferences(context).edit()
148
+ editor.remove("pending_call_action")
149
+ editor.remove("pending_call_id")
150
+ editor.remove("pending_call_timestamp")
151
+ editor.apply()
152
+ return true
153
+ }
154
+ }
@@ -28,7 +28,7 @@ RCT_EXTERN_METHOD(reportCallConnected:(NSString *)callUUID
28
28
  rejecter:(RCTPromiseRejectBlock)reject)
29
29
 
30
30
  RCT_EXTERN_METHOD(reportCallEnded:(NSString *)callUUID
31
- reason:(NSNumber *)reason
31
+ reason:(nonnull NSNumber *)reason
32
32
  resolver:(RCTPromiseResolveBlock)resolve
33
33
  rejecter:(RCTPromiseRejectBlock)reject)
34
34
 
@@ -358,15 +358,7 @@ import React
358
358
  )
359
359
  }
360
360
 
361
- func configureAudioSession() {
362
- let audioSession = AVAudioSession.sharedInstance()
363
- do {
364
- try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
365
- NSLog("Succeeded to activate audio session")
366
- } catch {
367
- NSLog("Failed to activate audio session: \(error)")
368
- }
369
- }
361
+
370
362
 
371
363
  private func observeAppDelegate() {
372
364
  // Automatically hook into app lifecycle if needed
@@ -760,8 +752,6 @@ import React
760
752
 
761
753
  NSLog("[TelnyxVoipPushHandler] ✅ CallKit provider ready, reporting incoming call")
762
754
 
763
- // Configure audio session before reporting the call
764
- //callKitManager.configureAudioSession()
765
755
 
766
756
  // Store call data manually and report to CallKit
767
757
  let isAppRunning = CallKitBridge.shared != nil
package/ios/README.md CHANGED
@@ -32,7 +32,7 @@ Add the following to your iOS app's `Info.plist`:
32
32
 
33
33
  ### 2. AppDelegate Integration
34
34
 
35
- #### Option A: Easy Integration (Recommended)
35
+ ### Option A: Easy Integration (Recommended)
36
36
 
37
37
  Import and use the `TelnyxVoiceAppDelegate` protocol:
38
38
 
@@ -77,7 +77,7 @@ extension AppDelegate: PKPushRegistryDelegate {}
77
77
  extension AppDelegate: CXProviderDelegate {}
78
78
  ```
79
79
 
80
- #### Option B: Manual Integration
80
+ ### Option B: Manual Integration
81
81
 
82
82
  If you prefer to implement the delegates manually:
83
83
 
@@ -320,6 +320,12 @@ class CallKitCoordinator {
320
320
  console.log('CallKitCoordinator: End action already being processed, skipping duplicate');
321
321
  return;
322
322
  }
323
+ if (this.endedCalls.has(callKitUUID)) {
324
+ console.log('CallKitCoordinator: Call already ended, skipping duplicate end action');
325
+ return;
326
+ }
327
+ // Mark as ended immediately to prevent any duplicate processing
328
+ this.endedCalls.add(callKitUUID);
323
329
  const call = this.callMap.get(callKitUUID);
324
330
  if (!call) {
325
331
  console.warn('CallKitCoordinator: No WebRTC call found for CallKit end action', {
@@ -598,9 +604,7 @@ class CallKitCoordinator {
598
604
  * Clean up call mappings and listeners
599
605
  */
600
606
  cleanupCall(callKitUUID) {
601
- // Remove from all tracking sets
602
607
  this.processingCalls.delete(callKitUUID);
603
- this.endedCalls.delete(callKitUUID);
604
608
  this.connectedCalls.delete(callKitUUID);
605
609
  // Get the call before removing it
606
610
  const call = this.callMap.get(callKitUUID);
@@ -31,6 +31,15 @@ export declare class CallStateController {
31
31
  * Current active call (synchronous access)
32
32
  */
33
33
  get currentActiveCall(): Call | null;
34
+ /**
35
+ * Access any active call tracked by the client.
36
+ * A call will be accessible until it has ended (transitioned to the ENDED state).
37
+ * This matches the TelnyxRTC `getCall(callId)` method for multi-call support.
38
+ *
39
+ * @param callId The unique identifier of a call.
40
+ * @returns The Call object that matches the requested callId, or null if not found.
41
+ */
42
+ getCall(callId: string): Call | null;
34
43
  /**
35
44
  * Set a call to connecting state (used for push notification calls when answered via CallKit)
36
45
  * @param callId The ID of the call to set to connecting state
@@ -18,7 +18,7 @@ class CallStateController {
18
18
  this._calls = new rxjs_1.BehaviorSubject([]);
19
19
  this._callMap = new Map();
20
20
  this._disposed = false;
21
- console.log('🔧 CallStateController: Constructor called - instance created');
21
+ console.log('CallStateController: Constructor called - instance created');
22
22
  // Don't set up client listeners here - client doesn't exist yet
23
23
  // Will be called when client is available
24
24
  }
@@ -69,6 +69,17 @@ class CallStateController {
69
69
  ) || null
70
70
  );
71
71
  }
72
+ /**
73
+ * Access any active call tracked by the client.
74
+ * A call will be accessible until it has ended (transitioned to the ENDED state).
75
+ * This matches the TelnyxRTC `getCall(callId)` method for multi-call support.
76
+ *
77
+ * @param callId The unique identifier of a call.
78
+ * @returns The Call object that matches the requested callId, or null if not found.
79
+ */
80
+ getCall(callId) {
81
+ return this._callMap.get(callId) || null;
82
+ }
72
83
  /**
73
84
  * Set a call to connecting state (used for push notification calls when answered via CallKit)
74
85
  * @param callId The ID of the call to set to connecting state
@@ -99,14 +110,11 @@ class CallStateController {
99
110
  * This should be called by the session manager after client creation
100
111
  */
101
112
  initializeClientListeners() {
102
- console.log('🔧 CallStateController: initializeClientListeners called');
103
- console.log(
104
- '🔧 CallStateController: Current client exists:',
105
- !!this._sessionManager.telnyxClient
106
- );
113
+ console.log('CallStateController: initializeClientListeners called');
114
+ console.log('CallStateController: Current client exists:', !!this._sessionManager.telnyxClient);
107
115
  this._setupClientListeners();
108
116
  // CallKit integration now handled by CallKitCoordinator
109
- console.log('🔧 CallStateController: Using CallKitCoordinator for CallKit integration');
117
+ console.log('CallStateController: Using CallKitCoordinator for CallKit integration');
110
118
  }
111
119
  /**
112
120
  * Initiate a new outgoing call
@@ -170,24 +178,24 @@ class CallStateController {
170
178
  * Set up event listeners for the Telnyx client
171
179
  */
172
180
  _setupClientListeners() {
173
- console.log('🔧 CallStateController: Setting up client listeners...');
181
+ console.log('CallStateController: Setting up client listeners...');
174
182
  if (!this._sessionManager.telnyxClient) {
175
- console.log('🔧 CallStateController: No telnyxClient available yet, skipping listener setup');
183
+ console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
176
184
  return;
177
185
  }
178
- console.log('🔧 CallStateController: TelnyxClient found, setting up incoming call listener');
186
+ console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
179
187
  console.log(
180
- '🔧 CallStateController: Client instance:',
188
+ 'CallStateController: Client instance:',
181
189
  this._sessionManager.telnyxClient.constructor.name
182
190
  );
183
191
  // Listen for incoming calls
184
192
  this._sessionManager.telnyxClient.on('telnyx.call.incoming', (telnyxCall, msg) => {
185
- console.log('📞 CallStateController: Incoming call received:', telnyxCall.callId);
193
+ console.log('CallStateController: Incoming call received:', telnyxCall.callId);
186
194
  this._handleIncomingCall(telnyxCall, msg, false);
187
195
  });
188
196
  // Listen for reattached calls (after network reconnection)
189
197
  this._sessionManager.telnyxClient.on('telnyx.call.reattached', (telnyxCall, msg) => {
190
- console.log('📞 CallStateController: Reattached call received:', telnyxCall.callId);
198
+ console.log('CallStateController: Reattached call received:', telnyxCall.callId);
191
199
  this._handleIncomingCall(telnyxCall, msg, true);
192
200
  });
193
201
  // Verify listeners are set up
@@ -196,14 +204,33 @@ class CallStateController {
196
204
  const reattachedListeners =
197
205
  this._sessionManager.telnyxClient.listenerCount('telnyx.call.reattached');
198
206
  console.log(
199
- '🔧 CallStateController: Listeners registered - incoming:',
207
+ 'CallStateController: Listeners registered - incoming:',
200
208
  incomingListeners,
201
209
  'reattached:',
202
210
  reattachedListeners
203
211
  );
204
- // Listen for other call events if needed
205
- // this._sessionManager.telnyxClient.on('telnyx.call.stateChange', this._handleCallStateChange.bind(this));
206
- console.log('🔧 CallStateController: Client listeners set up successfully');
212
+ // Listen for call state changes from the TelnyxRTC client (multi-call support)
213
+ this._sessionManager.telnyxClient.on('telnyx.call.stateChanged', (telnyxCall, state) => {
214
+ console.log(
215
+ 'CallStateController: Call state changed from TelnyxRTC:',
216
+ telnyxCall.callId,
217
+ state
218
+ );
219
+ // Find our wrapper call and update if needed
220
+ const call = this.findCallByTelnyxCall(telnyxCall);
221
+ if (call) {
222
+ console.log(
223
+ 'CallStateController: Found wrapper call, state sync handled by Call subscription'
224
+ );
225
+ }
226
+ });
227
+ // Listen for call removal events from TelnyxRTC (multi-call support)
228
+ this._sessionManager.telnyxClient.on('telnyx.call.removed', (callId) => {
229
+ console.log('CallStateController: Call removed from TelnyxRTC:', callId);
230
+ // The call cleanup is already handled by our call state subscription
231
+ // This event is informational for logging/debugging
232
+ });
233
+ console.log('CallStateController: Client listeners set up successfully');
207
234
  }
208
235
  /**
209
236
  * Handle incoming call or reattached call
@@ -211,20 +238,20 @@ class CallStateController {
211
238
  _handleIncomingCall(telnyxCall, inviteMsg, isReattached = false) {
212
239
  const callId = telnyxCall.callId || this._generateCallId();
213
240
  console.log(
214
- '📞 CallStateController: Handling incoming call:',
241
+ 'CallStateController: Handling incoming call:',
215
242
  callId,
216
243
  'isReattached:',
217
244
  isReattached
218
245
  );
219
- console.log('📞 CallStateController: TelnyxCall object:', telnyxCall);
220
- console.log('📞 CallStateController: Invite message:', inviteMsg);
246
+ console.log('CallStateController: TelnyxCall object:', telnyxCall);
247
+ console.log('CallStateController: Invite message:', inviteMsg);
221
248
  // For reattached calls, remove existing call and create new one
222
249
  if (isReattached && this._callMap.has(callId)) {
223
- console.log('📞 CallStateController: Removing existing call for reattachment');
250
+ console.log('CallStateController: Removing existing call for reattachment');
224
251
  const existingCall = this._callMap.get(callId);
225
252
  if (existingCall) {
226
253
  console.log(
227
- '📞 CallStateController: Existing call state before removal:',
254
+ 'CallStateController: Existing call state before removal:',
228
255
  existingCall.currentState
229
256
  );
230
257
  this._removeCall(callId);
@@ -242,7 +269,7 @@ class CallStateController {
242
269
  callerNumber = inviteMsg.params.caller_id_number || '';
243
270
  callerName = inviteMsg.params.caller_id_name || '';
244
271
  console.log(
245
- '📞 CallStateController: Extracted caller info from invite - Number:',
272
+ 'CallStateController: Extracted caller info from invite - Number:',
246
273
  callerNumber,
247
274
  'Name:',
248
275
  callerName
@@ -252,7 +279,7 @@ class CallStateController {
252
279
  callerNumber = telnyxCall.remoteCallerIdNumber || '';
253
280
  callerName = telnyxCall.remoteCallerIdName || '';
254
281
  console.log(
255
- '📞 CallStateController: Extracted caller info from TelnyxCall - Number:',
282
+ 'CallStateController: Extracted caller info from TelnyxCall - Number:',
256
283
  callerNumber,
257
284
  'Name:',
258
285
  callerName