@stream-io/react-native-callingx 0.1.0-beta.7 → 0.1.1-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 (45) hide show
  1. package/android/build.gradle +7 -1
  2. package/android/src/main/AndroidManifest.xml +31 -1
  3. package/android/src/main/java/io/getstream/rn/callingx/CallEventBroadcastReceiver.kt +17 -0
  4. package/android/src/main/java/io/getstream/rn/callingx/CallRegistrationStore.kt +176 -0
  5. package/android/src/main/java/io/getstream/rn/callingx/CallService.kt +302 -80
  6. package/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt +176 -191
  7. package/android/src/main/java/io/getstream/rn/callingx/StreamMessagingService.kt +48 -0
  8. package/android/src/main/java/io/getstream/rn/callingx/model/Call.kt +1 -0
  9. package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt +196 -46
  10. package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt +14 -8
  11. package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt +12 -1
  12. package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt +7 -0
  13. package/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt +38 -19
  14. package/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt +64 -55
  15. package/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt +241 -195
  16. package/android/src/main/java/io/getstream/rn/callingx/utils/CallEventBus.kt +61 -0
  17. package/android/src/main/java/io/getstream/rn/callingx/utils/SettingsStore.kt +51 -0
  18. package/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt +12 -3
  19. package/android/src/oldarch/java/io/getstream/rn/callingx/CallingxModule.kt +13 -3
  20. package/dist/module/CallingxModule.js +13 -10
  21. package/dist/module/CallingxModule.js.map +1 -1
  22. package/dist/module/spec/NativeCallingx.js.map +1 -1
  23. package/dist/module/utils/constants.js +24 -13
  24. package/dist/module/utils/constants.js.map +1 -1
  25. package/dist/typescript/src/CallingxModule.d.ts +3 -0
  26. package/dist/typescript/src/CallingxModule.d.ts.map +1 -1
  27. package/dist/typescript/src/spec/NativeCallingx.d.ts +7 -1
  28. package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
  29. package/dist/typescript/src/types.d.ts +31 -0
  30. package/dist/typescript/src/types.d.ts.map +1 -1
  31. package/dist/typescript/src/utils/constants.d.ts +1 -1
  32. package/dist/typescript/src/utils/constants.d.ts.map +1 -1
  33. package/ios/AudioSessionManager.swift +2 -2
  34. package/ios/Callingx.mm +41 -17
  35. package/ios/CallingxImpl.swift +213 -83
  36. package/ios/Settings.swift +2 -2
  37. package/ios/UUIDStorage.swift +10 -10
  38. package/ios/VoipNotificationsManager.swift +8 -8
  39. package/package.json +4 -2
  40. package/src/CallingxModule.ts +14 -10
  41. package/src/spec/NativeCallingx.ts +10 -3
  42. package/src/types.ts +34 -0
  43. package/src/utils/constants.ts +23 -9
  44. /package/android/src/main/java/io/getstream/rn/callingx/{ResourceUtils.kt → utils/ResourceUtils.kt} +0 -0
  45. /package/android/src/main/java/io/getstream/rn/callingx/{Utils.kt → utils/Utils.kt} +0 -0
@@ -30,7 +30,7 @@ import stream_react_native_webrtc
30
30
  // MARK: - Shared State
31
31
  @objc public static var sharedProvider: CXProvider?
32
32
  @objc public static var uuidStorage: UUIDStorage?
33
- @objc public static weak var sharedInstance: CallingxImpl?
33
+ @objc public static var sharedInstance: CallingxImpl?
34
34
  /// Events stored before the module instance exists (e.g. VoIP from killed state). Drained in getInitialEvents().
35
35
  private static var delayedEvents: [[String: Any]] = []
36
36
 
@@ -42,6 +42,21 @@ import stream_react_native_webrtc
42
42
 
43
43
  private var canSendEvents: Bool = false
44
44
  private var isSetup: Bool = false
45
+
46
+ // Pending CXActions awaiting JS fulfillment
47
+ private var pendingAnswerActions: [String: (action: CXAnswerCallAction, enqueuedAt: DispatchTime)] = [:]
48
+ private var pendingEndActions: [String: (action: CXEndCallAction, enqueuedAt: DispatchTime)] = [:]
49
+ private let pendingActionsQueue = DispatchQueue(label: "io.getstream.callingx.pendingActions")
50
+ // a large timeout to accomodate for cold start + metro server load time
51
+ private let pendingActionTimeoutSeconds = 30
52
+
53
+ @objc public static func getSharedInstance() -> CallingxImpl {
54
+ if sharedInstance == nil {
55
+ sharedInstance = CallingxImpl()
56
+ }
57
+
58
+ return sharedInstance!
59
+ }
45
60
 
46
61
  // MARK: - Initialization
47
62
  @objc public override init() {
@@ -58,7 +73,14 @@ import stream_react_native_webrtc
58
73
  )
59
74
 
60
75
  CallingxImpl.sharedInstance = self
61
- CallingxImpl.initializeIfNeeded()
76
+
77
+ if CallingxImpl.uuidStorage == nil {
78
+ CallingxImpl.uuidStorage = UUIDStorage()
79
+ }
80
+
81
+ if CallingxImpl.sharedProvider == nil {
82
+ CallingxImpl.sharedProvider = CXProvider(configuration: Settings.getProviderConfiguration())
83
+ }
62
84
 
63
85
  callKeepProvider = CallingxImpl.sharedProvider
64
86
  callKeepProvider?.setDelegate(nil, queue: nil)
@@ -77,13 +99,7 @@ import stream_react_native_webrtc
77
99
 
78
100
  // MARK: - Class Methods
79
101
  @objc public static func initializeIfNeeded() {
80
- if uuidStorage == nil {
81
- uuidStorage = UUIDStorage()
82
- }
83
-
84
- if sharedProvider == nil {
85
- sharedProvider = CXProvider(configuration: Settings.getProviderConfiguration())
86
- }
102
+ _ = getSharedInstance() // ensures the shared instance is created and CXProvider delegate is set
87
103
  }
88
104
 
89
105
  @objc public static func reportNewIncomingCall(
@@ -107,7 +123,7 @@ import stream_react_native_webrtc
107
123
 
108
124
  if storage.containsCid(callId) {
109
125
  #if DEBUG
110
- print("[Callingx][reportNewIncomingCall] callId already exists")
126
+ NSLog("%@","[Callingx][reportNewIncomingCall] callId already exists")
111
127
  #endif
112
128
  completion?()
113
129
  resolve?(true)
@@ -127,7 +143,7 @@ import stream_react_native_webrtc
127
143
 
128
144
  sharedProvider?.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
129
145
  #if DEBUG
130
- print("[Callingx][reportNewIncomingCall] callId = \(callId), error = \(String(describing: error))")
146
+ NSLog("%@","[Callingx][reportNewIncomingCall] callId = \(callId), error = \(String(describing: error))")
131
147
  #endif
132
148
 
133
149
  let errorCode = error != nil ? CallingxImpl.getIncomingCallErrorCode(error!) : ""
@@ -146,21 +162,13 @@ import stream_react_native_webrtc
146
162
  "payload": payload ?? ""
147
163
  ]
148
164
 
149
- if let instance = sharedInstance {
150
- instance.sendEvent(CallingxEvents.didDisplayIncomingCall, body: body)
151
- } else {
152
- let dictionary: [String: Any] = [
153
- "eventName": CallingxEvents.didDisplayIncomingCall,
154
- "params": body
155
- ]
156
- DispatchQueue.main.async {
157
- CallingxImpl.delayedEvents.append(dictionary)
158
- }
165
+ if let instance = CallingxImpl.sharedInstance {
166
+ instance.sendEvent(CallingxEvents.didDisplayIncomingCall, body: body)
159
167
  }
160
-
168
+
161
169
  if error == nil {
162
170
  #if DEBUG
163
- print("[Callingx][reportNewIncomingCall] success callId = \(callId)")
171
+ NSLog("%@","[Callingx][reportNewIncomingCall] success callId = \(callId)")
164
172
  #endif
165
173
  resolve?(true)
166
174
  } else {
@@ -204,12 +212,12 @@ import stream_react_native_webrtc
204
212
 
205
213
  @objc public static func endCall(_ callId: String, reason: Int) {
206
214
  #if DEBUG
207
- print("[Callingx][endCall] callId = \(callId) reason = \(reason)")
215
+ NSLog("%@","[Callingx][endCall] callId = \(callId) reason = \(reason)")
208
216
  #endif
209
217
 
210
218
  guard let call = uuidStorage?.getCall(forCid: callId) else {
211
219
  #if DEBUG
212
- print("[Callingx][endCall] callId not found")
220
+ NSLog("%@","[Callingx][endCall] callId not found")
213
221
  #endif
214
222
  return
215
223
  }
@@ -242,7 +250,7 @@ import stream_react_native_webrtc
242
250
  // MARK: - Instance Methods
243
251
  @objc public func requestTransaction(_ transaction: CXTransaction) {
244
252
  #if DEBUG
245
- print("[Callingx][requestTransaction] transaction = \(transaction)")
253
+ NSLog("%@","[Callingx][requestTransaction] transaction = \(transaction)")
246
254
  #endif
247
255
 
248
256
  if callKeepCallController == nil {
@@ -252,7 +260,7 @@ import stream_react_native_webrtc
252
260
  callKeepCallController?.request(transaction) { [weak self] error in
253
261
  if let error = error {
254
262
  #if DEBUG
255
- print("[Callingx][requestTransaction] Error requesting transaction (\(transaction.actions)): (\(error))")
263
+ NSLog("%@","[Callingx][requestTransaction] Error requesting transaction (\(transaction.actions)): (\(error))")
256
264
  #endif
257
265
 
258
266
  // Reset per-call action-source flags for all actions in the failed transaction
@@ -264,7 +272,7 @@ import stream_react_native_webrtc
264
272
  }
265
273
  } else {
266
274
  #if DEBUG
267
- print("[Callingx][requestTransaction] Requested transaction successfully")
275
+ NSLog("%@","[Callingx][requestTransaction] Requested transaction successfully")
268
276
  #endif
269
277
 
270
278
  if let startCallAction = transaction.actions.first as? CXStartCallAction {
@@ -285,7 +293,7 @@ import stream_react_native_webrtc
285
293
 
286
294
  @objc public func sendEvent(_ name: String, body: [String: Any]?) {
287
295
  #if DEBUG
288
- print("[Callingx] sendEventWithNameWrapper: \(name)")
296
+ NSLog("%@","[Callingx] sendEventWithNameWrapper: \(name)")
289
297
  #endif
290
298
 
291
299
  let sendEventAction = {
@@ -299,7 +307,7 @@ import stream_react_native_webrtc
299
307
  } else {
300
308
  CallingxImpl.delayedEvents.append(dictionary)
301
309
  #if DEBUG
302
- print("[Callingx] delayedEvents: \(CallingxImpl.delayedEvents)")
310
+ NSLog("%@","[Callingx] delayedEvents: \(CallingxImpl.delayedEvents)")
303
311
  #endif
304
312
  }
305
313
  }
@@ -334,11 +342,21 @@ import stream_react_native_webrtc
334
342
 
335
343
  Settings.setSettings(options)
336
344
 
337
- CallingxImpl.initializeIfNeeded()
338
-
339
- callKeepProvider = CallingxImpl.sharedProvider
340
- callKeepProvider?.setDelegate(self, queue: nil)
341
-
345
+ // This is mostly needed for very first setup, as we need to override the default
346
+ // provider configuration which is set in the constructor.
347
+ // IMPORTANT: We override CXProvider instance only if there is no registered call, otherwise we may lose corrsponding call state/events from CallKit
348
+ if !CallingxImpl.hasRegisteredCall() {
349
+ let oldProvider = CallingxImpl.sharedProvider
350
+ let newProvider = CXProvider(configuration: Settings.getProviderConfiguration())
351
+ newProvider.setDelegate(self, queue: nil)
352
+
353
+ CallingxImpl.sharedProvider = newProvider
354
+ callKeepProvider = newProvider
355
+
356
+ oldProvider?.setDelegate(nil, queue: nil)
357
+ oldProvider?.invalidate()
358
+ }
359
+
342
360
  isSetup = true
343
361
  }
344
362
 
@@ -346,7 +364,7 @@ import stream_react_native_webrtc
346
364
  var events: [[String: Any]] = []
347
365
  let action = {
348
366
  #if DEBUG
349
- print("[Callingx][getInitialEvents] delayedEvents = \(CallingxImpl.delayedEvents)")
367
+ NSLog("%@","[Callingx][getInitialEvents] delayedEvents = \(CallingxImpl.delayedEvents)")
350
368
  #endif
351
369
 
352
370
  events = CallingxImpl.delayedEvents
@@ -368,12 +386,12 @@ import stream_react_native_webrtc
368
386
  // MARK: - Call Management
369
387
  @objc public func answerIncomingCall(_ callId: String) -> Bool {
370
388
  #if DEBUG
371
- print("[Callingx][answerIncomingCall] callId = \(callId)")
389
+ NSLog("%@","[Callingx][answerIncomingCall] callId = \(callId)")
372
390
  #endif
373
-
391
+
374
392
  guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
375
393
  #if DEBUG
376
- print("[Callingx][answerIncomingCall] callId not found")
394
+ NSLog("%@","[Callingx][answerIncomingCall] callId not found")
377
395
  #endif
378
396
  return false
379
397
  }
@@ -381,7 +399,7 @@ import stream_react_native_webrtc
381
399
  // Guard: already answered or ended — prevent duplicate CXAnswerCallAction transactions
382
400
  if call.isAnswered || call.hasEnded {
383
401
  #if DEBUG
384
- print("[Callingx][answerIncomingCall] callId already answered/ended, skipping")
402
+ NSLog("%@","[Callingx][answerIncomingCall] callId already answered/ended, skipping")
385
403
  #endif
386
404
  return true
387
405
  }
@@ -430,7 +448,7 @@ import stream_react_native_webrtc
430
448
  DispatchQueue.main.asyncAfter(deadline: popTime) { [weak self] in
431
449
  guard let self = self, !self.isSetup else { return }
432
450
  #if DEBUG
433
- print("[Callingx] Displayed a call without a reachable app, ending the call: \(callId)")
451
+ NSLog("%@","[Callingx] Displayed a call without a reachable app, ending the call: \(callId)")
434
452
  #endif
435
453
  CallingxImpl.endCall(callId, reason: CXCallEndedReason.failed.rawValue)
436
454
  }
@@ -440,12 +458,12 @@ import stream_react_native_webrtc
440
458
 
441
459
  @objc public func endCall(_ callId: String) -> Bool {
442
460
  #if DEBUG
443
- print("[Callingx][endCall] callId = \(callId)")
461
+ NSLog("%@","[Callingx][endCall] callId = \(callId)")
444
462
  #endif
445
463
 
446
464
  guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
447
465
  #if DEBUG
448
- print("[Callingx][endCall] callId not found")
466
+ NSLog("%@","[Callingx][endCall] callId not found")
449
467
  #endif
450
468
  return false
451
469
  }
@@ -453,7 +471,7 @@ import stream_react_native_webrtc
453
471
  // Guard: already ended — prevent duplicate CXEndCallAction transactions
454
472
  if call.hasEnded {
455
473
  #if DEBUG
456
- print("[Callingx][endCall] callId already ended, skipping")
474
+ NSLog("%@","[Callingx][endCall] callId already ended, skipping")
457
475
  #endif
458
476
  return true
459
477
  }
@@ -471,7 +489,7 @@ import stream_react_native_webrtc
471
489
  @objc public func isCallTracked(_ callId: String) -> Bool {
472
490
  guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
473
491
  #if DEBUG
474
- print("[Callingx][isCallTracked] callId not found")
492
+ NSLog("%@","[Callingx][isCallTracked] callId not found")
475
493
  #endif
476
494
  return false
477
495
  }
@@ -487,12 +505,12 @@ import stream_react_native_webrtc
487
505
 
488
506
  @objc public func setCurrentCallActive(_ callId: String) -> Bool {
489
507
  #if DEBUG
490
- print("[Callingx][setCurrentCallActive] callId = \(callId)")
508
+ NSLog("%@","[Callingx][setCurrentCallActive] callId = \(callId)")
491
509
  #endif
492
510
 
493
511
  guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
494
512
  #if DEBUG
495
- print("[Callingx][setCurrentCallActive] callId not found")
513
+ NSLog("%@","[Callingx][setCurrentCallActive] callId not found")
496
514
  #endif
497
515
  return false
498
516
  }
@@ -507,12 +525,12 @@ import stream_react_native_webrtc
507
525
 
508
526
  @objc public func setMutedCall(_ callId: String, isMuted: Bool) -> Bool {
509
527
  #if DEBUG
510
- print("[Callingx][setMutedCall] muted = \(isMuted)")
528
+ NSLog("%@","[Callingx][setMutedCall] muted = \(isMuted)")
511
529
  #endif
512
530
 
513
531
  guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
514
532
  #if DEBUG
515
- print("[Callingx][setMutedCall] callId not found")
533
+ NSLog("%@","[Callingx][setMutedCall] callId not found")
516
534
  #endif
517
535
  return false
518
536
  }
@@ -528,12 +546,12 @@ import stream_react_native_webrtc
528
546
 
529
547
  @objc public func setOnHoldCall(_ callId: String, isOnHold: Bool) -> Bool {
530
548
  #if DEBUG
531
- print("[Callingx][setOnHold] uuidString = \(callId), shouldHold = \(isOnHold)")
549
+ NSLog("%@","[Callingx][setOnHold] uuidString = \(callId), shouldHold = \(isOnHold)")
532
550
  #endif
533
551
 
534
552
  guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
535
553
  #if DEBUG
536
- print("[Callingx][setOnHoldCall] callId not found")
554
+ NSLog("%@","[Callingx][setOnHoldCall] callId not found")
537
555
  #endif
538
556
  return false
539
557
  }
@@ -553,14 +571,14 @@ import stream_react_native_webrtc
553
571
  hasVideo: Bool
554
572
  ) {
555
573
  #if DEBUG
556
- print("[Callingx][startCall] uuidString = \(callId), phoneNumber = \(phoneNumber)")
574
+ NSLog("%@","[Callingx][startCall] uuidString = \(callId), phoneNumber = \(phoneNumber)")
557
575
  #endif
558
576
 
559
577
  guard let storage = CallingxImpl.uuidStorage else { return }
560
578
 
561
579
  if (storage.containsCid(callId)) {
562
580
  #if DEBUG
563
- print("[Callingx][startCall] Call \(callId) is already registered")
581
+ NSLog("%@","[Callingx][startCall] Call \(callId) is already registered")
564
582
  #endif
565
583
  return
566
584
  }
@@ -584,12 +602,12 @@ import stream_react_native_webrtc
584
602
  callerName: String
585
603
  ) -> Bool {
586
604
  #if DEBUG
587
- print("[Callingx][updateDisplay] uuidString = \(callId) displayName = \(callerName) uri = \(phoneNumber)")
605
+ NSLog("%@","[Callingx][updateDisplay] uuidString = \(callId) displayName = \(callerName) uri = \(phoneNumber)")
588
606
  #endif
589
607
 
590
608
  guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
591
609
  #if DEBUG
592
- print("[Callingx][updateDisplay] callId not found")
610
+ NSLog("%@","[Callingx][updateDisplay] callId not found")
593
611
  #endif
594
612
  return false
595
613
  }
@@ -608,12 +626,12 @@ import stream_react_native_webrtc
608
626
  // MARK: - CXProviderDelegate
609
627
  public func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
610
628
  #if DEBUG
611
- print("[Callingx][CXProviderDelegate][provider:performStartCallAction]")
629
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performStartCallAction]")
612
630
  #endif
613
631
 
614
632
  guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
615
633
  #if DEBUG
616
- print("[Callingx][CXProviderDelegate][provider:performStartCallAction] callId not found")
634
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performStartCallAction] callId not found")
617
635
  #endif
618
636
  action.fail()
619
637
  return
@@ -637,14 +655,14 @@ import stream_react_native_webrtc
637
655
  public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
638
656
  guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
639
657
  #if DEBUG
640
- print("[Callingx][CXProviderDelegate][provider:performAnswerCallAction] callId not found")
658
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] callId not found")
641
659
  #endif
642
660
  action.fail()
643
661
  return
644
662
  }
645
663
 
646
664
  #if DEBUG
647
- print("[Callingx][CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: \(call.isSelfAnswered)")
665
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: \(call.isSelfAnswered)")
648
666
  #endif
649
667
 
650
668
  getAudioDeviceModule()?.reset()
@@ -658,46 +676,85 @@ import stream_react_native_webrtc
658
676
 
659
677
  call.resetSelfAnswered()
660
678
  call.markConnected() // incoming: call is now connected
661
- // TODO: Use action.fulfill(withDateConnected: call.connectedAt ?? Date()) instead of bare
662
- // action.fulfill() to give CallKit more accurate call duration tracking in the system call log.
663
- // to be done with pending action fulfillment
664
- action.fulfill()
679
+
680
+ if source == "app" {
681
+ // App initiated this answer — no need to wait for JS, fulfill immediately
682
+ action.fulfill()
683
+ } else {
684
+ // System initiated — defer fulfillment until JS reports back via fulfillAnswerCallAction
685
+ let cid = call.cid
686
+ pendingActionsQueue.sync {
687
+ self.pendingAnswerActions[cid] = (action: action, enqueuedAt: DispatchTime.now())
688
+ }
689
+ // Safety timer: auto-fail if JS never responds.
690
+ // Answer timeout = call never connected
691
+ let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(pendingActionTimeoutSeconds)
692
+ pendingActionsQueue.asyncAfter(deadline: timeout) { [weak self] in
693
+ if let pending = self?.pendingAnswerActions.removeValue(forKey: cid) {
694
+ #if DEBUG
695
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] answer timeout for callId: \(cid)")
696
+ #endif
697
+ pending.action.fail()
698
+ }
699
+ }
700
+ }
665
701
  }
666
-
702
+
667
703
  public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
668
704
  guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
669
705
  #if DEBUG
670
- print("[Callingx][CXProviderDelegate][provider:performEndCallAction] callId not found")
706
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] callId not found")
671
707
  #endif
672
- action.fail()
708
+ // End actions represent explicit user intent to close call UI.
709
+ // Fulfill stale/duplicate end actions to avoid "Call Failed" UX.
710
+ action.fulfill()
673
711
  return
674
712
  }
675
-
713
+
676
714
  #if DEBUG
677
- print("[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(call.isSelfEnded)")
715
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(call.isSelfEnded)")
678
716
  #endif
679
-
717
+
680
718
  let source = call.isSelfEnded ? "app" : "sys"
681
719
  sendEvent(CallingxEvents.performEndCallAction, body: [
682
720
  "callId": call.cid,
683
721
  "source": source
684
722
  ])
685
-
723
+
686
724
  call.resetSelfEnded()
687
725
  call.markEnded()
688
726
  CallingxImpl.uuidStorage?.removeCid(call.cid)
689
-
690
- action.fulfill()
727
+
728
+ if source == "app" {
729
+ // App initiated this end — no need to wait for JS, fulfill immediately
730
+ action.fulfill()
731
+ } else {
732
+ // System initiated — defer fulfillment until JS reports back via fulfillEndCallAction
733
+ let cid = call.cid
734
+ pendingActionsQueue.sync {
735
+ self.pendingEndActions[cid] = (action: action, enqueuedAt: DispatchTime.now())
736
+ }
737
+ // Safety timer: auto-fulfill if JS never responds.
738
+ let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(pendingActionTimeoutSeconds)
739
+ pendingActionsQueue.asyncAfter(deadline: timeout) { [weak self] in
740
+ if let pending = self?.pendingEndActions.removeValue(forKey: cid) {
741
+ #if DEBUG
742
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] end timeout for callId: \(cid)")
743
+ #endif
744
+ pending.action.fulfill()
745
+ }
746
+ }
747
+ }
691
748
  }
692
749
 
693
750
  public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
694
751
  #if DEBUG
695
- print("[Callingx][CXProviderDelegate][provider:performSetHeldCallAction]")
752
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetHeldCallAction]")
696
753
  #endif
697
754
 
698
755
  guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
699
756
  #if DEBUG
700
- print("[Callingx][CXProviderDelegate][provider:performSetHeldCallAction] callId not found")
757
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetHeldCallAction] callId not found")
701
758
  #endif
702
759
  action.fail()
703
760
  return
@@ -714,7 +771,7 @@ import stream_react_native_webrtc
714
771
  public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
715
772
  guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
716
773
  #if DEBUG
717
- print("[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] callId not found")
774
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] callId not found")
718
775
  #endif
719
776
  action.fail()
720
777
  return
@@ -724,7 +781,7 @@ import stream_react_native_webrtc
724
781
  call.resetSelfMuted()
725
782
 
726
783
  #if DEBUG
727
- print("[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] \(action.isMuted) isAppInitiated: \(isAppInitiated)")
784
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] \(action.isMuted) isAppInitiated: \(isAppInitiated)")
728
785
  #endif
729
786
 
730
787
  // Only send the event to JS when the mute was initiated by the system
@@ -743,12 +800,12 @@ import stream_react_native_webrtc
743
800
 
744
801
  public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
745
802
  #if DEBUG
746
- print("[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction]")
803
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction]")
747
804
  #endif
748
805
 
749
806
  guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
750
807
  #if DEBUG
751
- print("[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction] callId not found")
808
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction] callId not found")
752
809
  #endif
753
810
  action.fail()
754
811
  return
@@ -764,7 +821,7 @@ import stream_react_native_webrtc
764
821
 
765
822
  public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
766
823
  #if DEBUG
767
- print("[Callingx][CXProviderDelegate][provider:didActivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
824
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:didActivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
768
825
  #endif
769
826
 
770
827
  // When CallKit activates the AVAudioSession, inform WebRTC as well.
@@ -780,7 +837,7 @@ import stream_react_native_webrtc
780
837
 
781
838
  public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
782
839
  #if DEBUG
783
- print("[Callingx][CXProviderDelegate][provider:didDeactivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
840
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:didDeactivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
784
841
  #endif
785
842
 
786
843
  // When CallKit deactivates the AVAudioSession, inform WebRTC as well.
@@ -796,24 +853,97 @@ import stream_react_native_webrtc
796
853
  }
797
854
 
798
855
  public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
856
+ // note: in practice we should never be getting this callback as we already have a pending timeout set.
857
+ // in our tests callkit timesout and exectutes this method in approximately 60 seconds.
799
858
  #if DEBUG
800
- print("[Callingx][CXProviderDelegate][provider:timedOutPerformingAction]")
859
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction]")
801
860
  #endif
861
+
862
+ guard let callAction = action as? CXCallAction else {
863
+ return
864
+ }
865
+
866
+ pendingActionsQueue.sync {
867
+ // cid mapping as soon as end is initiated, so cleanup by matching callUUID.
868
+ if let answerEntry = pendingAnswerActions.first(where: { $0.value.action.callUUID == callAction.callUUID }) {
869
+ pendingAnswerActions.removeValue(forKey: answerEntry.key)
870
+ let elapsedMs = elapsedMilliseconds(since: answerEntry.value.enqueuedAt)
871
+ #if DEBUG
872
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction] removed pending answer action for callId: \(answerEntry.key), elapsedMs=\(elapsedMs)")
873
+ #endif
874
+ }
875
+
876
+ if let endEntry = pendingEndActions.first(where: { $0.value.action.callUUID == callAction.callUUID }) {
877
+ pendingEndActions.removeValue(forKey: endEntry.key)
878
+ let elapsedMs = elapsedMilliseconds(since: endEntry.value.enqueuedAt)
879
+ #if DEBUG
880
+ NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction] removed pending end action for callId: \(endEntry.key), elapsedMs=\(elapsedMs)")
881
+ #endif
882
+ }
883
+ }
802
884
  }
803
885
 
804
886
  public func providerDidReset(_ provider: CXProvider) {
805
887
  #if DEBUG
806
- print("[Callingx][providerDidReset]")
888
+ NSLog("%@","[Callingx][providerDidReset]")
807
889
  #endif
808
890
 
891
+ // Clear any pending actions to prevent memory leaks.
892
+ // After a provider reset, all pending CXActions are invalid.
893
+ pendingActionsQueue.sync {
894
+ pendingAnswerActions.removeAll()
895
+ pendingEndActions.removeAll()
896
+ }
897
+
809
898
  sendEvent(CallingxEvents.providerReset, body: nil)
810
899
  }
811
900
 
901
+ // MARK: - Pending Action Fulfillment
902
+
903
+ @objc public func fulfillAnswerCallAction(_ callId: String, didFail: Bool) {
904
+ pendingActionsQueue.sync { [weak self] in
905
+ guard let pending = self?.pendingAnswerActions.removeValue(forKey: callId) else {
906
+ #if DEBUG
907
+ NSLog("%@","[Callingx][fulfillAnswerCallAction] action not found for callId: \(callId)")
908
+ #endif
909
+ return
910
+ }
911
+ let elapsedMs = elapsedMilliseconds(since: pending.enqueuedAt)
912
+ #if DEBUG
913
+ NSLog("%@","[Callingx][fulfillAnswerCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)")
914
+ #endif
915
+ if didFail { pending.action.fail() } else { pending.action.fulfill() }
916
+ }
917
+ }
918
+
919
+ @objc public func fulfillEndCallAction(_ callId: String, didFail: Bool) {
920
+ pendingActionsQueue.sync { [weak self] in
921
+ guard let pending = self?.pendingEndActions.removeValue(forKey: callId) else {
922
+ #if DEBUG
923
+ NSLog("%@","[Callingx][fulfillEndCallAction] action not found for callId: \(callId)")
924
+ #endif
925
+ return
926
+ }
927
+ let elapsedMs = elapsedMilliseconds(since: pending.enqueuedAt)
928
+ #if DEBUG
929
+ NSLog("%@","[Callingx][fulfillEndCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)")
930
+ #endif
931
+ if didFail { pending.action.fail() } else { pending.action.fulfill() }
932
+ }
933
+ }
934
+
812
935
  // MARK: - Helper Methods
936
+ private func elapsedMilliseconds(since start: DispatchTime) -> Int {
937
+ let nowNs = DispatchTime.now().uptimeNanoseconds
938
+ let startNs = start.uptimeNanoseconds
939
+ guard nowNs >= startNs else { return 0 }
940
+ return Int((nowNs - startNs) / 1_000_000)
941
+ }
942
+
813
943
  private func getAudioDeviceModule() -> AudioDeviceModule? {
814
944
  guard let adm = webRTCModule?.audioDeviceModule else {
815
945
  #if DEBUG
816
- print("[Callingx] WebRTCModule is not available. Ensure it was injected from the TurboModule host.")
946
+ NSLog("%@","[Callingx] WebRTCModule is not available. Ensure it was injected from the TurboModule host.")
817
947
  #endif
818
948
  return nil
819
949
  }
@@ -11,7 +11,7 @@ import UIKit
11
11
 
12
12
  public static func setSettings(_ options: [String: Any]?) {
13
13
  #if DEBUG
14
- print("[Settings][setSettings] options = \(String(describing: options))")
14
+ NSLog("%@","[Settings][setSettings] options = \(String(describing: options))")
15
15
  #endif
16
16
 
17
17
  var settings: [String: Any] = getSettings()
@@ -39,7 +39,7 @@ import UIKit
39
39
 
40
40
  public static func getProviderConfiguration() -> CXProviderConfiguration {
41
41
  #if DEBUG
42
- print("[Settings][getProviderConfiguration]")
42
+ NSLog("%@","[Settings][getProviderConfiguration]")
43
43
  #endif
44
44
 
45
45
  let settings = getSettings()