@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.
- package/CHANGELOG.md +12 -0
- package/README.md +24 -24
- package/android/build.gradle +45 -0
- package/android/src/main/AndroidManifest.xml +38 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +83 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxFirebaseMessagingService.kt +179 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxMainActivity.kt +216 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationActionReceiver.kt +177 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +277 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +247 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgePackage.kt +17 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +154 -0
- package/ios/CallKitBridge.m +1 -1
- package/ios/CallKitBridge.swift +1 -11
- package/ios/README.md +2 -2
- package/lib/callkit/callkit-coordinator.js +6 -2
- package/lib/internal/calls/call-state-controller.d.ts +9 -0
- package/lib/internal/calls/call-state-controller.js +51 -24
- package/lib/telnyx-voice-app.js +127 -151
- package/lib/telnyx-voip-client.d.ts +21 -0
- package/lib/telnyx-voip-client.js +30 -0
- package/package.json +4 -1
- package/src/callkit/callkit-coordinator.ts +8 -2
- package/src/internal/calls/call-state-controller.ts +56 -24
- package/src/telnyx-voice-app.tsx +154 -170
- 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
|
+
}
|
package/ios/CallKitBridge.m
CHANGED
|
@@ -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
|
|
package/ios/CallKitBridge.swift
CHANGED
|
@@ -358,15 +358,7 @@ import React
|
|
|
358
358
|
)
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
181
|
+
console.log('CallStateController: Setting up client listeners...');
|
|
174
182
|
if (!this._sessionManager.telnyxClient) {
|
|
175
|
-
console.log('
|
|
183
|
+
console.log('CallStateController: No telnyxClient available yet, skipping listener setup');
|
|
176
184
|
return;
|
|
177
185
|
}
|
|
178
|
-
console.log('
|
|
186
|
+
console.log('CallStateController: TelnyxClient found, setting up incoming call listener');
|
|
179
187
|
console.log(
|
|
180
|
-
'
|
|
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('
|
|
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('
|
|
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
|
-
'
|
|
207
|
+
'CallStateController: Listeners registered - incoming:',
|
|
200
208
|
incomingListeners,
|
|
201
209
|
'reattached:',
|
|
202
210
|
reattachedListeners
|
|
203
211
|
);
|
|
204
|
-
// Listen for
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
'
|
|
241
|
+
'CallStateController: Handling incoming call:',
|
|
215
242
|
callId,
|
|
216
243
|
'isReattached:',
|
|
217
244
|
isReattached
|
|
218
245
|
);
|
|
219
|
-
console.log('
|
|
220
|
-
console.log('
|
|
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('
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
282
|
+
'CallStateController: Extracted caller info from TelnyxCall - Number:',
|
|
256
283
|
callerNumber,
|
|
257
284
|
'Name:',
|
|
258
285
|
callerName
|