rns-nativecall 0.8.7 → 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,9 +109,9 @@ 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
- // Pre-configure audio session
83
115
  AVAudioSession *session = [AVAudioSession sharedInstance];
84
116
  [session setCategory:AVAudioSessionCategoryPlayAndRecord
85
117
  mode:AVAudioSessionModeVoiceChat
@@ -91,82 +123,51 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
91
123
  update.hasVideo = [callType isEqualToString:@"video"];
92
124
 
93
125
  [self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
94
- if (error) {
95
- reject(@"CALL_ERROR", error.localizedDescription, error);
96
- } else {
97
- resolve(@YES);
98
- }
126
+ if (error) { reject(@"CALL_ERROR", error.localizedDescription, error); }
127
+ else { resolve(@YES); }
99
128
  }];
100
129
  }
101
130
 
102
- RCT_EXPORT_METHOD(reportRemoteEnded:(NSString *)uuidString reason:(NSInteger)reason) {
103
- NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
104
- if (!uuid) return;
105
-
106
- [self.provider reportCallWithUUID:uuid
107
- endedAtDate:[NSDate date]
108
- reason:(CXCallEndedReason)reason];
109
-
110
- if ([uuid isEqual:self.currentCallUUID]) {
111
- 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};
112
135
  self.pendingCallUuid = nil;
113
136
  }
114
- }
115
-
116
- RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
117
- {
118
- [self reportRemoteEnded:uuidString reason:CXCallEndedReasonRemoteEnded];
137
+ resolve(result.count > 0 ? result : [NSNull null]);
119
138
  }
120
139
 
121
140
  // MARK: - CXProviderDelegate
122
141
 
123
142
  - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
124
- // 1. Force the Audio Session active immediately so the system hands off the hardware
125
143
  AVAudioSession *session = [AVAudioSession sharedInstance];
126
- NSError *audioError = nil;
127
- [session setCategory:AVAudioSessionCategoryPlayAndRecord
128
- mode:AVAudioSessionModeVoiceChat
129
- options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker
130
- error:nil];
131
- [session setActive:YES error:&audioError];
144
+ [session setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options:AVAudioSessionCategoryOptionAllowBluetoothHFP | AVAudioSessionCategoryOptionDefaultToSpeaker error:nil];
145
+ [session setActive:YES error:nil];
132
146
 
133
- // 2. Fulfill the system action
134
147
  [action fulfill];
135
148
 
136
149
  NSString *uuidStr = [action.callUUID.UUIDString lowercaseString];
137
150
  self.pendingCallUuid = uuidStr;
138
151
 
139
- // 3. Move to Main Queue for UI Handoff
140
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
+ }
141
158
 
142
- // This is the trigger that "destroys" the CallKit UI without failing the call
143
- // It tells iOS the call is being handled by another device/interface (the app)
144
- [self.provider reportCallWithUUID:action.callUUID
145
- endedAtDate:[NSDate date]
146
- reason:CXCallEndedReasonAnsweredElsewhere];
147
-
148
- // Ensure the app window is visible/active
149
- [[[UIApplication sharedApplication] keyWindow] makeKeyAndVisible];
150
-
151
- // Notify React Native that the call is accepted
152
159
  [self sendEventWithName:@"onCallAccepted" body:@{@"callUuid": uuidStr}];
153
-
154
- // Clear local tracking so displayIncomingCall can be used again
155
- self.currentCallUUID = nil;
156
160
  });
157
161
  }
158
162
 
159
163
  - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
160
164
  [action fulfill];
161
- self.currentCallUUID = nil;
162
- self.pendingCallUuid = nil;
165
+ self.isCallActive = NO;
163
166
  [self sendEventWithName:@"onCallRejected" body:@{@"callUuid": [action.callUUID.UUIDString lowercaseString]}];
164
167
  }
165
168
 
166
169
  - (void)providerDidReset:(CXProvider *)provider {
167
- // Important: Do NOT deactivate audio if a call is transitioning to the app
168
- self.currentCallUUID = nil;
169
- self.pendingCallUuid = nil;
170
+ self.isCallActive = NO;
170
171
  }
171
172
 
172
- @end
173
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.8.7",
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
  };