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 +74 -73
- package/package.json +1 -1
- package/withNativeCallVoip.js +40 -24
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,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
|
-
|
|
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(
|
|
103
|
-
|
|
104
|
-
if (
|
|
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
|
-
|
|
127
|
-
[session
|
|
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.
|
|
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
|
-
|
|
168
|
-
self.currentCallUUID = nil;
|
|
169
|
-
self.pendingCallUuid = nil;
|
|
170
|
+
self.isCallActive = NO;
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
@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,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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
214
|
+
withIosAppDelegateMod, // <--- ADDED THIS HERE
|
|
199
215
|
[withIosConfig, props]
|
|
200
216
|
]);
|
|
201
217
|
};
|