rns-nativecall 0.8.6 → 0.8.8

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/CallModule.m CHANGED
@@ -4,15 +4,14 @@
4
4
  @interface CallModule ()
5
5
  @property (nonatomic, strong) NSString *pendingCallUuid;
6
6
  @property (nonatomic, strong) NSMutableDictionary *pendingEvents;
7
+ @property (nonatomic, assign) BOOL isCallActive; // Track state for handoff vs hangup
7
8
  @end
8
9
 
9
10
  @implementation CallModule
10
11
 
11
12
  RCT_EXPORT_MODULE();
12
13
 
13
- + (BOOL)requiresMainQueueSetup {
14
- return YES;
15
- }
14
+ + (BOOL)requiresMainQueueSetup { return YES; }
16
15
 
17
16
  - (NSArray<NSString *> *)supportedEvents {
18
17
  return @[ @"onCallAccepted", @"onCallRejected", @"onCallFailed" ];
@@ -24,6 +23,7 @@ RCT_EXPORT_MODULE();
24
23
  self.pendingEvents = [NSMutableDictionary new];
25
24
  NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
26
25
 
26
+ // Corrected initialization for modern iOS
27
27
  CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:appName];
28
28
  config.supportsVideo = YES;
29
29
  config.maximumCallGroups = 1;
@@ -33,34 +33,66 @@ RCT_EXPORT_MODULE();
33
33
 
34
34
  self.provider = [[CXProvider alloc] initWithConfiguration:config];
35
35
  [self.provider setDelegate:self queue:nil];
36
+ self.isCallActive = NO;
36
37
  }
37
38
  return self;
38
39
  }
39
40
 
40
- // MARK: - New Parity Methods (Synced with Android)
41
-
42
- RCT_EXPORT_METHOD(checkCallValidity:(NSString *)uuidString resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
43
- BOOL isCurrent = [uuidString isEqualToString:self.currentCallUUID.UUIDString];
44
- resolve(@{
45
- @"isValid": @(isCurrent),
46
- @"isCanceled": @(!isCurrent)
47
- });
41
+ /**
42
+ * Modern helper to find the active UIWindow without using deprecated .keyWindow or .windows
43
+ */
44
+ - (UIWindow *)getActiveWindow {
45
+ if (@available(iOS 13.0, *)) {
46
+ NSSet<UIScene *> *scenes = [[UIApplication sharedApplication] connectedScenes];
47
+ for (UIScene *scene in scenes) {
48
+ // We look for a foreground active window scene
49
+ if (scene.activationState == UISceneActivationStateForegroundActive &&
50
+ [scene isKindOfClass:[UIWindowScene class]]) {
51
+ UIWindowScene *windowScene = (UIWindowScene *)scene;
52
+
53
+ // On iOS 15+, we use windowScene.keyWindow if available,
54
+ // otherwise iterate the windows array of that scene
55
+ if (@available(iOS 15.0, *)) {
56
+ if (windowScene.keyWindow) return windowScene.keyWindow;
57
+ }
58
+
59
+ for (UIWindow *window in windowScene.windows) {
60
+ if (window.isKeyWindow) return window;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ // Fallback for older devices or edge cases where scene is not yet active
67
+ #pragma clang diagnostic push
68
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
69
+ return [UIApplication sharedApplication].keyWindow;
70
+ #pragma clang diagnostic pop
48
71
  }
49
72
 
50
- RCT_EXPORT_METHOD(getInitialCallData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
51
- NSMutableDictionary *result = [NSMutableDictionary new];
52
-
53
- if (self.pendingCallUuid) {
54
- result[@"default"] = @{@"callUuid": self.pendingCallUuid};
73
+ // MARK: - Unified Export Method
74
+
75
+ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
76
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
77
+ if (!uuid) return;
78
+
79
+ dispatch_async(dispatch_get_main_queue(), ^{
80
+ if (self.isCallActive) {
81
+ // REAL HANG UP
82
+ [self.provider reportCallWithUUID:uuid
83
+ endedAtDate:[NSDate date]
84
+ reason:CXCallEndedReasonRemoteEnded];
85
+ self.isCallActive = NO;
86
+ } else {
87
+ // HANDOFF (Hide the CallKit banner/UI)
88
+ [self.provider reportCallWithUUID:uuid
89
+ endedAtDate:[NSDate date]
90
+ reason:CXCallEndedReasonAnsweredElsewhere];
91
+ self.isCallActive = YES;
92
+ }
93
+ self.currentCallUUID = nil;
55
94
  self.pendingCallUuid = nil;
56
- }
57
-
58
- if (self.pendingEvents.count > 0) {
59
- [result addEntriesFromDictionary:self.pendingEvents];
60
- [self.pendingEvents removeAllObjects];
61
- }
62
-
63
- resolve(result.count > 0 ? result : [NSNull null]);
95
+ });
64
96
  }
65
97
 
66
98
  // MARK: - Core Logic
@@ -77,6 +109,7 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
77
109
  return;
78
110
  }
79
111
 
112
+ self.isCallActive = NO;
80
113
  self.currentCallUUID = uuid;
81
114
 
82
115
  AVAudioSession *session = [AVAudioSession sharedInstance];
@@ -90,44 +123,25 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
90
123
  update.hasVideo = [callType isEqualToString:@"video"];
91
124
 
92
125
  [self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
93
- if (error) {
94
- reject(@"CALL_ERROR", error.localizedDescription, error);
95
- } else {
96
- resolve(@YES);
97
- }
126
+ if (error) { reject(@"CALL_ERROR", error.localizedDescription, error); }
127
+ else { resolve(@YES); }
98
128
  }];
99
129
  }
100
130
 
101
- // Handles programmatic ending (e.g., via FCM "CANCEL")
102
- RCT_EXPORT_METHOD(reportRemoteEnded:(NSString *)uuidString reason:(NSInteger)reason) {
103
- NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
104
- if (!uuid) return;
105
-
106
- // Report to CallKit that the remote user ended the call
107
- [self.provider reportCallWithUUID:uuid
108
- endedAtDate:[NSDate date]
109
- reason:(CXCallEndedReason)reason];
110
-
111
- // Clear local tracking
112
- if ([uuid isEqual:self.currentCallUUID]) {
113
- self.currentCallUUID = nil;
131
+ RCT_EXPORT_METHOD(getInitialCallData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
132
+ NSMutableDictionary *result = [NSMutableDictionary new];
133
+ if (self.pendingCallUuid) {
134
+ result[@"default"] = @{@"callUuid": self.pendingCallUuid};
114
135
  self.pendingCallUuid = nil;
115
136
  }
116
- }
117
-
118
- RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
119
- {
120
- [self reportRemoteEnded:uuidString reason:CXCallEndedReasonRemoteEnded];
137
+ resolve(result.count > 0 ? result : [NSNull null]);
121
138
  }
122
139
 
123
140
  // MARK: - CXProviderDelegate
124
141
 
125
142
  - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
126
143
  AVAudioSession *session = [AVAudioSession sharedInstance];
127
- [session setCategory:AVAudioSessionCategoryPlayAndRecord
128
- mode:AVAudioSessionModeVoiceChat
129
- options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker
130
- error:nil];
144
+ [session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker error:nil];
131
145
  [session setActive:YES error:nil];
132
146
 
133
147
  [action fulfill];
@@ -135,15 +149,12 @@ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
135
149
  NSString *uuidStr = [action.callUUID.UUIDString lowercaseString];
136
150
  self.pendingCallUuid = uuidStr;
137
151
 
138
- // Switch from system UI to App UI
139
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
140
- [self.provider reportCallWithUUID:action.callUUID
141
- endedAtDate:[NSDate date]
142
- reason:CXCallEndedReasonAnsweredElsewhere];
143
-
144
- dispatch_async(dispatch_get_main_queue(), ^{
145
- [[[UIApplication sharedApplication] keyWindow] makeKeyAndVisible];
146
- });
152
+ dispatch_async(dispatch_get_main_queue(), ^{
153
+ // Use our non-deprecated helper to bring app to front
154
+ UIWindow *window = [self getActiveWindow];
155
+ if (window) {
156
+ [window makeKeyAndVisible];
157
+ }
147
158
 
148
159
  [self sendEventWithName:@"onCallAccepted" body:@{@"callUuid": uuidStr}];
149
160
  });
@@ -151,15 +162,12 @@ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
151
162
 
152
163
  - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
153
164
  [action fulfill];
154
- self.currentCallUUID = nil;
155
- self.pendingCallUuid = nil;
165
+ self.isCallActive = NO;
156
166
  [self sendEventWithName:@"onCallRejected" body:@{@"callUuid": [action.callUUID.UUIDString lowercaseString]}];
157
167
  }
158
168
 
159
169
  - (void)providerDidReset:(CXProvider *)provider {
160
- [[AVAudioSession sharedInstance] setActive:NO error:nil];
161
- self.currentCallUUID = nil;
162
- self.pendingCallUuid = nil;
170
+ self.isCallActive = NO;
163
171
  }
164
172
 
165
- @end
173
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "High-performance React Native module for handling native VoIP call UI on Android and iOS.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,5 @@
1
- const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
1
+ const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity, withAppDelegate } = require('@expo/config-plugins');
2
2
 
3
- /** 1. ANDROID MAIN ACTIVITY MOD **/
4
3
  function withMainActivityDataFix(config) {
5
4
  return withMainActivity(config, (config) => {
6
5
  let contents = config.modResults.contents;
@@ -70,7 +69,6 @@ function withMainActivityDataFix(config) {
70
69
  return config;
71
70
  });
72
71
  }
73
-
74
72
  /** 2. ANDROID MANIFEST CONFIG **/
75
73
  function withAndroidConfig(config) {
76
74
  return withAndroidManifest(config, (config) => {
@@ -154,32 +152,44 @@ function withAndroidConfig(config) {
154
152
  return config;
155
153
  });
156
154
  }
155
+ /** 2. IOS APP DELEGATE MOD (The fix for Lock Screen Answer) **/
156
+ function withIosAppDelegateMod(config) {
157
+ return withAppDelegate(config, (config) => {
158
+ let contents = config.modResults.contents;
157
159
 
158
- /** 3. IOS CONFIG **/
159
- // Notice we accept (config, props) directly
160
- function withIosConfig(config, props = {}) {
161
- const iosBackgroundAudio = props.iosBackgroundAudio;
160
+ // 1. Add the Import if it doesn't exist
161
+ if (!contents.includes('#import <React/RCTLinkingManager.h>')) {
162
+ contents = '#import <React/RCTLinkingManager.h>\n' + contents;
163
+ }
162
164
 
163
- console.log(`[rns-nativecall] iosBackgroundAudio setting:`, iosBackgroundAudio);
165
+ // 2. The code to inject
166
+ const linkingCode = `
167
+ - (BOOL)application:(UIApplication *)application
168
+ continueUserActivity:(NSUserActivity *)userActivity
169
+ restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
170
+ {
171
+ return [RCTLinkingManager application:application
172
+ continueUserActivity:userActivity
173
+ restorationHandler:restorationHandler];
174
+ }
175
+ `;
176
+
177
+ // 3. Inject before the final @end if not already present
178
+ if (!contents.includes('continueUserActivity')) {
179
+ contents = contents.replace(/@end\s*$/, `${linkingCode}\n@end`);
180
+ }
164
181
 
182
+ config.modResults.contents = contents;
183
+ return config;
184
+ });
185
+ }
186
+
187
+ /** 3. IOS INFO.PLIST CONFIG (Existing) **/
188
+ function withIosConfig(config, props = {}) {
165
189
  return withInfoPlist(config, (config) => {
166
190
  const infoPlist = config.modResults;
167
191
  if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
168
192
 
169
- // Explicit check: only disable if strictly false
170
- const enableAudio = iosBackgroundAudio !== false;
171
-
172
- // if (enableAudio) {
173
- // if (!infoPlist.UIBackgroundModes.includes('audio')) {
174
- // infoPlist.UIBackgroundModes.push('audio');
175
- // }
176
- // } else {
177
- // infoPlist.UIBackgroundModes = infoPlist.UIBackgroundModes.filter(
178
- // (mode) => mode !== 'audio'
179
- // );
180
- // console.log(`[rns-nativecall] 'audio' background mode removed.`);
181
- // }
182
-
183
193
  ['voip', 'remote-notification'].forEach(mode => {
184
194
  if (!infoPlist.UIBackgroundModes.includes(mode)) {
185
195
  infoPlist.UIBackgroundModes.push(mode);
@@ -195,7 +205,7 @@ module.exports = (config, props) => {
195
205
  return withPlugins(config, [
196
206
  withAndroidConfig,
197
207
  withMainActivityDataFix,
198
- // Standard way to pass props to a plugin function
208
+ withIosAppDelegateMod, // <--- ADDED THIS HERE
199
209
  [withIosConfig, props]
200
210
  ]);
201
211
  };