@stream-io/react-native-callingx 0.3.1 → 0.4.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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [0.4.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/react-native-callingx-0.3.1...@stream-io/react-native-callingx-0.4.0) (2026-06-04)
6
+
7
+ ### Features
8
+
9
+ - added self managed push kit delegate management ([#2263](https://github.com/GetStream/stream-video-js/issues/2263)) ([ede4671](https://github.com/GetStream/stream-video-js/commit/ede467138a4727ccdc5cf3702b16747c516775a5))
10
+
11
+ ### Bug Fixes
12
+
13
+ - skip notification bg->fg transition case ([#2262](https://github.com/GetStream/stream-video-js/issues/2262)) ([e5cd46f](https://github.com/GetStream/stream-video-js/commit/e5cd46fa557d83f3de1c983d1aca2adfac9ad0ee))
14
+
5
15
  ## [0.3.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/react-native-callingx-0.3.0...@stream-io/react-native-callingx-0.3.1) (2026-05-26)
6
16
 
7
17
  ### Bug Fixes
package/Callingx.podspec CHANGED
@@ -14,7 +14,7 @@ Pod::Spec.new do |s|
14
14
  s.source = { :git => "https://github.com/GetStream/stream-video-js/tree/main/packages/react-native-callingx.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
17
- s.public_header_files = "ios/CallingxPublic.h"
17
+ s.public_header_files = "ios/CallingxPublic.h", "ios/VoipPushHandler.h"
18
18
  s.swift_version = "5.0"
19
19
 
20
20
  s.dependency "stream-react-native-webrtc"
@@ -20,6 +20,7 @@ import io.getstream.rn.callingx.debugLog
20
20
  import io.getstream.rn.callingx.getDisconnectCauseString
21
21
  import io.getstream.rn.callingx.model.Call
22
22
  import io.getstream.rn.callingx.repo.CallRepository
23
+ import io.getstream.rn.callingx.utils.LifecycleListener
23
24
  import io.getstream.rn.callingx.utils.SettingsStore
24
25
  import androidx.core.graphics.toColorInt
25
26
 
@@ -303,8 +304,24 @@ class CallNotificationManager(
303
304
  notificationsState[callId] = current.copy(optimisticState = OptimisticState.NONE, lastSnapshot = null)
304
305
  }
305
306
 
307
+ // when skipIncomingPushInForeground is true, we use the ongoing channel
308
+ // for the notification to avoid notification overlapping app ui, just for UX purposes
309
+ private fun shouldShowAsIncoming(
310
+ call: Call.Registered,
311
+ optimisticState: OptimisticState
312
+ ): Boolean {
313
+ val isIncoming =
314
+ call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE
315
+ if (!isIncoming) return false
316
+
317
+ val skipInForeground =
318
+ SettingsStore.shouldSkipIncomingPushInForeground(context) &&
319
+ LifecycleListener.isInForeground
320
+ return !skipInForeground
321
+ }
322
+
306
323
  private fun getChannelId(call: Call.Registered, optimisticState: OptimisticState): String {
307
- return if (call.isIncoming() && !call.isActive && optimisticState == OptimisticState.NONE) {
324
+ return if (shouldShowAsIncoming(call, optimisticState)) {
308
325
  notificationsConfig.incomingChannel.id
309
326
  } else {
310
327
  notificationsConfig.ongoingChannel.id
package/ios/Callingx.mm CHANGED
@@ -38,6 +38,8 @@
38
38
  CallingxImpl *_moduleImpl;
39
39
  }
40
40
 
41
+ @synthesize moduleRegistry = _moduleRegistry;
42
+
41
43
  #pragma mark - Singleton
42
44
 
43
45
  + (id)allocWithZone:(NSZone *)zone {
@@ -161,10 +163,10 @@ RCT_EXPORT_MODULE(Callingx)
161
163
  - (void)_setupiOSWithOptions:(NSDictionary *)optionsDict {
162
164
  [_moduleImpl setupWithOptions:optionsDict];
163
165
 
164
- // Inject WebRTCModule so CallingxImpl can access AudioDeviceModule.
165
- // self.bridge is NOT available on TurboModules use currentBridge instead,
166
- // which returns the real RCTBridge or RCTBridgeProxy (bridgeless interop).
167
- WebRTCModule *webrtcModule = [[RCTBridge currentBridge] moduleForName:@"WebRTCModule"];
166
+ // Resolve WebRTCModule via the injected RCTModuleRegistry so CallingxImpl can
167
+ // access its AudioDeviceModule. Works on both old and new arch, including
168
+ // bridgeless (where [RCTBridge currentBridge] is a stub that returns nil).
169
+ WebRTCModule *webrtcModule = [self.moduleRegistry moduleForName:"WebRTCModule"];
168
170
  _moduleImpl.webRTCModule = webrtcModule;
169
171
 
170
172
  self.callKeepCallController = _moduleImpl.callKeepCallController;
@@ -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
 
@@ -60,28 +60,19 @@ typealias RNVoipPushNotificationCompletion = () -> Void
60
60
  #endif
61
61
  let voipPushManager = VoipNotificationsManager.shared()
62
62
  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
- }
63
+ return
64
+ }
65
+
66
+ #if DEBUG
67
+ NSLog("%@","[VoipNotificationsManager] voipRegistration enter")
68
+ #endif
69
+
70
+ DispatchQueue.main.async {
71
+ let registry = PKPushRegistry(queue: DispatchQueue.main)
72
+ registry.delegate = VoipPushHandler.sharedInstance()
73
+ registry.desiredPushTypes = [.voIP]
74
+ VoipNotificationsManager.voipRegistry = registry
75
+ isVoipRegistered = true
85
76
  }
86
77
  }
87
78
 
@@ -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,203 @@
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
+ #if DEBUG
84
+ NSLog(@"[VoipPushHandler][handleIncomingPush] Stream payload not found");
85
+ #endif
86
+ if (completion) {
87
+ completion();
88
+ }
89
+ return;
90
+ }
91
+
92
+ NSString *callCid = streamPayload[@"call_cid"];
93
+ if (!callCid) {
94
+ #if DEBUG
95
+ NSLog(@"[VoipPushHandler][handleIncomingPush] Missing required field: call_cid");
96
+ #endif
97
+ if (completion) {
98
+ completion();
99
+ }
100
+ return;
101
+ }
102
+
103
+ if (![Callingx canRegisterCall]) {
104
+ if (completion) {
105
+ completion();
106
+ }
107
+ return;
108
+ }
109
+
110
+ reportIncomingCallFromStreamPayload(streamPayload, completion);
111
+ [VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
112
+ }
113
+
114
+ #pragma mark - PKPushRegistryDelegate (managed mode)
115
+
116
+ - (void)pushRegistry:(PKPushRegistry *)registry
117
+ didUpdatePushCredentials:(PKPushCredentials *)credentials
118
+ forType:(PKPushType)type {
119
+ [VoipNotificationsManager didUpdatePushCredentials:credentials forType:(NSString *)type];
120
+ }
121
+
122
+ - (void)pushRegistry:(PKPushRegistry *)registry
123
+ didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
124
+ forType:(PKPushType)type
125
+ withCompletionHandler:(void (^)(void))completion {
126
+ [VoipPushHandler handleIncomingPush:payload
127
+ forType:(NSString *)type
128
+ completionHandler:completion];
129
+ NSLog(@"[VoipPushHandler][pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:] completion");
130
+ }
131
+
132
+ #ifdef __IPHONE_26_4
133
+ + (void)handleIncomingVoIPPush:(PKPushPayload *)payload
134
+ metadata:(PKVoIPPushMetadata * _Nullable)metadata
135
+ completionHandler:(void (^_Nullable)(void))completion {
136
+ NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"];
137
+ if (!streamPayload) {
138
+ #if DEBUG
139
+ NSLog(@"[VoipPushHandler][handleIncomingVoIPPush] Stream payload not found");
140
+ #endif
141
+ if (completion) {
142
+ completion();
143
+ }
144
+ return;
145
+ }
146
+
147
+ NSString *callCid = streamPayload[@"call_cid"];
148
+ if (!callCid) {
149
+ #if DEBUG
150
+ NSLog(@"[VoipPushHandler][handleIncomingVoIPPush] Missing required field: call_cid");
151
+ #endif
152
+ if (completion) {
153
+ completion();
154
+ }
155
+ return;
156
+ }
157
+
158
+ NSString *type = @"PKPushTypeVoIP";
159
+ BOOL mustReport = metadata ? metadata.mustReport : YES;
160
+
161
+ // Both skip paths require mustReport == NO; skipping while YES risks
162
+ // PushKit terminating the app.
163
+ if (!mustReport && ![Callingx canRegisterCall]) {
164
+ // Busy reject: drop without forwarding to JS.
165
+ if (completion) {
166
+ completion();
167
+ }
168
+ return;
169
+ }
170
+
171
+ if (!mustReport &&
172
+ [Callingx shouldSkipIncomingPushInForeground] &&
173
+ isAppInForeground()) {
174
+ // Foreground skip: hide CallKit, let JS render the ringing UI.
175
+ [VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
176
+ if (completion) {
177
+ completion();
178
+ }
179
+ return;
180
+ }
181
+
182
+ reportIncomingCallFromStreamPayload(streamPayload, completion);
183
+ [VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
184
+ }
185
+
186
+ // iOS 26.4 added a new VoIP push selector that carries a `PKVoIPPushMetadata`
187
+ // argument (notably `mustReport`). The type only exists in the iOS 26.4 SDK,
188
+ // so the `#ifdef __IPHONE_26_4` gate ensures this file still compiles on
189
+ // older Xcode versions — older Xcode simply doesn't emit this method, and
190
+ // PushKit on those builds dispatches to the legacy selector above.
191
+ - (void)pushRegistry:(PKPushRegistry *)registry
192
+ didReceiveIncomingVoIPPushWithPayload:(PKPushPayload *)payload
193
+ metadata:(PKVoIPPushMetadata *)metadata
194
+ withCompletionHandler:(void (^)(void))completion
195
+ API_AVAILABLE(ios(26.4)) {
196
+ [VoipPushHandler handleIncomingVoIPPush:payload
197
+ metadata:metadata
198
+ completionHandler:completion];
199
+ NSLog(@"[VoipPushHandler][pushRegistry:didReceiveIncomingVoIPPushWithPayload:metadata:withCompletionHandler:] completion");
200
+ }
201
+ #endif
202
+
203
+ @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.4.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,15 +59,15 @@
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",
62
+ "@react-native-community/cli": "20.1.3",
63
+ "@react-native/babel-preset": "0.85.3",
64
64
  "@stream-io/react-native-webrtc": "137.2.0",
65
- "@types/react": "^19.1.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",