@stream-io/react-native-callingx 0.3.1 → 0.5.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.
@@ -0,0 +1,67 @@
1
+ import Foundation
2
+ import os
3
+
4
+ /// Unified-logging namespace for the Callingx package.
5
+ ///
6
+ /// Replaces the previous `NSLog` usage. Each category maps to one of the
7
+ /// `[Tag]` prefixes that used to be hand-built into the log strings, so logs
8
+ /// can be filtered by subsystem/category in Console.app or `log stream`:
9
+ ///
10
+ /// log stream --predicate 'subsystem == "io.getstream.callingx"' --level debug
11
+ ///
12
+ /// The subsystem matches the queue-label convention already used across this
13
+ /// package (e.g. `io.getstream.callingx.pendingActions`).
14
+ enum CallingxLog {
15
+ private static let subsystem = "io.getstream.callingx"
16
+
17
+ static let core = Logger(subsystem: subsystem, category: "Callingx")
18
+ static let uuid = Logger(subsystem: subsystem, category: "UUIDStorage")
19
+ static let voip = Logger(subsystem: subsystem, category: "VoipNotifications")
20
+ static let push = Logger(subsystem: subsystem, category: "VoipPush")
21
+ static let settings = Logger(subsystem: subsystem, category: "Settings")
22
+ static let audio = Logger(subsystem: subsystem, category: "AudioSession")
23
+ static let js = Logger(subsystem: subsystem, category: "JS")
24
+ }
25
+
26
+ extension Logger {
27
+ /// Logs a pre-built message at `.debug`, marking the whole string public so
28
+ /// values stay readable when streaming logs (incl. release/TestFlight).
29
+ /// `@autoclosure` keeps construction lazy: when the log is not being
30
+ /// collected the string is never built.
31
+ func debugPublic(_ message: @autoclosure @escaping () -> String) {
32
+ debug("\(message(), privacy: .public)")
33
+ }
34
+
35
+ /// Public-message variant at `.error` (persisted by default in the unified log).
36
+ func errorPublic(_ message: @autoclosure @escaping () -> String) {
37
+ error("\(message(), privacy: .public)")
38
+ }
39
+ }
40
+
41
+ /// Objective-C bridge so `Callingx.mm` and `VoipPushHandler.m` log through the
42
+ /// same `os.Logger` path. `Logger`'s interpolation API is Swift-only, hence the
43
+ /// thin `@objc` wrapper.
44
+ @objc public final class CallingxLogBridge: NSObject {
45
+ @objc public static func pushDebug(_ message: String) {
46
+ CallingxLog.push.debugPublic(message)
47
+ }
48
+
49
+ @objc public static func pushError(_ message: String) {
50
+ CallingxLog.push.errorPublic(message)
51
+ }
52
+
53
+ /// Routes JS-originated logs (from the `log(message, level)` TurboModule
54
+ /// method) to the matching os.Logger severity.
55
+ @objc public static func js(_ message: String, level: String) {
56
+ switch level {
57
+ case "error":
58
+ CallingxLog.js.error("\(message, privacy: .public)")
59
+ case "warn":
60
+ CallingxLog.js.warning("\(message, privacy: .public)")
61
+ case "info":
62
+ CallingxLog.js.info("\(message, privacy: .public)")
63
+ default:
64
+ CallingxLog.js.debug("\(message, privacy: .public)")
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,37 @@
1
+ import Foundation
2
+
3
+ /// Cross-package handoff guard. `CallingxImpl` flips `callingxOwnsSession`
4
+ /// to `true` at CX-action entry points (`CXStartCallAction.perform`,
5
+ /// `CXAnswerCallAction.perform`, re-asserted in `provider(_:didActivate:)`)
6
+ /// and back to `false` in `provider(_:didDeactivate:)` when no calls remain
7
+ /// (`UUIDStorage.count() == 0`) or on `providerDidReset`.
8
+ ///
9
+ /// `StreamInCallManager` (in `@stream-io/video-react-native-sdk`) consults this
10
+ /// flag from its AudioDeviceModule publisher sink and no-ops when callingx owns
11
+ /// the session, so the two packages don't write conflicting `AVAudioSession`
12
+ /// configurations during the transient overlap when a CallKit call is winding
13
+ /// down and the SDK is about to take over (or vice versa).
14
+ ///
15
+ /// Exposed as an `@objcMembers NSObject` with an `@objc class var` so that
16
+ /// react-native-sdk can read it via `NSClassFromString("Callingx.CallingxSessionOwnership")`
17
+ /// + KVC on the class object — `@stream-io/react-native-callingx` is an *optional*
18
+ /// peer dependency of react-native-sdk, so a direct `import Callingx` is not safe.
19
+ @objcMembers
20
+ public class CallingxSessionOwnership: NSObject {
21
+
22
+ private static let lock = NSLock()
23
+ private static var _callingxOwnsSession: Bool = false
24
+
25
+ @objc public class var callingxOwnsSession: Bool {
26
+ get {
27
+ lock.lock()
28
+ defer { lock.unlock() }
29
+ return _callingxOwnsSession
30
+ }
31
+ set {
32
+ lock.lock()
33
+ defer { lock.unlock() }
34
+ _callingxOwnsSession = newValue
35
+ }
36
+ }
37
+ }
@@ -10,9 +10,7 @@ import UIKit
10
10
  }
11
11
 
12
12
  public static func setSettings(_ options: [String: Any]?) {
13
- #if DEBUG
14
- NSLog("%@","[Settings][setSettings] options = \(String(describing: options))")
15
- #endif
13
+ CallingxLog.settings.debugPublic("[setSettings] options = \(String(describing: options))")
16
14
 
17
15
  var settings: [String: Any] = getSettings()
18
16
 
@@ -45,9 +43,7 @@ import UIKit
45
43
  }
46
44
 
47
45
  public static func getProviderConfiguration() -> CXProviderConfiguration {
48
- #if DEBUG
49
- NSLog("%@","[Settings][getProviderConfiguration]")
50
- #endif
46
+ CallingxLog.settings.debugPublic("[getProviderConfiguration]")
51
47
 
52
48
  let settings = getSettings()
53
49
  let providerConfiguration = CXProviderConfiguration()
@@ -17,9 +17,7 @@ import Foundation
17
17
  public func getOrCreateCall(forCid cid: String, isOutgoing: Bool = false) -> CallingxCall {
18
18
  return queue.sync {
19
19
  if let existing = callsByCid[cid] {
20
- #if DEBUG
21
- NSLog("%@","[UUIDStorage] getOrCreateCall: found existing \(existing)")
22
- #endif
20
+ CallingxLog.uuid.debugPublic("getOrCreateCall: found existing \(existing)")
23
21
  return existing
24
22
  }
25
23
 
@@ -28,9 +26,7 @@ import Foundation
28
26
  let uuidString = uuid.uuidString.lowercased()
29
27
  callsByCid[cid] = call
30
28
  callsByUUID[uuidString] = call
31
- #if DEBUG
32
- NSLog("%@","[UUIDStorage] getOrCreateCall: created \(call)")
33
- #endif
29
+ CallingxLog.uuid.debugPublic("getOrCreateCall: created \(call)")
34
30
  return call
35
31
  }
36
32
  }
@@ -62,9 +58,7 @@ import Foundation
62
58
  public func getOrCreateUUID(forCid cid: String) -> UUID {
63
59
  return queue.sync {
64
60
  if let existing = callsByCid[cid] {
65
- #if DEBUG
66
- NSLog("%@","[UUIDStorage] getUUIDForCid: found existing UUID \(existing.uuid.uuidString.lowercased()) for cid \(cid)")
67
- #endif
61
+ CallingxLog.uuid.debugPublic("getUUIDForCid: found existing UUID \(existing.uuid.uuidString.lowercased()) for cid \(cid)")
68
62
  return existing.uuid
69
63
  }
70
64
 
@@ -73,9 +67,7 @@ import Foundation
73
67
  let uuidString = uuid.uuidString.lowercased()
74
68
  callsByCid[cid] = call
75
69
  callsByUUID[uuidString] = call
76
- #if DEBUG
77
- NSLog("%@","[UUIDStorage] getUUIDForCid: created new UUID \(uuidString) for cid \(cid)")
78
- #endif
70
+ CallingxLog.uuid.debugPublic("getUUIDForCid: created new UUID \(uuidString) for cid \(cid)")
79
71
  return uuid
80
72
  }
81
73
  }
@@ -90,9 +82,7 @@ import Foundation
90
82
  return queue.sync {
91
83
  let uuidString = uuid.uuidString.lowercased()
92
84
  let cid = callsByUUID[uuidString]?.cid
93
- #if DEBUG
94
- NSLog("%@","[UUIDStorage] getCidForUUID: UUID \(uuidString) -> cid \(cid ?? "(not found)")")
95
- #endif
85
+ CallingxLog.uuid.debugPublic("getCidForUUID: UUID \(uuidString) -> cid \(cid ?? "(not found)")")
96
86
  return cid
97
87
  }
98
88
  }
@@ -103,13 +93,9 @@ import Foundation
103
93
  if let call = callsByUUID[uuidString] {
104
94
  callsByCid.removeValue(forKey: call.cid)
105
95
  callsByUUID.removeValue(forKey: uuidString)
106
- #if DEBUG
107
- NSLog("%@","[UUIDStorage] removeCidForUUID: removed cid \(call.cid) for UUID \(uuidString)")
108
- #endif
96
+ CallingxLog.uuid.debugPublic("removeCidForUUID: removed cid \(call.cid) for UUID \(uuidString)")
109
97
  } else {
110
- #if DEBUG
111
- NSLog("%@","[UUIDStorage] removeCidForUUID: no cid found for UUID \(uuidString)")
112
- #endif
98
+ CallingxLog.uuid.debugPublic("removeCidForUUID: no cid found for UUID \(uuidString)")
113
99
  }
114
100
  }
115
101
  }
@@ -120,13 +106,9 @@ import Foundation
120
106
  let uuidString = call.uuid.uuidString.lowercased()
121
107
  callsByUUID.removeValue(forKey: uuidString)
122
108
  callsByCid.removeValue(forKey: cid)
123
- #if DEBUG
124
- NSLog("%@","[UUIDStorage] removeCid: removed cid \(cid) with UUID \(uuidString)")
125
- #endif
109
+ CallingxLog.uuid.debugPublic("removeCid: removed cid \(cid) with UUID \(uuidString)")
126
110
  } else {
127
- #if DEBUG
128
- NSLog("%@","[UUIDStorage] removeCid: no UUID found for cid \(cid)")
129
- #endif
111
+ CallingxLog.uuid.debugPublic("removeCid: no UUID found for cid \(cid)")
130
112
  }
131
113
  }
132
114
  }
@@ -136,9 +118,7 @@ import Foundation
136
118
  let count = callsByCid.count
137
119
  callsByCid.removeAll()
138
120
  callsByUUID.removeAll()
139
- #if DEBUG
140
- NSLog("%@","[UUIDStorage] removeAllObjects: cleared \(count) entries")
141
- #endif
121
+ CallingxLog.uuid.debugPublic("removeAllObjects: cleared \(count) entries")
142
122
  }
143
123
  }
144
124
 
@@ -20,7 +20,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
20
20
  private static var isVoipRegistered = false
21
21
  private static var lastVoipToken = ""
22
22
  private static var voipRegistry: PKPushRegistry?
23
-
23
+
24
24
  private var canSendEvents: Bool = false
25
25
  private var delayedEvents: [[String:Any]] = []
26
26
 
@@ -55,41 +55,26 @@ typealias RNVoipPushNotificationCompletion = () -> Void
55
55
 
56
56
  @objc public static func voipRegistration() {
57
57
  if isVoipRegistered {
58
- #if DEBUG
59
- NSLog("%@","[VoipNotificationsManager] voipRegistration is already registered. return _lastVoipToken = \(lastVoipToken)")
60
- #endif
58
+ CallingxLog.voip.debugPublic("voipRegistration is already registered. return _lastVoipToken = \(lastVoipToken)")
61
59
  let voipPushManager = VoipNotificationsManager.shared()
62
60
  voipPushManager.sendEventWithNameWrapper(name: VoipNotificationsEvents.registered, body: ["token": lastVoipToken])
63
- } else {
64
- #if DEBUG
65
- NSLog("%@","[VoipNotificationsManager] voipRegistration enter")
66
- #endif
67
- DispatchQueue.main.async {
68
- let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
69
- // Set the registry's delegate to AppDelegate
70
- // Note: The original code casts the delegate, but this should be handled by AppDelegate
71
- if let appDelegate = RCTSharedApplication()?.delegate as? PKPushRegistryDelegate {
72
- voipRegistry.delegate = appDelegate
73
- // Set the push type to VoIP
74
- // Store the registry to prevent deallocation
75
- voipRegistry.desiredPushTypes = [.voIP]
76
- VoipNotificationsManager.voipRegistry = voipRegistry
77
-
78
- isVoipRegistered = true
79
- } else {
80
- #if DEBUG
81
- NSLog("%@","[VoipNotificationsManager] voipRegistration appDelegate not found. return")
82
- #endif
83
- }
84
- }
61
+ return
62
+ }
63
+
64
+ CallingxLog.voip.debugPublic("voipRegistration enter")
65
+
66
+ DispatchQueue.main.async {
67
+ let registry = PKPushRegistry(queue: DispatchQueue.main)
68
+ registry.delegate = VoipPushHandler.sharedInstance()
69
+ registry.desiredPushTypes = [.voIP]
70
+ VoipNotificationsManager.voipRegistry = registry
71
+ isVoipRegistered = true
85
72
  }
86
73
  }
87
74
 
88
75
  @objc public static func didUpdatePushCredentials(_ credentials: PKPushCredentials, forType type: String) {
89
- #if DEBUG
90
- NSLog("%@","[VoipNotificationsManager] didUpdatePushCredentials credentials.token = \(credentials.token), type = \(type)")
91
- #endif
92
-
76
+ CallingxLog.voip.debug("didUpdatePushCredentials credentials.token = \(credentials.token, privacy: .private), type = \(type, privacy: .public)")
77
+
93
78
  let voipTokenLength = credentials.token.count
94
79
  if voipTokenLength == 0 {
95
80
  return
@@ -102,10 +87,8 @@ typealias RNVoipPushNotificationCompletion = () -> Void
102
87
  }
103
88
 
104
89
  @objc public static func didReceiveIncomingPushWithPayload(_ payload: PKPushPayload, forType type: String) {
105
- #if DEBUG
106
- NSLog("%@","[VoipNotificationsManager] didReceiveIncomingPushWithPayload payload.dictionaryPayload = \(payload.dictionaryPayload), type = \(type)")
107
- #endif
108
-
90
+ CallingxLog.voip.debug("didReceiveIncomingPushWithPayload payload.dictionaryPayload = \(payload.dictionaryPayload, privacy: .private), type = \(type, privacy: .public)")
91
+
109
92
  let dictionaryPayload: [String: Any] = Dictionary(uniqueKeysWithValues: payload.dictionaryPayload.map { (key, value) in
110
93
  (String(describing: key), value)
111
94
  })
@@ -118,9 +101,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
118
101
  @objc public func getInitialEvents() -> [[String: Any]] {
119
102
  var events: [[String: Any]] = []
120
103
  let action = {
121
- #if DEBUG
122
- NSLog("%@","[VoipNotificationsManager][getInitialEvents] delayedEvents = \(self.delayedEvents)")
123
- #endif
104
+ CallingxLog.voip.debugPublic("[getInitialEvents] delayedEvents = \(self.delayedEvents)")
124
105
 
125
106
  events = self.delayedEvents
126
107
  self.delayedEvents = []
@@ -147,9 +128,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
147
128
  }
148
129
 
149
130
  private func sendEventWithNameWrapper(name: String, body: [String: Any]?) {
150
- #if DEBUG
151
- NSLog("%@","[VoipNotificationsManager] sendEventWithNameWrapper: \(name)")
152
- #endif
131
+ CallingxLog.voip.debugPublic("sendEventWithNameWrapper: \(name)")
153
132
 
154
133
  let sendEventAction = {
155
134
  var dictionary: [String: Any] = ["eventName": name]
@@ -161,9 +140,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
161
140
  self.eventEmitter?.emitVoipEvent(dictionary)
162
141
  } else {
163
142
  self.delayedEvents.append(dictionary)
164
- #if DEBUG
165
- NSLog("%@","[VoipNotificationsManager] delayedEvents: \(self.delayedEvents)")
166
- #endif
143
+ CallingxLog.voip.debugPublic("delayedEvents: \(self.delayedEvents)")
167
144
  }
168
145
  }
169
146
 
@@ -0,0 +1,29 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <PushKit/PushKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface VoipPushHandler : NSObject <PKPushRegistryDelegate>
7
+
8
+ + (instancetype)sharedInstance;
9
+
10
+ /** Handles a legacy `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` callback. */
11
+ + (void)handleIncomingPush:(PKPushPayload *)payload
12
+ forType:(NSString *)type
13
+ completionHandler:(void (^_Nullable)(void))completion;
14
+
15
+ /**
16
+ * Handles an iOS 26.4+ `pushRegistry:didReceiveIncomingVoIPPushWithPayload:metadata:withCompletionHandler:`
17
+ * callback. Gated behind `__IPHONE_26_4` because `PKVoIPPushMetadata` only
18
+ * exists in the iOS 26.4 SDK; on older Xcode this declaration is omitted and
19
+ * PushKit dispatches to the legacy `handleIncomingPush:forType:` path instead.
20
+ */
21
+ #ifdef __IPHONE_26_4
22
+ + (void)handleIncomingVoIPPush:(PKPushPayload *)payload
23
+ metadata:(PKVoIPPushMetadata * _Nullable)metadata
24
+ completionHandler:(void (^_Nullable)(void))completion API_AVAILABLE(ios(26.4));
25
+ #endif
26
+
27
+ @end
28
+
29
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,195 @@
1
+ #import "VoipPushHandler.h"
2
+ #import "CallingxPublic.h"
3
+ #import <UIKit/UIKit.h>
4
+
5
+ // Import Swift generated header for VoipNotificationsManager
6
+ #if __has_include("Callingx-Swift.h")
7
+ #import "Callingx-Swift.h"
8
+ #else
9
+ #import <Callingx/Callingx-Swift.h>
10
+ #endif
11
+
12
+ static NSString *const DEFAULT_DISPLAY_NAME = @"Unknown Caller";
13
+
14
+ @interface Callingx (VoipPushHandlerInternal)
15
+ + (BOOL)shouldSkipIncomingPushInForeground;
16
+ @end
17
+
18
+ #pragma mark - Helpers
19
+
20
+ // applicationState must be read on the main thread (PushKit delivers on
21
+ // main, so the common path skips dispatch). Treat Inactive as foreground:
22
+ // covers brief transitions and system overlays.
23
+ static BOOL isAppInForeground(void) {
24
+ __block UIApplicationState state = UIApplicationStateActive;
25
+ void (^readState)(void) = ^{
26
+ state = [UIApplication sharedApplication].applicationState;
27
+ };
28
+ if ([NSThread isMainThread]) {
29
+ readState();
30
+ } else {
31
+ dispatch_sync(dispatch_get_main_queue(), readState);
32
+ }
33
+ return state != UIApplicationStateBackground;
34
+ }
35
+
36
+ // Extracts CallKit-display fields from the Stream payload and reports the
37
+ // incoming call via Callingx.
38
+ static void reportIncomingCallFromStreamPayload(NSDictionary *streamPayload,
39
+ void (^_Nullable completion)(void)) {
40
+ NSString *callCid = streamPayload[@"call_cid"];
41
+ NSString *callDisplayName = streamPayload[@"call_display_name"];
42
+ NSString *createdByDisplayName = streamPayload[@"created_by_display_name"];
43
+ NSString *createdCallerName = callDisplayName.length > 0 ? callDisplayName : createdByDisplayName;
44
+ NSString *localizedCallerName = createdCallerName.length > 0 ? createdCallerName : DEFAULT_DISPLAY_NAME;
45
+ NSString *createdById = streamPayload[@"created_by_id"];
46
+ NSString *handle = createdById.length > 0 ? createdById : localizedCallerName;
47
+ NSString *videoIncluded = streamPayload[@"video"];
48
+ BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES;
49
+
50
+ [Callingx reportNewIncomingCall:callCid
51
+ handle:handle
52
+ handleType:@"generic"
53
+ hasVideo:hasVideo
54
+ localizedCallerName:localizedCallerName
55
+ supportsHolding:NO
56
+ supportsDTMF:NO
57
+ supportsGrouping:NO
58
+ supportsUngrouping:NO
59
+ payload:streamPayload
60
+ withCompletionHandler:completion];
61
+ }
62
+
63
+ #pragma mark - Implementation
64
+
65
+ @implementation VoipPushHandler
66
+
67
+ + (instancetype)sharedInstance {
68
+ static VoipPushHandler *instance;
69
+ static dispatch_once_t onceToken;
70
+ dispatch_once(&onceToken, ^{
71
+ instance = [[self alloc] init];
72
+ });
73
+ return instance;
74
+ }
75
+
76
+ #pragma mark - Static orchestration
77
+
78
+ + (void)handleIncomingPush:(PKPushPayload *)payload
79
+ forType:(NSString *)type
80
+ completionHandler:(void (^_Nullable)(void))completion {
81
+ NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"];
82
+ if (!streamPayload) {
83
+ [CallingxLogBridge pushError:@"[handleIncomingPush] Stream payload not found"];
84
+ if (completion) {
85
+ completion();
86
+ }
87
+ return;
88
+ }
89
+
90
+ NSString *callCid = streamPayload[@"call_cid"];
91
+ if (!callCid) {
92
+ [CallingxLogBridge pushError:@"[handleIncomingPush] Missing required field: call_cid"];
93
+ if (completion) {
94
+ completion();
95
+ }
96
+ return;
97
+ }
98
+
99
+ if (![Callingx canRegisterCall]) {
100
+ if (completion) {
101
+ completion();
102
+ }
103
+ return;
104
+ }
105
+
106
+ reportIncomingCallFromStreamPayload(streamPayload, completion);
107
+ [VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
108
+ }
109
+
110
+ #pragma mark - PKPushRegistryDelegate (managed mode)
111
+
112
+ - (void)pushRegistry:(PKPushRegistry *)registry
113
+ didUpdatePushCredentials:(PKPushCredentials *)credentials
114
+ forType:(PKPushType)type {
115
+ [VoipNotificationsManager didUpdatePushCredentials:credentials forType:(NSString *)type];
116
+ }
117
+
118
+ - (void)pushRegistry:(PKPushRegistry *)registry
119
+ didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
120
+ forType:(PKPushType)type
121
+ withCompletionHandler:(void (^)(void))completion {
122
+ [VoipPushHandler handleIncomingPush:payload
123
+ forType:(NSString *)type
124
+ completionHandler:completion];
125
+ [CallingxLogBridge pushDebug:@"[pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:] completion"];
126
+ }
127
+
128
+ #ifdef __IPHONE_26_4
129
+ + (void)handleIncomingVoIPPush:(PKPushPayload *)payload
130
+ metadata:(PKVoIPPushMetadata * _Nullable)metadata
131
+ completionHandler:(void (^_Nullable)(void))completion {
132
+ NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"];
133
+ if (!streamPayload) {
134
+ [CallingxLogBridge pushError:@"[handleIncomingVoIPPush] Stream payload not found"];
135
+ if (completion) {
136
+ completion();
137
+ }
138
+ return;
139
+ }
140
+
141
+ NSString *callCid = streamPayload[@"call_cid"];
142
+ if (!callCid) {
143
+ [CallingxLogBridge pushError:@"[handleIncomingVoIPPush] Missing required field: call_cid"];
144
+ if (completion) {
145
+ completion();
146
+ }
147
+ return;
148
+ }
149
+
150
+ NSString *type = @"PKPushTypeVoIP";
151
+ BOOL mustReport = metadata ? metadata.mustReport : YES;
152
+
153
+ // Both skip paths require mustReport == NO; skipping while YES risks
154
+ // PushKit terminating the app.
155
+ if (!mustReport && ![Callingx canRegisterCall]) {
156
+ // Busy reject: drop without forwarding to JS.
157
+ if (completion) {
158
+ completion();
159
+ }
160
+ return;
161
+ }
162
+
163
+ if (!mustReport &&
164
+ [Callingx shouldSkipIncomingPushInForeground] &&
165
+ isAppInForeground()) {
166
+ // Foreground skip: hide CallKit, let JS render the ringing UI.
167
+ [VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
168
+ if (completion) {
169
+ completion();
170
+ }
171
+ return;
172
+ }
173
+
174
+ reportIncomingCallFromStreamPayload(streamPayload, completion);
175
+ [VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
176
+ }
177
+
178
+ // iOS 26.4 added a new VoIP push selector that carries a `PKVoIPPushMetadata`
179
+ // argument (notably `mustReport`). The type only exists in the iOS 26.4 SDK,
180
+ // so the `#ifdef __IPHONE_26_4` gate ensures this file still compiles on
181
+ // older Xcode versions — older Xcode simply doesn't emit this method, and
182
+ // PushKit on those builds dispatches to the legacy selector above.
183
+ - (void)pushRegistry:(PKPushRegistry *)registry
184
+ didReceiveIncomingVoIPPushWithPayload:(PKPushPayload *)payload
185
+ metadata:(PKVoIPPushMetadata *)metadata
186
+ withCompletionHandler:(void (^)(void))completion
187
+ API_AVAILABLE(ios(26.4)) {
188
+ [VoipPushHandler handleIncomingVoIPPush:payload
189
+ metadata:metadata
190
+ completionHandler:completion];
191
+ [CallingxLogBridge pushDebug:@"[pushRegistry:didReceiveIncomingVoIPPushWithPayload:metadata:withCompletionHandler:] completion"];
192
+ }
193
+ #endif
194
+
195
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/react-native-callingx",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "CallKit and Telecom API capabilities for React Native",
5
5
  "main": "./dist/module/index.js",
6
6
  "module": "./dist/module/index.js",
@@ -59,20 +59,20 @@
59
59
  "registry": "https://registry.npmjs.org/"
60
60
  },
61
61
  "devDependencies": {
62
- "@react-native-community/cli": "20.0.1",
63
- "@react-native/babel-preset": "0.81.5",
64
- "@stream-io/react-native-webrtc": "137.2.0",
65
- "@types/react": "^19.1.0",
62
+ "@react-native-community/cli": "20.1.3",
63
+ "@react-native/babel-preset": "0.85.3",
64
+ "@stream-io/react-native-webrtc": "145.0.0",
65
+ "@types/react": "^19.2.15",
66
66
  "del-cli": "^6.0.0",
67
- "react": "19.1.0",
68
- "react-native": "0.81.5",
69
- "react-native-builder-bob": "^0.40.15",
70
- "typescript": "^5.9.2"
67
+ "react": "19.2.3",
68
+ "react-native": "0.85.3",
69
+ "react-native-builder-bob": "^0.41.0",
70
+ "typescript": "^5.9.3"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@react-native-firebase/app": ">=23.0.0",
74
74
  "@react-native-firebase/messaging": ">=23.0.0",
75
- "@stream-io/react-native-webrtc": ">=137.1.2",
75
+ "@stream-io/react-native-webrtc": "^145.0.0",
76
76
  "react": "*",
77
77
  "react-native": "*"
78
78
  },
@@ -48,11 +48,14 @@ export interface Spec extends TurboModule {
48
48
  getInitialEvents(): Array<{
49
49
  eventName: string;
50
50
  params: {
51
- callId: string;
51
+ callId?: string;
52
52
  cause?: string;
53
53
  muted?: boolean;
54
54
  hold?: boolean;
55
55
  source?: string;
56
+ phase?: string;
57
+ reason?: string;
58
+ shouldResume?: boolean;
56
59
  };
57
60
  }>;
58
61
 
@@ -147,10 +150,14 @@ export interface Spec extends TurboModule {
147
150
  readonly onNewEvent: EventEmitter<{
148
151
  eventName: string;
149
152
  params: {
150
- callId: string;
153
+ callId?: string;
151
154
  cause?: string;
152
155
  muted?: boolean;
153
156
  hold?: boolean;
157
+ source?: string;
158
+ phase?: string;
159
+ reason?: string;
160
+ shouldResume?: boolean;
154
161
  };
155
162
  }>;
156
163
 
package/src/types.ts CHANGED
@@ -292,14 +292,12 @@ export type InfoDisplayOptions = {
292
292
  };
293
293
 
294
294
  export type EventData = {
295
- eventName: EventName;
296
- params: EventParams[EventName];
297
- };
295
+ [K in EventName]: { eventName: K; params: EventParams[K] };
296
+ }[EventName];
298
297
 
299
298
  export type VoipEventData = {
300
- eventName: VoipEventName;
301
- params: VoipEventParams[VoipEventName];
302
- };
299
+ [K in VoipEventName]: { eventName: K; params: VoipEventParams[K] };
300
+ }[VoipEventName];
303
301
 
304
302
  export type EventName =
305
303
  | 'answerCall'
@@ -307,11 +305,19 @@ export type EventName =
307
305
  | 'didDisplayIncomingCall'
308
306
  | 'didToggleHoldCallAction'
309
307
  | 'didChangeAudioRoute'
308
+ | 'didAudioInterruption'
310
309
  | 'didReceiveStartCallAction'
311
310
  | 'didPerformSetMutedCallAction'
312
311
  | 'didActivateAudioSession'
313
312
  | 'didDeactivateAudioSession';
314
313
 
314
+ export type IOSAudioInterruptionEvent = {
315
+ source: 'callingx';
316
+ phase: 'began' | 'ended';
317
+ reason?: 'default' | 'builtInMicMuted' | 'routeDisconnected' | (string & {});
318
+ shouldResume?: boolean;
319
+ };
320
+
315
321
  export type EventParams = {
316
322
  answerCall: {
317
323
  callId: string;
@@ -337,6 +343,7 @@ export type EventParams = {
337
343
  callId: string;
338
344
  output: string;
339
345
  };
346
+ didAudioInterruption: IOSAudioInterruptionEvent;
340
347
  didReceiveStartCallAction: {
341
348
  callId: string;
342
349
  };