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 +74 -66
- package/package.json +1 -1
- package/withNativeCallVoip.js +33 -23
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
[self
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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
|
-
|
|
161
|
-
self.currentCallUUID = nil;
|
|
162
|
-
self.pendingCallUuid = nil;
|
|
170
|
+
self.isCallActive = NO;
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
@end
|
|
173
|
+
@end
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
208
|
+
withIosAppDelegateMod, // <--- ADDED THIS HERE
|
|
199
209
|
[withIosConfig, props]
|
|
200
210
|
]);
|
|
201
211
|
};
|