@telnyx/react-voice-commons-sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +28 -7
- package/android/src/main/java/com/telnyx/react_voice_commons/CallForegroundService.kt +16 -2
- package/android/src/main/java/com/telnyx/react_voice_commons/TelnyxNotificationHelper.kt +345 -24
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnBridgeModule.kt +19 -0
- package/android/src/main/java/com/telnyx/react_voice_commons/VoicePnManager.kt +19 -0
- package/ios/CallKitBridge.m +20 -1
- package/ios/CallKitBridge.swift +527 -78
- package/ios/README.md +2 -2
- package/ios/VoicePnBridge.m +6 -0
- package/ios/VoicePnBridge.swift +15 -0
- package/lib/callkit/callkit-coordinator.d.ts +39 -3
- package/lib/callkit/callkit-coordinator.js +561 -148
- package/lib/callkit/callkit.d.ts +7 -0
- package/lib/callkit/callkit.js +83 -11
- package/lib/hooks/useAppStateHandler.js +6 -2
- package/lib/internal/CallKitHandler.d.ts +3 -0
- package/lib/internal/CallKitHandler.js +52 -46
- package/lib/internal/calls/call-state-controller.d.ts +33 -1
- package/lib/internal/calls/call-state-controller.js +100 -28
- package/lib/internal/session/session-manager.d.ts +15 -1
- package/lib/internal/session/session-manager.js +214 -62
- package/lib/internal/voice-pn-bridge.d.ts +18 -0
- package/lib/internal/voice-pn-bridge.js +39 -0
- package/lib/models/call.js +35 -0
- package/lib/models/config.d.ts +10 -0
- package/lib/models/config.js +2 -0
- package/lib/telnyx-voice-app.js +85 -43
- package/lib/telnyx-voip-client.d.ts +46 -5
- package/lib/telnyx-voip-client.js +238 -22
- package/package.json +6 -4
- package/src/callkit/callkit-coordinator.ts +642 -160
- package/src/callkit/callkit.ts +110 -12
- package/src/hooks/useAppStateHandler.ts +7 -2
- package/src/internal/CallKitHandler.tsx +60 -55
- package/src/internal/calls/call-state-controller.ts +112 -29
- package/src/internal/session/session-manager.ts +260 -65
- package/src/internal/voice-pn-bridge.ts +47 -0
- package/src/models/call.ts +34 -0
- package/src/models/config.ts +10 -0
- package/src/telnyx-voice-app.tsx +96 -45
- package/src/telnyx-voip-client.ts +216 -24
- package/src/types/telnyx-sdk.d.ts +2 -1
package/ios/CallKitBridge.swift
CHANGED
|
@@ -9,6 +9,49 @@ import React
|
|
|
9
9
|
import UserNotifications
|
|
10
10
|
import WebRTC
|
|
11
11
|
|
|
12
|
+
/// Small synchronized dictionary used because React Native module methods
|
|
13
|
+
/// and CXProvider delegate callbacks are not guaranteed to share a queue.
|
|
14
|
+
public final class LockedDictionary<Key: Hashable, Value> {
|
|
15
|
+
private var storage: [Key: Value] = [:]
|
|
16
|
+
private let lock: NSRecursiveLock
|
|
17
|
+
|
|
18
|
+
init(lock: NSRecursiveLock) {
|
|
19
|
+
self.lock = lock
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public subscript(key: Key) -> Value? {
|
|
23
|
+
get {
|
|
24
|
+
lock.lock()
|
|
25
|
+
defer { lock.unlock() }
|
|
26
|
+
return storage[key]
|
|
27
|
+
}
|
|
28
|
+
set {
|
|
29
|
+
lock.lock()
|
|
30
|
+
defer { lock.unlock() }
|
|
31
|
+
storage[key] = newValue
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public var values: [Value] {
|
|
36
|
+
lock.lock()
|
|
37
|
+
defer { lock.unlock() }
|
|
38
|
+
return Array(storage.values)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@discardableResult
|
|
42
|
+
public func removeValue(forKey key: Key) -> Value? {
|
|
43
|
+
lock.lock()
|
|
44
|
+
defer { lock.unlock() }
|
|
45
|
+
return storage.removeValue(forKey: key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public func removeAll() {
|
|
49
|
+
lock.lock()
|
|
50
|
+
defer { lock.unlock() }
|
|
51
|
+
storage.removeAll()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
@objc(CallKitBridge)
|
|
13
56
|
class CallKitBridge: RCTEventEmitter {
|
|
14
57
|
|
|
@@ -29,6 +72,7 @@ import React
|
|
|
29
72
|
"CallKitDidReceiveStartCallAction",
|
|
30
73
|
"CallKitDidPerformAnswerCallAction",
|
|
31
74
|
"CallKitDidPerformEndCallAction",
|
|
75
|
+
"CallKitDidPerformHeldCallAction",
|
|
32
76
|
"CallKitDidReceivePush",
|
|
33
77
|
"AudioSessionActivated",
|
|
34
78
|
"AudioSessionDeactivated",
|
|
@@ -50,8 +94,9 @@ import React
|
|
|
50
94
|
}
|
|
51
95
|
|
|
52
96
|
// Direct event emission methods called by TelnyxVoiceAppDelegate
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
@discardableResult
|
|
98
|
+
public func emitCallEvent(_ eventName: String, callUUID: UUID, callData: [String: Any]?) -> Bool {
|
|
99
|
+
guard hasListeners else { return false }
|
|
55
100
|
|
|
56
101
|
let eventData: [String: Any] = [
|
|
57
102
|
"callUUID": callUUID.uuidString,
|
|
@@ -59,6 +104,22 @@ import React
|
|
|
59
104
|
]
|
|
60
105
|
|
|
61
106
|
sendEvent(withName: eventName, body: eventData)
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@discardableResult
|
|
111
|
+
public func emitHeldCallEvent(
|
|
112
|
+
callUUID: UUID, isOnHold: Bool, callData: [String: Any]?
|
|
113
|
+
) -> Bool {
|
|
114
|
+
guard hasListeners else { return false }
|
|
115
|
+
|
|
116
|
+
let eventData: [String: Any] = [
|
|
117
|
+
"callUUID": callUUID.uuidString,
|
|
118
|
+
"isOnHold": isOnHold,
|
|
119
|
+
"callData": callData ?? [:],
|
|
120
|
+
]
|
|
121
|
+
sendEvent(withName: "CallKitDidPerformHeldCallAction", body: eventData)
|
|
122
|
+
return true
|
|
62
123
|
}
|
|
63
124
|
|
|
64
125
|
// Event emission method for audio session changes
|
|
@@ -90,8 +151,17 @@ import React
|
|
|
90
151
|
let startCallAction = CXStartCallAction(call: uuid, handle: callHandle)
|
|
91
152
|
let transaction = CXTransaction(action: startCallAction)
|
|
92
153
|
|
|
154
|
+
manager.activeCalls[uuid] = [
|
|
155
|
+
"caller": displayName,
|
|
156
|
+
"handle": handle,
|
|
157
|
+
"uuid": uuid.uuidString,
|
|
158
|
+
"direction": "outgoing",
|
|
159
|
+
"source": "react_native",
|
|
160
|
+
]
|
|
161
|
+
|
|
93
162
|
callController.request(transaction) { error in
|
|
94
163
|
if let error = error {
|
|
164
|
+
manager.activeCalls.removeValue(forKey: uuid)
|
|
95
165
|
reject("START_CALL_ERROR", error.localizedDescription, error)
|
|
96
166
|
} else {
|
|
97
167
|
resolve(["success": true, "callUUID": callUUID])
|
|
@@ -120,11 +190,23 @@ import React
|
|
|
120
190
|
callUpdate.remoteHandle = callHandle
|
|
121
191
|
callUpdate.hasVideo = false
|
|
122
192
|
callUpdate.localizedCallerName = displayName
|
|
193
|
+
callUpdate.supportsHolding = true
|
|
194
|
+
|
|
195
|
+
manager.activeCalls[uuid] = [
|
|
196
|
+
"caller": displayName,
|
|
197
|
+
"handle": handle,
|
|
198
|
+
"uuid": uuid.uuidString,
|
|
199
|
+
"direction": "incoming",
|
|
200
|
+
"source": "react_native",
|
|
201
|
+
"isCallKitRegistered": false,
|
|
202
|
+
]
|
|
123
203
|
|
|
124
204
|
provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
|
|
125
205
|
if let error = error {
|
|
206
|
+
manager.activeCalls.removeValue(forKey: uuid)
|
|
126
207
|
reject("INCOMING_CALL_ERROR", error.localizedDescription, error)
|
|
127
208
|
} else {
|
|
209
|
+
manager.markCallKitRegistered(uuid)
|
|
128
210
|
resolve(["success": true, "callUUID": callUUID])
|
|
129
211
|
}
|
|
130
212
|
}
|
|
@@ -157,6 +239,127 @@ import React
|
|
|
157
239
|
}
|
|
158
240
|
}
|
|
159
241
|
|
|
242
|
+
@objc func completeHeldCallAction(
|
|
243
|
+
_ callUUID: String, success: Bool,
|
|
244
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
245
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
246
|
+
) {
|
|
247
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
248
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let manager = getCallKitManager()
|
|
253
|
+
let completeAction = {
|
|
254
|
+
guard let action = manager.pendingHeldActions.removeValue(forKey: uuid) else {
|
|
255
|
+
reject("NO_PENDING_HELD_ACTION", "No pending held action for UUID", nil)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if success {
|
|
260
|
+
if var callData = manager.activeCalls[uuid] {
|
|
261
|
+
callData["isOnHold"] = action.isOnHold
|
|
262
|
+
manager.activeCalls[uuid] = callData
|
|
263
|
+
}
|
|
264
|
+
action.fulfill()
|
|
265
|
+
} else {
|
|
266
|
+
action.fail()
|
|
267
|
+
}
|
|
268
|
+
resolve(["success": success, "callUUID": callUUID])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if Thread.isMainThread {
|
|
272
|
+
completeAction()
|
|
273
|
+
} else {
|
|
274
|
+
DispatchQueue.main.async(execute: completeAction)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@objc func setCallHeld(
|
|
279
|
+
_ callUUID: String, isOnHold: Bool,
|
|
280
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
281
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
282
|
+
) {
|
|
283
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
284
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let manager = getCallKitManager()
|
|
289
|
+
guard let callController = manager.callKitController else {
|
|
290
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
guard manager.activeCalls[uuid] != nil else {
|
|
295
|
+
reject("UNKNOWN_CALL", "Call must be registered with CallKit", nil)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let action = CXSetHeldCallAction(call: uuid, onHold: isOnHold)
|
|
300
|
+
let transaction = CXTransaction(action: action)
|
|
301
|
+
callController.request(transaction) { error in
|
|
302
|
+
if let error = error {
|
|
303
|
+
reject("SET_HELD_ERROR", error.localizedDescription, error)
|
|
304
|
+
} else {
|
|
305
|
+
resolve([
|
|
306
|
+
"success": true,
|
|
307
|
+
"callUUID": callUUID,
|
|
308
|
+
"isOnHold": isOnHold,
|
|
309
|
+
])
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@objc func swapCalls(
|
|
315
|
+
_ activeCallUUID: String, heldCallUUID: String,
|
|
316
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
317
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
318
|
+
) {
|
|
319
|
+
guard let activeUUID = UUID(uuidString: activeCallUUID),
|
|
320
|
+
let heldUUID = UUID(uuidString: heldCallUUID)
|
|
321
|
+
else {
|
|
322
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
guard activeUUID != heldUUID else {
|
|
327
|
+
reject("INVALID_SWAP", "Active and held call UUIDs must be different", nil)
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let manager = getCallKitManager()
|
|
332
|
+
guard let callController = manager.callKitController else {
|
|
333
|
+
reject("NO_CALLKIT", "CallKit not available", nil)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
guard manager.activeCalls[activeUUID] != nil,
|
|
338
|
+
manager.activeCalls[heldUUID] != nil
|
|
339
|
+
else {
|
|
340
|
+
reject("UNKNOWN_CALL", "Both calls must be registered with CallKit", nil)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let actions: [CXAction] = [
|
|
345
|
+
CXSetHeldCallAction(call: activeUUID, onHold: true),
|
|
346
|
+
CXSetHeldCallAction(call: heldUUID, onHold: false),
|
|
347
|
+
]
|
|
348
|
+
let transaction = CXTransaction(actions: actions)
|
|
349
|
+
|
|
350
|
+
callController.request(transaction) { error in
|
|
351
|
+
if let error = error {
|
|
352
|
+
reject("SWAP_CALLS_ERROR", error.localizedDescription, error)
|
|
353
|
+
} else {
|
|
354
|
+
resolve([
|
|
355
|
+
"success": true,
|
|
356
|
+
"activeCallUUID": activeCallUUID,
|
|
357
|
+
"heldCallUUID": heldCallUUID,
|
|
358
|
+
])
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
160
363
|
@objc func answerCall(
|
|
161
364
|
_ callUUID: String, resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
162
365
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
@@ -172,6 +375,15 @@ import React
|
|
|
172
375
|
return
|
|
173
376
|
}
|
|
174
377
|
|
|
378
|
+
guard manager.isCallKitRegistered(uuid) else {
|
|
379
|
+
reject(
|
|
380
|
+
"UNKNOWN_CALL",
|
|
381
|
+
"Call was not registered with CallKit or its registration was rejected",
|
|
382
|
+
nil
|
|
383
|
+
)
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
175
387
|
// Mark this call as already answered to prevent duplicate emission
|
|
176
388
|
if var callData = manager.activeCalls[uuid] {
|
|
177
389
|
callData["isAlreadyAnswered"] = true
|
|
@@ -190,6 +402,22 @@ import React
|
|
|
190
402
|
}
|
|
191
403
|
}
|
|
192
404
|
|
|
405
|
+
@objc func isCallRegistered(
|
|
406
|
+
_ callUUID: String, resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
407
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
408
|
+
) {
|
|
409
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
410
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let registered = getCallKitManager().isCallKitRegistered(uuid)
|
|
415
|
+
resolve([
|
|
416
|
+
"registered": registered,
|
|
417
|
+
"callUUID": callUUID,
|
|
418
|
+
])
|
|
419
|
+
}
|
|
420
|
+
|
|
193
421
|
@objc func reportCallConnected(
|
|
194
422
|
_ callUUID: String, resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
195
423
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
@@ -208,10 +436,9 @@ import React
|
|
|
208
436
|
provider.reportOutgoingCall(with: uuid, connectedAt: Date())
|
|
209
437
|
|
|
210
438
|
// Fulfill deferred CXAnswerCallAction now that peer connection is ready
|
|
211
|
-
if let pendingAction = manager.
|
|
439
|
+
if let pendingAction = manager.pendingAnswerActions.removeValue(forKey: uuid) {
|
|
212
440
|
NSLog("TelnyxVoice: reportCallConnected - fulfilling deferred CXAnswerCallAction")
|
|
213
441
|
pendingAction.fulfill()
|
|
214
|
-
manager.pendingAnswerAction = nil
|
|
215
442
|
} else {
|
|
216
443
|
// Fallback: ensure audio is enabled for non-push or already-fulfilled cases
|
|
217
444
|
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
|
@@ -254,6 +481,9 @@ import React
|
|
|
254
481
|
|
|
255
482
|
let endReason = CXCallEndedReason(rawValue: reason.intValue) ?? .remoteEnded
|
|
256
483
|
provider.reportCall(with: uuid, endedAt: Date(), reason: endReason)
|
|
484
|
+
manager.activeCalls.removeValue(forKey: uuid)
|
|
485
|
+
manager.pendingAnswerActions.removeValue(forKey: uuid)?.fail()
|
|
486
|
+
manager.pendingHeldActions.removeValue(forKey: uuid)?.fail()
|
|
257
487
|
resolve(["success": true])
|
|
258
488
|
}
|
|
259
489
|
|
|
@@ -286,6 +516,7 @@ import React
|
|
|
286
516
|
let callUpdate = CXCallUpdate()
|
|
287
517
|
callUpdate.remoteHandle = callHandle
|
|
288
518
|
callUpdate.localizedCallerName = displayName
|
|
519
|
+
callUpdate.supportsHolding = true
|
|
289
520
|
|
|
290
521
|
provider.reportCall(with: uuid, updated: callUpdate)
|
|
291
522
|
resolve(["success": true])
|
|
@@ -507,8 +738,33 @@ import React
|
|
|
507
738
|
public var voipRegistry: PKPushRegistry?
|
|
508
739
|
public var callKitProvider: CXProvider?
|
|
509
740
|
public var callKitController: CXCallController?
|
|
510
|
-
|
|
511
|
-
public var
|
|
741
|
+
private let stateLock = NSRecursiveLock()
|
|
742
|
+
public lazy var activeCalls = LockedDictionary<UUID, [String: Any]>(lock: stateLock)
|
|
743
|
+
public lazy var pendingAnswerActions = LockedDictionary<UUID, CXAnswerCallAction>(
|
|
744
|
+
lock: stateLock)
|
|
745
|
+
public lazy var pendingHeldActions = LockedDictionary<UUID, CXSetHeldCallAction>(
|
|
746
|
+
lock: stateLock)
|
|
747
|
+
|
|
748
|
+
fileprivate func markCallKitRegistered(_ callUUID: UUID) {
|
|
749
|
+
guard var callData = activeCalls[callUUID] else {
|
|
750
|
+
return
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
callData["isCallKitRegistered"] = true
|
|
754
|
+
activeCalls[callUUID] = callData
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
fileprivate func isCallKitRegistered(_ callUUID: UUID) -> Bool {
|
|
758
|
+
guard let callData = activeCalls[callUUID] else {
|
|
759
|
+
return false
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if callData["direction"] as? String == "incoming" {
|
|
763
|
+
return callData["isCallKitRegistered"] as? Bool == true
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return true
|
|
767
|
+
}
|
|
512
768
|
|
|
513
769
|
private override init() {
|
|
514
770
|
super.init()
|
|
@@ -529,18 +785,27 @@ import React
|
|
|
529
785
|
}
|
|
530
786
|
}
|
|
531
787
|
|
|
532
|
-
|
|
788
|
+
@discardableResult
|
|
789
|
+
public func setupSynchronously() -> CXProvider {
|
|
533
790
|
// Only initialize once
|
|
534
|
-
|
|
791
|
+
if let callKitProvider = callKitProvider {
|
|
535
792
|
NSLog("TelnyxVoice: CallKit already setup, skipping")
|
|
536
|
-
return
|
|
793
|
+
return callKitProvider
|
|
537
794
|
}
|
|
538
795
|
|
|
539
796
|
// CRITICAL: Setup CallKit synchronously for terminated app VoIP push handling
|
|
540
797
|
// This MUST happen in the same run loop as the VoIP push
|
|
541
798
|
NSLog("TelnyxVoice: Synchronous CallKit setup for terminated app scenario...")
|
|
542
799
|
setupCallKit()
|
|
800
|
+
if let callKitProvider = callKitProvider {
|
|
801
|
+
NSLog("TelnyxVoice: ✅ CallKit provider ready for terminated app handling")
|
|
802
|
+
return callKitProvider
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
NSLog("TelnyxVoice: CallKit setup did not create a provider; creating fallback provider")
|
|
806
|
+
let callKitProvider = installCallKitProvider()
|
|
543
807
|
NSLog("TelnyxVoice: ✅ CallKit provider ready for terminated app handling")
|
|
808
|
+
return callKitProvider
|
|
544
809
|
}
|
|
545
810
|
|
|
546
811
|
private func setupAutomatically() {
|
|
@@ -571,6 +836,10 @@ import React
|
|
|
571
836
|
return
|
|
572
837
|
}
|
|
573
838
|
|
|
839
|
+
_ = installCallKitProvider()
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private func installCallKitProvider() -> CXProvider {
|
|
574
843
|
// Use the localizedName from the app's bundle display name or fallback
|
|
575
844
|
let appName =
|
|
576
845
|
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
|
|
@@ -579,19 +848,66 @@ import React
|
|
|
579
848
|
|
|
580
849
|
let configuration = CXProviderConfiguration(localizedName: appName)
|
|
581
850
|
configuration.supportsVideo = false
|
|
582
|
-
configuration.maximumCallGroups =
|
|
851
|
+
configuration.maximumCallGroups = 2
|
|
583
852
|
configuration.maximumCallsPerCallGroup = 1
|
|
584
853
|
configuration.supportedHandleTypes = [.phoneNumber, .generic]
|
|
585
854
|
configuration.includesCallsInRecents = true
|
|
586
855
|
|
|
587
|
-
|
|
588
|
-
|
|
856
|
+
let provider = CXProvider(configuration: configuration)
|
|
857
|
+
provider.setDelegate(self, queue: nil)
|
|
858
|
+
callKitProvider = provider
|
|
589
859
|
callKitController = CXCallController()
|
|
590
860
|
|
|
591
861
|
NSLog("TelnyxVoice: CallKit provider and controller created")
|
|
862
|
+
return provider
|
|
592
863
|
}
|
|
593
864
|
|
|
594
|
-
|
|
865
|
+
fileprivate func reportAndEndWatchdogCall(
|
|
866
|
+
callUUID: UUID,
|
|
867
|
+
callerName: String,
|
|
868
|
+
callerNumber: String,
|
|
869
|
+
payload: [AnyHashable: Any],
|
|
870
|
+
source: String,
|
|
871
|
+
endedReason: CXCallEndedReason,
|
|
872
|
+
completion: (() -> Void)? = nil
|
|
873
|
+
) {
|
|
874
|
+
let callKitProvider = setupSynchronously()
|
|
875
|
+
|
|
876
|
+
activeCalls[callUUID] = [
|
|
877
|
+
"caller": callerName,
|
|
878
|
+
"handle": callerNumber,
|
|
879
|
+
"payload": payload,
|
|
880
|
+
"uuid": callUUID.uuidString,
|
|
881
|
+
"direction": "incoming",
|
|
882
|
+
"source": source,
|
|
883
|
+
"watchdogPlaceholder": true,
|
|
884
|
+
]
|
|
885
|
+
|
|
886
|
+
let handle = CXHandle(type: .phoneNumber, value: callerNumber)
|
|
887
|
+
let callUpdate = CXCallUpdate()
|
|
888
|
+
callUpdate.remoteHandle = handle
|
|
889
|
+
callUpdate.hasVideo = false
|
|
890
|
+
callUpdate.localizedCallerName = callerName
|
|
891
|
+
callUpdate.supportsHolding = true
|
|
892
|
+
|
|
893
|
+
callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
|
|
894
|
+
if let error = error {
|
|
895
|
+
NSLog(
|
|
896
|
+
"TelnyxVoice: Watchdog CallKit report failed for \(source): \(error.localizedDescription)"
|
|
897
|
+
)
|
|
898
|
+
} else {
|
|
899
|
+
NSLog("TelnyxVoice: Watchdog CallKit call reported for \(source)")
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
callKitProvider.reportCall(with: callUUID, endedAt: Date(), reason: endedReason)
|
|
903
|
+
self.activeCalls.removeValue(forKey: callUUID)
|
|
904
|
+
|
|
905
|
+
self.pendingAnswerActions.removeValue(forKey: callUUID)?.fail()
|
|
906
|
+
self.pendingHeldActions.removeValue(forKey: callUUID)?.fail()
|
|
907
|
+
|
|
908
|
+
completion?()
|
|
909
|
+
}
|
|
910
|
+
}
|
|
595
911
|
|
|
596
912
|
private func observeAppDelegate() {
|
|
597
913
|
// Automatically hook into app lifecycle if needed
|
|
@@ -620,28 +936,34 @@ import React
|
|
|
620
936
|
}
|
|
621
937
|
|
|
622
938
|
NSLog("TelnyxVoice: Missed call VoIP push received: \(payload)")
|
|
623
|
-
setupSynchronously()
|
|
939
|
+
let callKitProvider = setupSynchronously()
|
|
624
940
|
|
|
625
941
|
let callId = TelnyxMissedCallPush.callId(from: payload)
|
|
626
942
|
let missedCallId = TelnyxMissedCallPush.stableIdentifier(from: payload)
|
|
627
|
-
if hasProcessedMissedCall(id: missedCallId) {
|
|
628
|
-
NSLog("TelnyxVoice: Ignoring duplicate missed call push: \(missedCallId)")
|
|
629
|
-
completion?()
|
|
630
|
-
return true
|
|
631
|
-
}
|
|
632
|
-
|
|
633
943
|
let callUUID = TelnyxMissedCallPush.uuid(from: missedCallId)
|
|
634
944
|
let callerName = TelnyxMissedCallPush.callerName(from: payload)
|
|
635
945
|
let callerNumber = TelnyxMissedCallPush.callerNumber(from: payload)
|
|
636
946
|
|
|
947
|
+
if hasProcessedMissedCall(id: missedCallId) {
|
|
948
|
+
NSLog("TelnyxVoice: Suppressing duplicate missed call notification: \(missedCallId)")
|
|
949
|
+
reportAndEndWatchdogCall(
|
|
950
|
+
callUUID: UUID(),
|
|
951
|
+
callerName: callerName,
|
|
952
|
+
callerNumber: callerNumber,
|
|
953
|
+
payload: payload,
|
|
954
|
+
source: "missed_call_duplicate_watchdog",
|
|
955
|
+
endedReason: .remoteEnded,
|
|
956
|
+
completion: completion
|
|
957
|
+
)
|
|
958
|
+
return true
|
|
959
|
+
}
|
|
960
|
+
|
|
637
961
|
let finishMissedCall = {
|
|
638
|
-
|
|
962
|
+
callKitProvider.reportCall(with: callUUID, endedAt: Date(), reason: .remoteEnded)
|
|
639
963
|
self.activeCalls.removeValue(forKey: callUUID)
|
|
640
964
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
self.pendingAnswerAction = nil
|
|
644
|
-
}
|
|
965
|
+
self.pendingAnswerActions.removeValue(forKey: callUUID)?.fail()
|
|
966
|
+
self.pendingHeldActions.removeValue(forKey: callUUID)?.fail()
|
|
645
967
|
|
|
646
968
|
NSLog("TelnyxVoice: Ended stale CallKit call for missed call UUID: \(callUUID)")
|
|
647
969
|
self.clearPendingPushData(for: callId)
|
|
@@ -656,13 +978,30 @@ import React
|
|
|
656
978
|
|
|
657
979
|
if let activeCall = activeCalls[callUUID] {
|
|
658
980
|
guard activeCall["source"] as? String == "missed_call_push" else {
|
|
659
|
-
NSLog("TelnyxVoice:
|
|
981
|
+
NSLog("TelnyxVoice: Suppressing missed call notification because UUID belongs to an active call: \(callUUID)")
|
|
660
982
|
markMissedCallProcessed(id: missedCallId)
|
|
661
|
-
|
|
983
|
+
reportAndEndWatchdogCall(
|
|
984
|
+
callUUID: UUID(),
|
|
985
|
+
callerName: callerName,
|
|
986
|
+
callerNumber: callerNumber,
|
|
987
|
+
payload: payload,
|
|
988
|
+
source: "missed_call_collision_watchdog",
|
|
989
|
+
endedReason: .remoteEnded,
|
|
990
|
+
completion: completion
|
|
991
|
+
)
|
|
662
992
|
return true
|
|
663
993
|
}
|
|
664
994
|
|
|
665
|
-
|
|
995
|
+
reportAndEndWatchdogCall(
|
|
996
|
+
callUUID: UUID(),
|
|
997
|
+
callerName: callerName,
|
|
998
|
+
callerNumber: callerNumber,
|
|
999
|
+
payload: payload,
|
|
1000
|
+
source: "missed_call_active_watchdog",
|
|
1001
|
+
endedReason: .remoteEnded
|
|
1002
|
+
) {
|
|
1003
|
+
finishMissedCall()
|
|
1004
|
+
}
|
|
666
1005
|
return true
|
|
667
1006
|
}
|
|
668
1007
|
|
|
@@ -680,10 +1019,12 @@ import React
|
|
|
680
1019
|
callUpdate.remoteHandle = handle
|
|
681
1020
|
callUpdate.hasVideo = false
|
|
682
1021
|
callUpdate.localizedCallerName = callerName
|
|
1022
|
+
callUpdate.supportsHolding = true
|
|
683
1023
|
|
|
684
|
-
callKitProvider
|
|
1024
|
+
callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
|
|
685
1025
|
if let error = error {
|
|
686
1026
|
NSLog("TelnyxVoice: Missed call CallKit report failed: \(error.localizedDescription)")
|
|
1027
|
+
callKitProvider.reportCall(with: callUUID, endedAt: Date(), reason: .failed)
|
|
687
1028
|
self.activeCalls.removeValue(forKey: callUUID)
|
|
688
1029
|
self.clearPendingPushData(for: callId)
|
|
689
1030
|
completion?()
|
|
@@ -696,7 +1037,7 @@ import React
|
|
|
696
1037
|
return true
|
|
697
1038
|
}
|
|
698
1039
|
|
|
699
|
-
|
|
1040
|
+
fileprivate func clearPendingPushData(for callId: String?) {
|
|
700
1041
|
if hasPendingPushData(forDifferentCallThan: callId) {
|
|
701
1042
|
NSLog("TelnyxVoice: Skipping pending push cleanup for missed call; pending data belongs to a different call")
|
|
702
1043
|
return
|
|
@@ -721,7 +1062,9 @@ import React
|
|
|
721
1062
|
UserDefaults.standard.string(forKey: "@pending_callkit_uuid"),
|
|
722
1063
|
].compactMap { $0 }
|
|
723
1064
|
|
|
724
|
-
return pendingIds.contains {
|
|
1065
|
+
return pendingIds.contains {
|
|
1066
|
+
!$0.isEmpty && $0.caseInsensitiveCompare(missedCallId) != .orderedSame
|
|
1067
|
+
}
|
|
725
1068
|
}
|
|
726
1069
|
|
|
727
1070
|
private func pendingCallId(fromJsonForKey key: String) -> String? {
|
|
@@ -918,6 +1261,7 @@ import React
|
|
|
918
1261
|
"uuid": callUUID.uuidString,
|
|
919
1262
|
"direction": "incoming",
|
|
920
1263
|
"source": "push",
|
|
1264
|
+
"isCallKitRegistered": false,
|
|
921
1265
|
]
|
|
922
1266
|
|
|
923
1267
|
let handle = CXHandle(type: .phoneNumber, value: caller)
|
|
@@ -925,6 +1269,7 @@ import React
|
|
|
925
1269
|
callUpdate.remoteHandle = handle
|
|
926
1270
|
callUpdate.hasVideo = false
|
|
927
1271
|
callUpdate.localizedCallerName = caller
|
|
1272
|
+
callUpdate.supportsHolding = true
|
|
928
1273
|
|
|
929
1274
|
callKitProvider?.reportNewIncomingCall(with: callUUID, update: callUpdate) {
|
|
930
1275
|
[weak self] error in
|
|
@@ -932,6 +1277,7 @@ import React
|
|
|
932
1277
|
NSLog("TelnyxVoice: CallKit error: \(error.localizedDescription)")
|
|
933
1278
|
self?.activeCalls.removeValue(forKey: callUUID)
|
|
934
1279
|
} else {
|
|
1280
|
+
self?.markCallKitRegistered(callUUID)
|
|
935
1281
|
NSLog("TelnyxVoice: CallKit incoming call reported successfully")
|
|
936
1282
|
}
|
|
937
1283
|
completion()
|
|
@@ -946,12 +1292,35 @@ import React
|
|
|
946
1292
|
NSLog("📞 TelnyxVoice: CALLKIT PROVIDER RESET - Provider: \(provider)")
|
|
947
1293
|
NSLog("TelnyxVoice: CallKit provider reset - ending all active calls")
|
|
948
1294
|
activeCalls.removeAll()
|
|
1295
|
+
pendingAnswerActions.values.forEach { $0.fail() }
|
|
1296
|
+
pendingAnswerActions.removeAll()
|
|
1297
|
+
pendingHeldActions.values.forEach { $0.fail() }
|
|
1298
|
+
pendingHeldActions.removeAll()
|
|
949
1299
|
}
|
|
950
1300
|
|
|
951
1301
|
public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
|
952
1302
|
NSLog("📞 TelnyxVoice: CALLKIT ANSWER ACTION - Provider: \(provider), Action: \(action)")
|
|
953
1303
|
NSLog("TelnyxVoice: User answered call with UUID: \(action.callUUID)")
|
|
954
1304
|
|
|
1305
|
+
if activeCalls[action.callUUID]?["watchdogPlaceholder"] as? Bool == true {
|
|
1306
|
+
NSLog("TelnyxVoice: Failing answer for watchdog placeholder call: \(action.callUUID)")
|
|
1307
|
+
activeCalls.removeValue(forKey: action.callUUID)
|
|
1308
|
+
action.fail()
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
guard isCallKitRegistered(action.callUUID) else {
|
|
1313
|
+
NSLog("TelnyxVoice: Failing answer for unknown UUID: \(action.callUUID)")
|
|
1314
|
+
action.fail()
|
|
1315
|
+
return
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
guard pendingAnswerActions[action.callUUID] == nil else {
|
|
1319
|
+
NSLog("TelnyxVoice: Failing duplicate answer for UUID: \(action.callUUID)")
|
|
1320
|
+
action.fail()
|
|
1321
|
+
return
|
|
1322
|
+
}
|
|
1323
|
+
|
|
955
1324
|
// Check if this is a programmatic answer (call already answered in WebRTC)
|
|
956
1325
|
// vs a user answer from CallKit UI
|
|
957
1326
|
if let callData = activeCalls[action.callUUID],
|
|
@@ -961,25 +1330,83 @@ import React
|
|
|
961
1330
|
NSLog("TelnyxVoice: Call already answered in WebRTC, skipping emission")
|
|
962
1331
|
} else {
|
|
963
1332
|
// Notify React Native via CallKit bridge (only for user-initiated answers)
|
|
964
|
-
CallKitBridge.shared?.emitCallEvent(
|
|
1333
|
+
let eventDelivered = CallKitBridge.shared?.emitCallEvent(
|
|
965
1334
|
"CallKitDidPerformAnswerCallAction", callUUID: action.callUUID,
|
|
966
|
-
callData: activeCalls[action.callUUID])
|
|
1335
|
+
callData: activeCalls[action.callUUID]) ?? false
|
|
1336
|
+
if !eventDelivered {
|
|
1337
|
+
UserDefaults.standard.set(action.callUUID.uuidString, forKey: "pending_callkit_answer")
|
|
1338
|
+
UserDefaults.standard.synchronize()
|
|
1339
|
+
NSLog("TelnyxVoice: Stored pending CallKit answer for UUID: \(action.callUUID)")
|
|
1340
|
+
}
|
|
967
1341
|
}
|
|
968
1342
|
|
|
969
1343
|
// Defer action.fulfill() until reportCallConnected when peer connection is ready
|
|
970
1344
|
NSLog("TelnyxVoice: Deferring CXAnswerCallAction.fulfill() until peer connection is ready")
|
|
971
|
-
self.
|
|
1345
|
+
self.pendingAnswerActions[action.callUUID] = action
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
|
|
1349
|
+
NSLog(
|
|
1350
|
+
"TelnyxVoice: User requested held=\(action.isOnHold) for UUID: \(action.callUUID)"
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
guard activeCalls[action.callUUID] != nil else {
|
|
1354
|
+
NSLog("TelnyxVoice: Failing held action for unknown UUID: \(action.callUUID)")
|
|
1355
|
+
action.fail()
|
|
1356
|
+
return
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
guard pendingHeldActions[action.callUUID] == nil else {
|
|
1360
|
+
NSLog("TelnyxVoice: Failing duplicate held action for UUID: \(action.callUUID)")
|
|
1361
|
+
action.fail()
|
|
1362
|
+
return
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
pendingHeldActions[action.callUUID] = action
|
|
1366
|
+
let emitted = CallKitBridge.shared?.emitHeldCallEvent(
|
|
1367
|
+
callUUID: action.callUUID,
|
|
1368
|
+
isOnHold: action.isOnHold,
|
|
1369
|
+
callData: activeCalls[action.callUUID]
|
|
1370
|
+
) ?? false
|
|
1371
|
+
|
|
1372
|
+
guard emitted else {
|
|
1373
|
+
pendingHeldActions.removeValue(forKey: action.callUUID)
|
|
1374
|
+
action.fail()
|
|
1375
|
+
return
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self, weak action] in
|
|
1379
|
+
guard let self = self, let action = action,
|
|
1380
|
+
self.pendingHeldActions[action.callUUID] === action
|
|
1381
|
+
else { return }
|
|
1382
|
+
|
|
1383
|
+
NSLog("TelnyxVoice: Held action timed out for UUID: \(action.callUUID)")
|
|
1384
|
+
self.pendingHeldActions.removeValue(forKey: action.callUUID)
|
|
1385
|
+
action.fail()
|
|
1386
|
+
}
|
|
972
1387
|
}
|
|
973
1388
|
|
|
974
1389
|
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
|
975
1390
|
NSLog("TelnyxVoice: User ended call with UUID: \(action.callUUID)")
|
|
976
1391
|
|
|
1392
|
+
guard let callData = activeCalls[action.callUUID] else {
|
|
1393
|
+
NSLog("TelnyxVoice: Failing end action for unknown UUID: \(action.callUUID)")
|
|
1394
|
+
action.fail()
|
|
1395
|
+
return
|
|
1396
|
+
}
|
|
1397
|
+
if UserDefaults.standard.string(forKey: "pending_callkit_answer") == action.callUUID.uuidString {
|
|
1398
|
+
UserDefaults.standard.removeObject(forKey: "pending_callkit_answer")
|
|
1399
|
+
UserDefaults.standard.synchronize()
|
|
1400
|
+
}
|
|
1401
|
+
|
|
977
1402
|
// Notify React Native via CallKit bridge
|
|
978
1403
|
CallKitBridge.shared?.emitCallEvent(
|
|
979
1404
|
"CallKitDidPerformEndCallAction", callUUID: action.callUUID,
|
|
980
|
-
callData:
|
|
1405
|
+
callData: callData)
|
|
981
1406
|
|
|
982
1407
|
activeCalls.removeValue(forKey: action.callUUID)
|
|
1408
|
+
pendingAnswerActions.removeValue(forKey: action.callUUID)?.fail()
|
|
1409
|
+
pendingHeldActions.removeValue(forKey: action.callUUID)?.fail()
|
|
983
1410
|
NSLog("📞 TelnyxVoice: Fulfilling CXEndCallAction for call UUID: \(action.callUUID)")
|
|
984
1411
|
action.fulfill()
|
|
985
1412
|
NSLog("📞 TelnyxVoice: ✅ CXEndCallAction fulfilled successfully")
|
|
@@ -993,11 +1420,26 @@ import React
|
|
|
993
1420
|
"CallKitDidReceiveStartCallAction", callUUID: action.callUUID,
|
|
994
1421
|
callData: activeCalls[action.callUUID])
|
|
995
1422
|
|
|
1423
|
+
let callUpdate = CXCallUpdate()
|
|
1424
|
+
callUpdate.remoteHandle = action.handle
|
|
1425
|
+
callUpdate.hasVideo = false
|
|
1426
|
+
callUpdate.supportsHolding = true
|
|
1427
|
+
provider.reportCall(with: action.callUUID, updated: callUpdate)
|
|
1428
|
+
|
|
996
1429
|
NSLog("📞 TelnyxVoice: Fulfilling CXStartCallAction for call UUID: \(action.callUUID)")
|
|
997
1430
|
action.fulfill()
|
|
998
1431
|
NSLog("📞 TelnyxVoice: ✅ CXStartCallAction fulfilled successfully")
|
|
999
1432
|
}
|
|
1000
1433
|
|
|
1434
|
+
public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
|
1435
|
+
if let answerAction = action as? CXAnswerCallAction {
|
|
1436
|
+
pendingAnswerActions.removeValue(forKey: answerAction.callUUID)
|
|
1437
|
+
} else if let heldAction = action as? CXSetHeldCallAction {
|
|
1438
|
+
pendingHeldActions.removeValue(forKey: heldAction.callUUID)
|
|
1439
|
+
}
|
|
1440
|
+
action.fail()
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1001
1443
|
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
|
1002
1444
|
NSLog("TelnyxVoice: Audio session activated by CallKit")
|
|
1003
1445
|
|
|
@@ -1084,6 +1526,33 @@ import React
|
|
|
1084
1526
|
super.init()
|
|
1085
1527
|
}
|
|
1086
1528
|
|
|
1529
|
+
private func storePendingVoipPush(_ payload: [AnyHashable: Any]) {
|
|
1530
|
+
do {
|
|
1531
|
+
let jsonData = try JSONSerialization.data(withJSONObject: payload)
|
|
1532
|
+
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
|
|
1533
|
+
|
|
1534
|
+
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
1535
|
+
UserDefaults.standard.set(jsonString, forKey: "pending_push_metadata")
|
|
1536
|
+
|
|
1537
|
+
let voipPushData = [
|
|
1538
|
+
"payload": payload
|
|
1539
|
+
]
|
|
1540
|
+
let voipJsonData = try JSONSerialization.data(withJSONObject: voipPushData)
|
|
1541
|
+
let voipJsonString = String(data: voipJsonData, encoding: .utf8) ?? ""
|
|
1542
|
+
UserDefaults.standard.set(voipJsonString, forKey: "pending_voip_push")
|
|
1543
|
+
UserDefaults.standard.synchronize()
|
|
1544
|
+
NSLog(
|
|
1545
|
+
"[TelnyxVoipPushHandler] Stored VoIP push data after CallKit registration succeeded"
|
|
1546
|
+
)
|
|
1547
|
+
} catch {
|
|
1548
|
+
NSLog("[TelnyxVoipPushHandler] Error converting VoIP payload to JSON: \(error)")
|
|
1549
|
+
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
1550
|
+
UserDefaults.standard.set("{}", forKey: "pending_push_metadata")
|
|
1551
|
+
UserDefaults.standard.set("{\"payload\":{}}", forKey: "pending_voip_push")
|
|
1552
|
+
UserDefaults.standard.synchronize()
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1087
1556
|
/**
|
|
1088
1557
|
* Initialize VoIP push registration
|
|
1089
1558
|
* Call this from the main app's AppDelegate didFinishLaunchingWithOptions
|
|
@@ -1132,35 +1601,6 @@ import React
|
|
|
1132
1601
|
} catch {
|
|
1133
1602
|
NSLog("Failed to activate audio session: \(error)")
|
|
1134
1603
|
}
|
|
1135
|
-
// Store the VoIP push data for VoicePnBridge
|
|
1136
|
-
do {
|
|
1137
|
-
let jsonData = try JSONSerialization.data(withJSONObject: payload.dictionaryPayload)
|
|
1138
|
-
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
|
|
1139
|
-
|
|
1140
|
-
// Store for TelnyxVoiceApp (push action flow)
|
|
1141
|
-
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
1142
|
-
UserDefaults.standard.set(jsonString, forKey: "pending_push_metadata")
|
|
1143
|
-
|
|
1144
|
-
// ALSO store for VoicePnBridge.getPendingVoipPush() (CallKit coordinator flow)
|
|
1145
|
-
// This creates a structured object with payload property
|
|
1146
|
-
let voipPushData = [
|
|
1147
|
-
"payload": payload.dictionaryPayload
|
|
1148
|
-
]
|
|
1149
|
-
let voipJsonData = try JSONSerialization.data(withJSONObject: voipPushData)
|
|
1150
|
-
let voipJsonString = String(data: voipJsonData, encoding: .utf8) ?? ""
|
|
1151
|
-
UserDefaults.standard.set(voipJsonString, forKey: "pending_voip_push")
|
|
1152
|
-
|
|
1153
|
-
UserDefaults.standard.synchronize()
|
|
1154
|
-
NSLog(
|
|
1155
|
-
"[TelnyxVoipPushHandler] Stored VoIP push data for both TelnyxVoiceApp and VoicePnBridge"
|
|
1156
|
-
)
|
|
1157
|
-
} catch {
|
|
1158
|
-
NSLog("[TelnyxVoipPushHandler] Error converting VoIP payload to JSON: \(error)")
|
|
1159
|
-
UserDefaults.standard.set("incoming_call", forKey: "pending_push_action")
|
|
1160
|
-
UserDefaults.standard.set("{}", forKey: "pending_push_metadata")
|
|
1161
|
-
UserDefaults.standard.set("{\"payload\":{}}", forKey: "pending_voip_push")
|
|
1162
|
-
UserDefaults.standard.synchronize()
|
|
1163
|
-
}
|
|
1164
1604
|
|
|
1165
1605
|
// Extract caller information and call_id from the payload
|
|
1166
1606
|
var callerName = "Unknown Caller"
|
|
@@ -1181,12 +1621,20 @@ import React
|
|
|
1181
1621
|
callId = payload.dictionaryPayload["call_id"] as? String
|
|
1182
1622
|
}
|
|
1183
1623
|
|
|
1184
|
-
// Use call_id as CallKit UUID to ensure matching with WebRTC
|
|
1624
|
+
// Use call_id as CallKit UUID to ensure matching with WebRTC.
|
|
1185
1625
|
guard let callIdString = callId, let callUUID = UUID(uuidString: callIdString) else {
|
|
1186
1626
|
NSLog(
|
|
1187
|
-
"[TelnyxVoipPushHandler]
|
|
1627
|
+
"[TelnyxVoipPushHandler] ⚠️ Invalid or missing call_id in payload (\(callId ?? "nil")); reporting failed watchdog placeholder"
|
|
1628
|
+
)
|
|
1629
|
+
TelnyxCallKitManager.shared.reportAndEndWatchdogCall(
|
|
1630
|
+
callUUID: UUID(),
|
|
1631
|
+
callerName: callerName,
|
|
1632
|
+
callerNumber: callerNumber,
|
|
1633
|
+
payload: payload.dictionaryPayload,
|
|
1634
|
+
source: "malformed_push_watchdog",
|
|
1635
|
+
endedReason: .failed,
|
|
1636
|
+
completion: completion
|
|
1188
1637
|
)
|
|
1189
|
-
completion()
|
|
1190
1638
|
return
|
|
1191
1639
|
}
|
|
1192
1640
|
|
|
@@ -1202,16 +1650,7 @@ import React
|
|
|
1202
1650
|
|
|
1203
1651
|
// CRITICAL: Setup CallKit SYNCHRONOUSLY - no async dispatch allowed
|
|
1204
1652
|
// This must happen in the same run loop as the VoIP push
|
|
1205
|
-
callKitManager.setupSynchronously()
|
|
1206
|
-
|
|
1207
|
-
// Ensure we have a valid CallKit provider after setup
|
|
1208
|
-
guard let callKitProvider = callKitManager.callKitProvider else {
|
|
1209
|
-
NSLog(
|
|
1210
|
-
"[TelnyxVoipPushHandler] ❌ FATAL: CallKit provider not available after synchronous setup!"
|
|
1211
|
-
)
|
|
1212
|
-
completion()
|
|
1213
|
-
return
|
|
1214
|
-
}
|
|
1653
|
+
let callKitProvider = callKitManager.setupSynchronously()
|
|
1215
1654
|
|
|
1216
1655
|
NSLog("[TelnyxVoipPushHandler] ✅ CallKit provider ready, reporting incoming call")
|
|
1217
1656
|
|
|
@@ -1225,6 +1664,7 @@ import React
|
|
|
1225
1664
|
"uuid": callUUID.uuidString,
|
|
1226
1665
|
"direction": "incoming",
|
|
1227
1666
|
"source": isAppRunning ? "push_notification" : "terminated_app_push",
|
|
1667
|
+
"isCallKitRegistered": false,
|
|
1228
1668
|
]
|
|
1229
1669
|
|
|
1230
1670
|
// Report to CallKit immediately
|
|
@@ -1233,18 +1673,27 @@ import React
|
|
|
1233
1673
|
callUpdate.remoteHandle = handle
|
|
1234
1674
|
callUpdate.hasVideo = false
|
|
1235
1675
|
callUpdate.localizedCallerName = callerName
|
|
1676
|
+
callUpdate.supportsHolding = true
|
|
1236
1677
|
|
|
1237
1678
|
callKitProvider.reportNewIncomingCall(with: callUUID, update: callUpdate) { error in
|
|
1238
1679
|
if let error = error {
|
|
1680
|
+
let nsError = error as NSError
|
|
1681
|
+
let reason =
|
|
1682
|
+
nsError.domain == CXErrorDomainIncomingCall && nsError.code == 3
|
|
1683
|
+
? "filteredByDoNotDisturb" : "registrationRejected"
|
|
1239
1684
|
NSLog(
|
|
1240
|
-
"[TelnyxVoipPushHandler]
|
|
1685
|
+
"[TelnyxVoipPushHandler] CallKit incoming registration rejected: domain=\(nsError.domain), code=\(nsError.code), reason=\(reason)"
|
|
1241
1686
|
)
|
|
1242
1687
|
callKitManager.activeCalls.removeValue(forKey: callUUID)
|
|
1688
|
+
callKitManager.clearPendingPushData(for: callUUID.uuidString)
|
|
1243
1689
|
} else {
|
|
1244
1690
|
NSLog(
|
|
1245
1691
|
"[TelnyxVoipPushHandler] ✅ CallKit call reported successfully via unified path"
|
|
1246
1692
|
)
|
|
1247
1693
|
|
|
1694
|
+
callKitManager.markCallKitRegistered(callUUID)
|
|
1695
|
+
self.storePendingVoipPush(payload.dictionaryPayload)
|
|
1696
|
+
|
|
1248
1697
|
// CRITICAL: Store the CallKit UUID for the React Native CallKitCoordinator
|
|
1249
1698
|
// This allows the coordinator to find the existing CallKit call instead of creating a duplicate
|
|
1250
1699
|
UserDefaults.standard.set(callUUID.uuidString, forKey: "@pending_callkit_uuid")
|