rns-nativecall 0.8.7 → 0.8.9

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.9",
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,51 @@ 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. Check for the import
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. Only inject if the method name doesn't exist AT ALL
166
+ // This prevents the "Duplicate Method" error
167
+ if (!contents.includes('continueUserActivity')) {
168
+ const linkingCode = `
169
+ - (BOOL)application:(UIApplication *)application
170
+ continueUserActivity:(NSUserActivity *)userActivity
171
+ restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
172
+ {
173
+ return [RCTLinkingManager application:application
174
+ continueUserActivity:userActivity
175
+ restorationHandler:restorationHandler];
176
+ }
177
+ `;
178
+ // Inject before the LAST @end in the file
179
+ const parts = contents.split('@end');
180
+ const lastPart = parts.pop();
181
+ contents = parts.join('@end') + linkingCode + '\n@end' + lastPart;
182
+ } else {
183
+ // 3. If it DOES exist, ensure RCTLinkingManager is inside it
184
+ // Many Expo apps have an empty continueUserActivity or one that only handles notifications
185
+ if (!contents.includes('RCTLinkingManager')) {
186
+ console.warn("[rns-nativecall] continueUserActivity exists but doesn't have RCTLinkingManager. Manual check required.");
187
+ }
188
+ }
164
189
 
190
+ config.modResults.contents = contents;
191
+ return config;
192
+ });
193
+ }
194
+ /** 3. IOS INFO.PLIST CONFIG (Existing) **/
195
+ function withIosConfig(config, props = {}) {
165
196
  return withInfoPlist(config, (config) => {
166
197
  const infoPlist = config.modResults;
167
198
  if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
168
199
 
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
200
  ['voip', 'remote-notification'].forEach(mode => {
184
201
  if (!infoPlist.UIBackgroundModes.includes(mode)) {
185
202
  infoPlist.UIBackgroundModes.push(mode);
@@ -189,13 +206,12 @@ function withIosConfig(config, props = {}) {
189
206
  return config;
190
207
  });
191
208
  }
192
-
193
209
  // Main Plugin Entry
194
210
  module.exports = (config, props) => {
195
211
  return withPlugins(config, [
196
212
  withAndroidConfig,
197
213
  withMainActivityDataFix,
198
- // Standard way to pass props to a plugin function
214
+ withIosAppDelegateMod, // <--- ADDED THIS HERE
199
215
  [withIosConfig, props]
200
216
  ]);
201
217
  };