@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 +10 -0
- package/Callingx.podspec +1 -1
- package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt +18 -1
- package/ios/Callingx.mm +6 -4
- package/ios/VoipNotificationsManager.swift +14 -23
- package/ios/VoipPushHandler.h +29 -0
- package/ios/VoipPushHandler.m +203 -0
- package/package.json +8 -8
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"
|
package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt
CHANGED
|
@@ -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 (
|
|
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
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
WebRTCModule *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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
"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.
|
|
63
|
-
"@react-native/babel-preset": "0.
|
|
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.
|
|
65
|
+
"@types/react": "^19.2.15",
|
|
66
66
|
"del-cli": "^6.0.0",
|
|
67
|
-
"react": "19.
|
|
68
|
-
"react-native": "0.
|
|
69
|
-
"react-native-builder-bob": "^0.
|
|
70
|
-
"typescript": "^5.9.
|
|
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",
|