@stream-io/react-native-callingx 0.1.0-beta.4 → 0.1.0-beta.5

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/ios/Callingx.mm CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  #import <AVFoundation/AVAudioSession.h>
5
5
  #import <CallKit/CallKit.h>
6
+ #import "WebRTCModule.h"
6
7
 
7
8
  // Import Swift generated header
8
9
  #if __has_include("Callingx-Swift.h")
@@ -54,7 +55,6 @@
54
55
  supportsDTMF:(BOOL)supportsDTMF
55
56
  supportsGrouping:(BOOL)supportsGrouping
56
57
  supportsUngrouping:(BOOL)supportsUngrouping
57
- fromPushKit:(BOOL)fromPushKit
58
58
  payload:(NSDictionary *_Nullable)payload
59
59
  withCompletionHandler:(void (^_Nullable)(void))completion {
60
60
 
@@ -67,9 +67,11 @@
67
67
  supportsDTMF:supportsDTMF
68
68
  supportsGrouping:supportsGrouping
69
69
  supportsUngrouping:supportsUngrouping
70
- fromPushKit:fromPushKit
71
70
  payload:payload
72
- completion:completion];
71
+ completion:completion
72
+ resolve:nil
73
+ reject:nil
74
+ ];
73
75
  }
74
76
 
75
77
  + (BOOL)canRegisterCall {
@@ -127,6 +129,12 @@
127
129
 
128
130
  [_moduleImpl setupWithOptions:optionsDict];
129
131
 
132
+ // Inject WebRTCModule so CallingxImpl can access AudioDeviceModule.
133
+ // self.bridge is NOT available on TurboModules — use currentBridge instead,
134
+ // which returns the real RCTBridge or RCTBridgeProxy (bridgeless interop).
135
+ WebRTCModule *webrtcModule = [[RCTBridge currentBridge] moduleForName:@"WebRTCModule"];
136
+ _moduleImpl.webRTCModule = webrtcModule;
137
+
130
138
  self.callKeepCallController = _moduleImpl.callKeepCallController;
131
139
  self.callKeepProvider = _moduleImpl.callKeepProvider;
132
140
  }
@@ -161,11 +169,13 @@
161
169
  displayOptions:(JS::NativeCallingx::SpecDisplayIncomingCallDisplayOptions &)displayOptions
162
170
  resolve:(nonnull RCTPromiseResolveBlock)resolve
163
171
  reject:(nonnull RCTPromiseRejectBlock)reject {
164
- BOOL result = [_moduleImpl displayIncomingCallWithCallId:callId
165
- phoneNumber:phoneNumber
166
- callerName:callerName
167
- hasVideo:hasVideo];
168
- resolve(@(result));
172
+ [_moduleImpl displayIncomingCallWithCallId:callId
173
+ phoneNumber:phoneNumber
174
+ callerName:callerName
175
+ hasVideo:hasVideo
176
+ resolve:resolve
177
+ reject:reject
178
+ ];
169
179
  }
170
180
 
171
181
  - (void)endCallWithReason:(nonnull NSString *)callId
@@ -183,8 +193,8 @@
183
193
  resolve(@(result));
184
194
  }
185
195
 
186
- - (NSNumber *)isCallRegistered:(nonnull NSString *)callId {
187
- return @([_moduleImpl isCallRegistered:callId]);
196
+ - (NSNumber *)isCallTracked:(nonnull NSString *)callId {
197
+ return @([_moduleImpl isCallTracked:callId]);
188
198
  }
189
199
 
190
200
  - (NSNumber *)hasRegisteredCall {
@@ -0,0 +1,105 @@
1
+ import Foundation
2
+
3
+ /// Represents a call tracked by the CallingxImpl module.
4
+ /// Holds per-call lifecycle state to guard against duplicate actions
5
+ /// and to track timestamps for CallKit reporting.
6
+ @objcMembers public class CallingxCall: NSObject {
7
+ public let uuid: UUID
8
+ public let cid: String
9
+ public let isOutgoing: Bool
10
+
11
+ // MARK: - Action-source flags
12
+ // These flags track whether an action was initiated from the app (vs system UI).
13
+ // They follow a "set-then-read-then-reset" pattern in the CXProviderDelegate methods
14
+ // to determine the event source ("app" vs "sys") before being reset.
15
+
16
+ /// Whether answerIncomingCall was initiated from the app (vs system UI)
17
+ public private(set) var isSelfAnswered: Bool = false
18
+ /// Whether endCall was initiated from the app (vs system UI)
19
+ public private(set) var isSelfEnded: Bool = false
20
+ /// Whether setMutedCall was initiated from the app (vs system UI)
21
+ public private(set) var isSelfMuted: Bool = false
22
+
23
+ // MARK: - Lifecycle timestamps
24
+
25
+ /// When the call started connecting (outgoing: maps to reportOutgoingCall(startedConnectingAt:);
26
+ /// incoming: set when answerIncomingCall is called, internal-only)
27
+ public private(set) var startedConnectingAt: Date?
28
+ /// When the call became connected (outgoing: maps to reportOutgoingCall(connectedAt:);
29
+ /// incoming: set when CXAnswerCallAction delegate fires, internal-only)
30
+ public private(set) var connectedAt: Date?
31
+ /// When the call ended
32
+ public private(set) var endedAt: Date?
33
+
34
+ // MARK: - Derived states
35
+
36
+ public var hasStartedConnecting: Bool { startedConnectingAt != nil }
37
+ public var isConnected: Bool { connectedAt != nil }
38
+ public var hasEnded: Bool { endedAt != nil }
39
+ /// Whether the call has been answered (incoming) or started connecting (outgoing).
40
+ /// Checks both startedConnectingAt (set by answerIncomingCall from app) and connectedAt
41
+ /// (set by CXAnswerCallAction delegate, which fires even when answered from system UI).
42
+ /// Used as the primary guard against duplicate answerIncomingCall invocations.
43
+ public var isAnswered: Bool { startedConnectingAt != nil || connectedAt != nil }
44
+
45
+ // MARK: - Initialization
46
+
47
+ public init(uuid: UUID, cid: String, isOutgoing: Bool = false) {
48
+ self.uuid = uuid
49
+ self.cid = cid
50
+ self.isOutgoing = isOutgoing
51
+ }
52
+
53
+ // MARK: - Action-source flag methods
54
+
55
+ public func markSelfAnswered() { isSelfAnswered = true }
56
+ public func markSelfEnded() { isSelfEnded = true }
57
+ public func markSelfMuted() { isSelfMuted = true }
58
+
59
+ public func resetSelfAnswered() { isSelfAnswered = false }
60
+ public func resetSelfEnded() { isSelfEnded = false }
61
+ public func resetSelfMuted() { isSelfMuted = false }
62
+
63
+ /// Resets all action-source flags. Called when a CXTransaction fails.
64
+ public func resetAllSelfFlags() {
65
+ isSelfAnswered = false
66
+ isSelfEnded = false
67
+ isSelfMuted = false
68
+ }
69
+
70
+ // MARK: - Lifecycle transition methods
71
+
72
+ public func markStartedConnecting() {
73
+ if startedConnectingAt == nil {
74
+ startedConnectingAt = Date()
75
+ }
76
+ }
77
+
78
+ public func markConnected() {
79
+ if connectedAt == nil {
80
+ connectedAt = Date()
81
+ }
82
+ }
83
+
84
+ public func markEnded() {
85
+ if endedAt == nil {
86
+ endedAt = Date()
87
+ }
88
+ }
89
+
90
+ // MARK: - Debug description
91
+
92
+ public override var description: String {
93
+ let state: String
94
+ if hasEnded {
95
+ state = "ended"
96
+ } else if isConnected {
97
+ state = "connected"
98
+ } else if hasStartedConnecting {
99
+ state = "connecting"
100
+ } else {
101
+ state = "ringing"
102
+ }
103
+ return "CallingxCall(cid: \(cid), uuid: \(uuid.uuidString.lowercased()), outgoing: \(isOutgoing), state: \(state))"
104
+ }
105
+ }
@@ -31,25 +31,23 @@ import stream_react_native_webrtc
31
31
  @objc public static var sharedProvider: CXProvider?
32
32
  @objc public static var uuidStorage: UUIDStorage?
33
33
  @objc public static weak var sharedInstance: CallingxImpl?
34
+ /// Events stored before the module instance exists (e.g. VoIP from killed state). Drained in getInitialEvents().
35
+ private static var delayedEvents: [[String: Any]] = []
34
36
 
35
37
  // MARK: - Instance Properties
36
38
  @objc public var callKeepCallController: CXCallController?
37
39
  @objc public var callKeepProvider: CXProvider?
38
40
  @objc public weak var eventEmitter: CallingxEventEmitter?
41
+ @objc public weak var webRTCModule: WebRTCModule?
39
42
 
40
43
  private var canSendEvents: Bool = false
41
44
  private var isSetup: Bool = false
42
- private var isSelfAnswered: Bool = false
43
- private var isSelfEnded: Bool = false
44
- private var isSelfMuted: Bool = false
45
- private var delayedEvents: [[String: Any]] = []
46
45
 
47
46
  // MARK: - Initialization
48
47
  @objc public override init() {
49
48
  super.init()
50
49
 
51
50
  isSetup = false
52
- delayedEvents = []
53
51
  canSendEvents = false
54
52
 
55
53
  NotificationCenter.default.addObserver(
@@ -74,7 +72,6 @@ import stream_react_native_webrtc
74
72
  callKeepProvider?.invalidate()
75
73
  CallingxImpl.sharedProvider = nil
76
74
  canSendEvents = false
77
- delayedEvents = []
78
75
  isSetup = false
79
76
  }
80
77
 
@@ -99,9 +96,10 @@ import stream_react_native_webrtc
99
96
  supportsDTMF: Bool,
100
97
  supportsGrouping: Bool,
101
98
  supportsUngrouping: Bool,
102
- fromPushKit: Bool,
103
99
  payload: [String: Any]?,
104
- completion: (() -> Void)?
100
+ completion: (() -> Void)?,
101
+ resolve: RCTPromiseResolveBlock?,
102
+ reject: RCTPromiseRejectBlock?
105
103
  ) {
106
104
  initializeIfNeeded()
107
105
 
@@ -112,6 +110,7 @@ import stream_react_native_webrtc
112
110
  print("[Callingx][reportNewIncomingCall] callId already exists")
113
111
  #endif
114
112
  completion?()
113
+ resolve?(true)
115
114
  return
116
115
  }
117
116
 
@@ -144,16 +143,28 @@ import stream_react_native_webrtc
144
143
  "supportsDTMF": supportsDTMF ? "1" : "0",
145
144
  "supportsGrouping": supportsGrouping ? "1" : "0",
146
145
  "supportsUngrouping": supportsUngrouping ? "1" : "0",
147
- "fromPushKit": fromPushKit ? "1" : "0",
148
146
  "payload": payload ?? ""
149
147
  ]
150
148
 
151
- sharedInstance?.sendEvent(CallingxEvents.didDisplayIncomingCall, body: body)
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
+ }
159
+ }
152
160
 
153
161
  if error == nil {
154
162
  #if DEBUG
155
163
  print("[Callingx][reportNewIncomingCall] success callId = \(callId)")
156
164
  #endif
165
+ resolve?(true)
166
+ } else {
167
+ reject?("DISPLAY_INCOMING_CALL_ERROR", error?.localizedDescription, error)
157
168
  }
158
169
 
159
170
  completion?()
@@ -196,17 +207,19 @@ import stream_react_native_webrtc
196
207
  print("[Callingx][endCall] callId = \(callId) reason = \(reason)")
197
208
  #endif
198
209
 
199
- guard let uuid = uuidStorage?.getUUID(forCid: callId) else {
210
+ guard let call = uuidStorage?.getCall(forCid: callId) else {
200
211
  #if DEBUG
201
212
  print("[Callingx][endCall] callId not found")
202
213
  #endif
203
214
  return
204
215
  }
205
216
 
217
+ call.markEnded()
218
+
206
219
  // CXCallEndedReason raw values: failed=1, remoteEnded=2, unanswered=3, answeredElsewhere=4, declinedElsewhere=5
207
220
  let endedReason = CXCallEndedReason(rawValue: reason) ?? .failed
208
221
 
209
- sharedProvider?.reportCall(with: uuid, endedAt: Date(), reason: endedReason)
222
+ sharedProvider?.reportCall(with: call.uuid, endedAt: call.endedAt ?? Date(), reason: endedReason)
210
223
  uuidStorage?.removeCid(callId)
211
224
  }
212
225
 
@@ -242,9 +255,13 @@ import stream_react_native_webrtc
242
255
  print("[Callingx][requestTransaction] Error requesting transaction (\(transaction.actions)): (\(error))")
243
256
  #endif
244
257
 
245
- self?.isSelfAnswered = false
246
- self?.isSelfEnded = false
247
- self?.isSelfMuted = false
258
+ // Reset per-call action-source flags for all actions in the failed transaction
259
+ for action in transaction.actions {
260
+ if let callAction = action as? CXCallAction,
261
+ let call = CallingxImpl.uuidStorage?.getCallByUUID(callAction.callUUID) {
262
+ call.resetAllSelfFlags()
263
+ }
264
+ }
248
265
  } else {
249
266
  #if DEBUG
250
267
  print("[Callingx][requestTransaction] Requested transaction successfully")
@@ -280,9 +297,9 @@ import stream_react_native_webrtc
280
297
  if self.canSendEvents {
281
298
  self.eventEmitter?.emitEvent(dictionary)
282
299
  } else {
283
- self.delayedEvents.append(dictionary)
300
+ CallingxImpl.delayedEvents.append(dictionary)
284
301
  #if DEBUG
285
- print("[Callingx] delayedEvents: \(self.delayedEvents)")
302
+ print("[Callingx] delayedEvents: \(CallingxImpl.delayedEvents)")
286
303
  #endif
287
304
  }
288
305
  }
@@ -329,11 +346,11 @@ import stream_react_native_webrtc
329
346
  var events: [[String: Any]] = []
330
347
  let action = {
331
348
  #if DEBUG
332
- print("[Callingx][getInitialEvents] delayedEvents = \(self.delayedEvents)")
349
+ print("[Callingx][getInitialEvents] delayedEvents = \(CallingxImpl.delayedEvents)")
333
350
  #endif
334
351
 
335
- events = self.delayedEvents
336
- self.delayedEvents = []
352
+ events = CallingxImpl.delayedEvents
353
+ CallingxImpl.delayedEvents = []
337
354
  self.canSendEvents = true
338
355
  }
339
356
 
@@ -354,15 +371,25 @@ import stream_react_native_webrtc
354
371
  print("[Callingx][answerIncomingCall] callId = \(callId)")
355
372
  #endif
356
373
 
357
- guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
374
+ guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
358
375
  #if DEBUG
359
376
  print("[Callingx][answerIncomingCall] callId not found")
360
377
  #endif
361
378
  return false
362
379
  }
363
380
 
364
- isSelfAnswered = true
365
- let answerCallAction = CXAnswerCallAction(call: uuid)
381
+ // Guard: already answered or ended — prevent duplicate CXAnswerCallAction transactions
382
+ if call.isAnswered || call.hasEnded {
383
+ #if DEBUG
384
+ print("[Callingx][answerIncomingCall] callId already answered/ended, skipping")
385
+ #endif
386
+ return true
387
+ }
388
+
389
+ call.markSelfAnswered()
390
+ call.markStartedConnecting() // internal state: incoming call is now connecting
391
+
392
+ let answerCallAction = CXAnswerCallAction(call: call.uuid)
366
393
  let transaction = CXTransaction()
367
394
  transaction.addAction(answerCallAction)
368
395
 
@@ -374,8 +401,10 @@ import stream_react_native_webrtc
374
401
  callId: String,
375
402
  phoneNumber: String,
376
403
  callerName: String,
377
- hasVideo: Bool
378
- ) -> Bool {
404
+ hasVideo: Bool,
405
+ resolve: @escaping RCTPromiseResolveBlock,
406
+ reject: @escaping RCTPromiseRejectBlock
407
+ ) {
379
408
  let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId)
380
409
  CallingxImpl.reportNewIncomingCall(
381
410
  callId: callId,
@@ -387,9 +416,10 @@ import stream_react_native_webrtc
387
416
  supportsDTMF: false,
388
417
  supportsGrouping: false,
389
418
  supportsUngrouping: false,
390
- fromPushKit: false,
391
419
  payload: nil,
392
- completion: nil
420
+ completion: nil,
421
+ resolve: resolve,
422
+ reject: reject
393
423
  )
394
424
 
395
425
  let wasAlreadyAnswered = uuid != nil
@@ -406,7 +436,6 @@ import stream_react_native_webrtc
406
436
  }
407
437
  }
408
438
  }
409
- return true
410
439
  }
411
440
 
412
441
  @objc public func endCall(_ callId: String) -> Bool {
@@ -414,25 +443,35 @@ import stream_react_native_webrtc
414
443
  print("[Callingx][endCall] callId = \(callId)")
415
444
  #endif
416
445
 
417
- guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
446
+ guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
418
447
  #if DEBUG
419
448
  print("[Callingx][endCall] callId not found")
420
449
  #endif
421
450
  return false
422
451
  }
423
452
 
424
- isSelfEnded = true
425
- let endCallAction = CXEndCallAction(call: uuid)
453
+ // Guard: already ended — prevent duplicate CXEndCallAction transactions
454
+ if call.hasEnded {
455
+ #if DEBUG
456
+ print("[Callingx][endCall] callId already ended, skipping")
457
+ #endif
458
+ return true
459
+ }
460
+
461
+ call.markSelfEnded()
462
+ call.markEnded()
463
+
464
+ let endCallAction = CXEndCallAction(call: call.uuid)
426
465
  let transaction = CXTransaction(action: endCallAction)
427
466
 
428
467
  requestTransaction(transaction)
429
468
  return true
430
469
  }
431
470
 
432
- @objc public func isCallRegistered(_ callId: String) -> Bool {
471
+ @objc public func isCallTracked(_ callId: String) -> Bool {
433
472
  guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
434
473
  #if DEBUG
435
- print("[Callingx][isCallRegistered] callId not found")
474
+ print("[Callingx][isCallTracked] callId not found")
436
475
  #endif
437
476
  return false
438
477
  }
@@ -451,16 +490,18 @@ import stream_react_native_webrtc
451
490
  print("[Callingx][setCurrentCallActive] callId = \(callId)")
452
491
  #endif
453
492
 
454
- guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
493
+ guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
455
494
  #if DEBUG
456
495
  print("[Callingx][setCurrentCallActive] callId not found")
457
496
  #endif
458
497
  return false
459
498
  }
460
499
 
461
- //consider to split this into to calls startedConnectingAt with startCall, connectedAt with setCurrentCallActive
462
- callKeepProvider?.reportOutgoingCall(with: uuid, startedConnectingAt: Date())
463
- callKeepProvider?.reportOutgoingCall(with: uuid, connectedAt: Date())
500
+ call.markConnected()
501
+
502
+ // Report connected timestamp to CallKit.
503
+ // startedConnectingAt is reported separately in the CXStartCallAction delegate.
504
+ callKeepProvider?.reportOutgoingCall(with: call.uuid, connectedAt: call.connectedAt ?? Date())
464
505
  return true
465
506
  }
466
507
 
@@ -469,15 +510,15 @@ import stream_react_native_webrtc
469
510
  print("[Callingx][setMutedCall] muted = \(isMuted)")
470
511
  #endif
471
512
 
472
- guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
513
+ guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
473
514
  #if DEBUG
474
515
  print("[Callingx][setMutedCall] callId not found")
475
516
  #endif
476
517
  return false
477
518
  }
478
519
 
479
- isSelfMuted = true
480
- let setMutedAction = CXSetMutedCallAction(call: uuid, muted: isMuted)
520
+ call.markSelfMuted()
521
+ let setMutedAction = CXSetMutedCallAction(call: call.uuid, muted: isMuted)
481
522
  let transaction = CXTransaction()
482
523
  transaction.addAction(setMutedAction)
483
524
 
@@ -524,10 +565,12 @@ import stream_react_native_webrtc
524
565
  return
525
566
  }
526
567
 
568
+ let call = storage.getOrCreateCall(forCid: callId, isOutgoing: true)
569
+ call.markStartedConnecting() // outgoing: will be reported via reportOutgoingCall(startedConnectingAt:)
570
+
527
571
  let handleType = Settings.getHandleType("generic")
528
- let uuid = storage.getOrCreateUUID(forCid: callId)
529
572
  let callHandle = CXHandle(type: handleType, value: phoneNumber)
530
- let startCallAction = CXStartCallAction(call: uuid, handle: callHandle)
573
+ let startCallAction = CXStartCallAction(call: call.uuid, handle: callHandle)
531
574
  startCallAction.isVideo = hasVideo
532
575
  startCallAction.contactIdentifier = callerName
533
576
 
@@ -568,7 +611,7 @@ import stream_react_native_webrtc
568
611
  print("[Callingx][CXProviderDelegate][provider:performStartCallAction]")
569
612
  #endif
570
613
 
571
- guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
614
+ guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
572
615
  #if DEBUG
573
616
  print("[Callingx][CXProviderDelegate][provider:performStartCallAction] callId not found")
574
617
  #endif
@@ -576,22 +619,23 @@ import stream_react_native_webrtc
576
619
  return
577
620
  }
578
621
 
622
+ getAudioDeviceModule()?.reset()
579
623
  AudioSessionManager.createAudioSessionIfNeeded()
580
624
 
581
625
  sendEvent(CallingxEvents.didReceiveStartCallAction, body: [
582
- "callId": callId,
626
+ "callId": call.cid,
583
627
  "handle": action.handle.value
584
628
  ])
585
629
 
586
630
  action.fulfill()
631
+
632
+ // Report startedConnectingAt to CallKit now that the action is fulfilled.
633
+ // The timestamp was set in startCall when the call was created.
634
+ callKeepProvider?.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.startedConnectingAt ?? Date())
587
635
  }
588
636
 
589
637
  public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
590
- #if DEBUG
591
- print("[Callingx][CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: \(isSelfAnswered)")
592
- #endif
593
-
594
- guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
638
+ guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
595
639
  #if DEBUG
596
640
  print("[Callingx][CXProviderDelegate][provider:performAnswerCallAction] callId not found")
597
641
  #endif
@@ -599,24 +643,29 @@ import stream_react_native_webrtc
599
643
  return
600
644
  }
601
645
 
646
+ #if DEBUG
647
+ print("[Callingx][CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: \(call.isSelfAnswered)")
648
+ #endif
649
+
650
+ getAudioDeviceModule()?.reset()
602
651
  AudioSessionManager.createAudioSessionIfNeeded()
603
652
 
604
- let source = isSelfAnswered ? "app" : "sys"
653
+ let source = call.isSelfAnswered ? "app" : "sys"
605
654
  sendEvent(CallingxEvents.performAnswerCallAction, body: [
606
- "callId": callId,
655
+ "callId": call.cid,
607
656
  "source": source
608
657
  ])
609
658
 
610
- isSelfAnswered = false
659
+ call.resetSelfAnswered()
660
+ 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
611
664
  action.fulfill()
612
665
  }
613
666
 
614
667
  public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
615
- #if DEBUG
616
- print("[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(isSelfEnded)")
617
- #endif
618
-
619
- guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
668
+ guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
620
669
  #if DEBUG
621
670
  print("[Callingx][CXProviderDelegate][provider:performEndCallAction] callId not found")
622
671
  #endif
@@ -624,14 +673,19 @@ import stream_react_native_webrtc
624
673
  return
625
674
  }
626
675
 
627
- let source = isSelfEnded ? "app" : "sys"
676
+ #if DEBUG
677
+ print("[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(call.isSelfEnded)")
678
+ #endif
679
+
680
+ let source = call.isSelfEnded ? "app" : "sys"
628
681
  sendEvent(CallingxEvents.performEndCallAction, body: [
629
- "callId": callId,
682
+ "callId": call.cid,
630
683
  "source": source
631
684
  ])
632
685
 
633
- isSelfEnded = false
634
- CallingxImpl.uuidStorage?.removeCid(callId)
686
+ call.resetSelfEnded()
687
+ call.markEnded()
688
+ CallingxImpl.uuidStorage?.removeCid(call.cid)
635
689
 
636
690
  action.fulfill()
637
691
  }
@@ -658,14 +712,7 @@ import stream_react_native_webrtc
658
712
  }
659
713
 
660
714
  public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
661
- let isAppInitiated = isSelfMuted
662
- isSelfMuted = false
663
-
664
- #if DEBUG
665
- print("[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] \(action.isMuted) isAppInitiated: \(isAppInitiated)")
666
- #endif
667
-
668
- guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
715
+ guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
669
716
  #if DEBUG
670
717
  print("[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] callId not found")
671
718
  #endif
@@ -673,6 +720,13 @@ import stream_react_native_webrtc
673
720
  return
674
721
  }
675
722
 
723
+ let isAppInitiated = call.isSelfMuted
724
+ call.resetSelfMuted()
725
+
726
+ #if DEBUG
727
+ print("[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] \(action.isMuted) isAppInitiated: \(isAppInitiated)")
728
+ #endif
729
+
676
730
  // Only send the event to JS when the mute was initiated by the system
677
731
  // (e.g. user tapped mute on the native CallKit UI).
678
732
  // Skip app-initiated actions to prevent the feedback loop:
@@ -680,7 +734,7 @@ import stream_react_native_webrtc
680
734
  if !isAppInitiated {
681
735
  sendEvent(CallingxEvents.didPerformSetMutedCallAction, body: [
682
736
  "muted": action.isMuted,
683
- "callId": callId
737
+ "callId": call.cid
684
738
  ])
685
739
  }
686
740
 
@@ -731,6 +785,7 @@ import stream_react_native_webrtc
731
785
 
732
786
  // When CallKit deactivates the AVAudioSession, inform WebRTC as well.
733
787
  RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
788
+ getAudioDeviceModule()?.reset()
734
789
 
735
790
  // Disable wake lock when the call ends
736
791
  DispatchQueue.main.async {
@@ -753,5 +808,16 @@ import stream_react_native_webrtc
753
808
 
754
809
  sendEvent(CallingxEvents.providerReset, body: nil)
755
810
  }
811
+
812
+ // MARK: - Helper Methods
813
+ private func getAudioDeviceModule() -> AudioDeviceModule? {
814
+ guard let adm = webRTCModule?.audioDeviceModule else {
815
+ #if DEBUG
816
+ print("[Callingx] WebRTCModule is not available. Ensure it was injected from the TurboModule host.")
817
+ #endif
818
+ return nil
819
+ }
820
+ return adm
821
+ }
756
822
  }
757
823
 
@@ -21,9 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
21
21
  * supportsDTMF:NO
22
22
  * supportsGrouping:NO
23
23
  * supportsUngrouping:NO
24
- * fromPushKit:YES
25
24
  * payload:payload
26
- * withCompletionHandler:^{ }];
25
+ * withCompletionHandler:^(void){ }];
27
26
  * ```
28
27
  */
29
28
  @interface Callingx : NSObject
@@ -41,9 +40,8 @@ NS_ASSUME_NONNULL_BEGIN
41
40
  * @param supportsDTMF Whether the call supports DTMF tones
42
41
  * @param supportsGrouping Whether the call can be grouped with other calls
43
42
  * @param supportsUngrouping Whether the call can be ungrouped
44
- * @param fromPushKit Whether this call is from a PushKit notification
45
43
  * @param payload Optional payload data from the push notification
46
- * @param completion Completion handler called after the call is reported
44
+ * @param completion Completion handler called after the call is reported, with an error if the call could not be displayed
47
45
  */
48
46
  + (void)reportNewIncomingCall:(NSString *)callId
49
47
  handle:(NSString *)handle
@@ -54,7 +52,6 @@ NS_ASSUME_NONNULL_BEGIN
54
52
  supportsDTMF:(BOOL)supportsDTMF
55
53
  supportsGrouping:(BOOL)supportsGrouping
56
54
  supportsUngrouping:(BOOL)supportsUngrouping
57
- fromPushKit:(BOOL)fromPushKit
58
55
  payload:(NSDictionary * _Nullable)payload
59
56
  withCompletionHandler:(void (^_Nullable)(void))completion;
60
57