rns-nativecall 0.0.4 → 0.0.6
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/android/src/main/java/com/rnsnativecall/CallModule.kt +22 -15
- package/index.d.ts +17 -10
- package/index.js +30 -48
- package/ios/CallModule.m +20 -13
- package/package.json +2 -2
- package/withRaiidrVoip.js +5 -6
|
@@ -22,20 +22,28 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
22
22
|
|
|
23
23
|
override fun getName() = "CallModule"
|
|
24
24
|
|
|
25
|
+
private fun getPhoneAccountHandle(): PhoneAccountHandle {
|
|
26
|
+
val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
|
|
27
|
+
// Using the app's package name for the ID makes it unique for every app using this package
|
|
28
|
+
return PhoneAccountHandle(componentName, "${reactApplicationContext.packageName}.voip")
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
private fun registerPhoneAccount() {
|
|
26
32
|
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
27
|
-
val
|
|
28
|
-
val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
|
|
33
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
|
|
35
|
+
// Dynamically get the app's display name from the Android Manifest
|
|
36
|
+
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Correct bitmasking: Combine all capabilities into a single integer
|
|
39
|
+
val capabilities = PhoneAccount.CAPABILITY_SELF_MANAGED or
|
|
40
|
+
PhoneAccount.CAPABILITY_CALL_PROVIDER or
|
|
41
|
+
PhoneAccount.CAPABILITY_VIDEO_CALLING or
|
|
42
|
+
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
|
|
43
|
+
|
|
44
|
+
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, appName)
|
|
45
|
+
.setCapabilities(capabilities)
|
|
46
|
+
.build()
|
|
39
47
|
|
|
40
48
|
telecomManager.registerPhoneAccount(phoneAccount)
|
|
41
49
|
}
|
|
@@ -50,8 +58,7 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
50
58
|
promise: Promise
|
|
51
59
|
) {
|
|
52
60
|
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
53
|
-
val
|
|
54
|
-
val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
|
|
61
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
55
62
|
|
|
56
63
|
val extras = Bundle().apply {
|
|
57
64
|
putParcelable(
|
|
@@ -85,13 +92,13 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
85
92
|
@ReactMethod
|
|
86
93
|
fun checkTelecomPermissions(promise: Promise) {
|
|
87
94
|
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
88
|
-
val
|
|
89
|
-
val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
|
|
95
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
90
96
|
|
|
91
97
|
val account = telecomManager.getPhoneAccount(phoneAccountHandle)
|
|
92
98
|
if (account != null && account.isEnabled) {
|
|
93
99
|
promise.resolve(true)
|
|
94
100
|
} else {
|
|
101
|
+
// Opens the system settings for "Calling Accounts" so the user can enable the app
|
|
95
102
|
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
96
103
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
97
104
|
reactApplicationContext.startActivity(intent)
|
|
@@ -114,4 +121,4 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
114
121
|
?.emit(eventName, params)
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
|
-
}
|
|
124
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
///Users/bush/Desktop/Apps/Raiidr/package/index.d.ts
|
|
2
|
+
|
|
2
3
|
export interface CallData {
|
|
3
4
|
callUUID: string;
|
|
4
5
|
}
|
|
@@ -7,14 +8,20 @@ export type CallAcceptedCallback = (data: CallData) => void;
|
|
|
7
8
|
export type CallRejectedCallback = (data: CallData) => void;
|
|
8
9
|
export type CallFailedCallback = (data: CallData) => void;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Manually request/check Android permissions for Telecom.
|
|
13
|
+
* @returns Promise resolving to true if granted
|
|
14
|
+
*/
|
|
15
|
+
export function ensureAndroidPermissions(): Promise<boolean>;
|
|
16
|
+
|
|
10
17
|
export interface CallHandlerType {
|
|
11
18
|
/**
|
|
12
|
-
* Display an incoming call UI.
|
|
19
|
+
* Display an incoming call UI using ConnectionService (Android) or CallKit (iOS).
|
|
13
20
|
* @param uuid Unique call identifier
|
|
14
|
-
* @param number Caller number
|
|
21
|
+
* @param number Caller number (or URI)
|
|
15
22
|
* @param name Caller display name
|
|
16
|
-
* @param hasVideo True if video call
|
|
17
|
-
* @param shouldRing True to play native ringtone
|
|
23
|
+
* @param hasVideo True if video call (default: false)
|
|
24
|
+
* @param shouldRing True to play native ringtone (default: true)
|
|
18
25
|
* @returns Promise resolving to true if successfully displayed
|
|
19
26
|
*/
|
|
20
27
|
displayCall(
|
|
@@ -26,16 +33,16 @@ export interface CallHandlerType {
|
|
|
26
33
|
): Promise<boolean>;
|
|
27
34
|
|
|
28
35
|
/**
|
|
29
|
-
* Dismiss native call UI
|
|
36
|
+
* Dismiss the native call UI (e.g., if the caller hangs up before the user answers).
|
|
30
37
|
* @param uuid Call identifier
|
|
31
38
|
*/
|
|
32
39
|
destroyNativeCallUI(uuid: string): void;
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
|
-
* Subscribe to call events
|
|
36
|
-
* @param onAccept Callback
|
|
37
|
-
* @param onReject Callback
|
|
38
|
-
* @param onFailed Optional callback for
|
|
42
|
+
* Subscribe to call events from the native side.
|
|
43
|
+
* @param onAccept Callback when user presses Answer
|
|
44
|
+
* @param onReject Callback when user presses Decline
|
|
45
|
+
* @param onFailed Optional callback for system-level failures
|
|
39
46
|
* @returns Function to unsubscribe all listeners
|
|
40
47
|
*/
|
|
41
48
|
subscribe(
|
|
@@ -47,4 +54,4 @@ export interface CallHandlerType {
|
|
|
47
54
|
|
|
48
55
|
declare const CallHandler: CallHandlerType;
|
|
49
56
|
|
|
50
|
-
export default CallHandler;
|
|
57
|
+
export default CallHandler;
|
package/index.js
CHANGED
|
@@ -9,6 +9,15 @@ import {
|
|
|
9
9
|
} from 'react-native';
|
|
10
10
|
|
|
11
11
|
const { CallModule } = NativeModules;
|
|
12
|
+
|
|
13
|
+
// Safety check for the Native Module
|
|
14
|
+
if (!CallModule && __DEV__) {
|
|
15
|
+
console.warn(
|
|
16
|
+
"rns-nativecall: NativeModule.CallModule is undefined. " +
|
|
17
|
+
"Make sure you have rebuilt your native project (npx expo run:android / ios)."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
const callEventEmitter = CallModule ? new NativeEventEmitter(CallModule) : null;
|
|
13
22
|
|
|
14
23
|
const REQUIRED_PERMISSIONS = Platform.OS === 'android' ? [
|
|
@@ -19,84 +28,57 @@ const REQUIRED_PERMISSIONS = Platform.OS === 'android' ? [
|
|
|
19
28
|
export async function ensureAndroidPermissions() {
|
|
20
29
|
if (Platform.OS !== 'android') return true;
|
|
21
30
|
const result = await PermissionsAndroid.requestMultiple(REQUIRED_PERMISSIONS);
|
|
22
|
-
|
|
23
|
-
.filter(([_, status]) => status !== PermissionsAndroid.RESULTS.GRANTED)
|
|
24
|
-
.map(([perm, _]) => perm);
|
|
25
|
-
|
|
26
|
-
if (deniedPermissions.length > 0) {
|
|
27
|
-
Alert.alert(
|
|
28
|
-
'Permissions Required',
|
|
29
|
-
`The following permissions are still missing: \n${deniedPermissions.join('\n')}`,
|
|
30
|
-
[{ text: 'Open Settings', onPress: () => Linking.openSettings() }]
|
|
31
|
-
);
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return true;
|
|
31
|
+
return Object.values(result).every(status => status === PermissionsAndroid.RESULTS.GRANTED);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
export const CallHandler = {
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {string} uuid
|
|
41
|
-
* @param {string} number
|
|
42
|
-
* @param {string} name
|
|
43
|
-
* @param {boolean} hasVideo
|
|
44
|
-
* @param {boolean} shouldRing - NEW: Controls native ringtone/vibration
|
|
45
|
-
*/
|
|
46
35
|
displayCall: async (uuid, number, name, hasVideo = false, shouldRing = true) => {
|
|
47
|
-
if (!CallModule)
|
|
48
|
-
console.log("CallModule is null. Check native linking.");
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
36
|
+
if (!CallModule) return false;
|
|
51
37
|
|
|
52
|
-
// 1. Android Specific Checks
|
|
53
38
|
if (Platform.OS === 'android') {
|
|
54
39
|
const hasPerms = await ensureAndroidPermissions();
|
|
55
40
|
if (!hasPerms) return false;
|
|
56
41
|
|
|
42
|
+
// This triggers the "Calling Accounts" system settings if not enabled
|
|
57
43
|
const isAccountEnabled = await CallModule.checkTelecomPermissions();
|
|
58
44
|
if (!isAccountEnabled) return false;
|
|
59
45
|
}
|
|
60
46
|
|
|
61
|
-
const cleanUuid = uuid.toLowerCase().trim();
|
|
62
|
-
|
|
63
|
-
// 2. Cross-platform execution
|
|
64
47
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const success = await CallModule.displayIncomingCall(
|
|
68
|
-
cleanUuid,
|
|
48
|
+
return await CallModule.displayIncomingCall(
|
|
49
|
+
uuid.toLowerCase().trim(),
|
|
69
50
|
number,
|
|
70
51
|
name,
|
|
71
52
|
hasVideo,
|
|
72
53
|
shouldRing
|
|
73
54
|
);
|
|
74
|
-
return success;
|
|
75
55
|
} catch (e) {
|
|
76
|
-
console.
|
|
56
|
+
console.error("Native Call Error:", e);
|
|
77
57
|
return false;
|
|
78
58
|
}
|
|
79
59
|
},
|
|
80
60
|
|
|
81
61
|
destroyNativeCallUI: (uuid) => {
|
|
82
|
-
if (CallModule
|
|
62
|
+
if (CallModule?.endNativeCall) {
|
|
83
63
|
CallModule.endNativeCall(uuid.toLowerCase());
|
|
84
64
|
}
|
|
85
65
|
},
|
|
86
66
|
|
|
87
67
|
subscribe: (onAccept, onReject, onFailed) => {
|
|
88
|
-
if (!callEventEmitter) {
|
|
89
|
-
return () => { };
|
|
90
|
-
}
|
|
68
|
+
if (!callEventEmitter) return () => { };
|
|
91
69
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
70
|
+
const subs = [
|
|
71
|
+
callEventEmitter.addListener('onCallAccepted', onAccept),
|
|
72
|
+
callEventEmitter.addListener('onCallRejected', onReject),
|
|
73
|
+
];
|
|
95
74
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
75
|
+
if (onFailed) {
|
|
76
|
+
subs.push(callEventEmitter.addListener('onCallFailed', onFailed));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return () => subs.forEach(s => s.remove());
|
|
101
80
|
}
|
|
102
|
-
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// IMPORTANT: Exporting as default ensures "import CallHandler from '...'" works
|
|
84
|
+
export default CallHandler;
|
package/ios/CallModule.m
CHANGED
|
@@ -11,20 +11,22 @@ RCT_EXPORT_MODULE();
|
|
|
11
11
|
- (instancetype)init {
|
|
12
12
|
self = [super init];
|
|
13
13
|
if (self) {
|
|
14
|
+
// Dynamically get the App Name from the bundle so it's not hardcoded
|
|
15
|
+
NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
|
16
|
+
if (!appName) {
|
|
17
|
+
appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
#pragma clang diagnostic push
|
|
15
21
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
16
|
-
CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName
|
|
22
|
+
CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:appName];
|
|
17
23
|
#pragma clang diagnostic pop
|
|
18
24
|
|
|
19
25
|
config.supportsVideo = YES;
|
|
20
26
|
config.maximumCallGroups = 1;
|
|
21
27
|
config.maximumCallsPerCallGroup = 1;
|
|
22
28
|
config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)];
|
|
23
|
-
config.includesCallsInRecents = NO;
|
|
24
|
-
|
|
25
|
-
// If you had a custom file like "Ringtone.aif" in your project,
|
|
26
|
-
// you would set it here:
|
|
27
|
-
// config.ringtoneSound = @"Ringtone.aif";
|
|
29
|
+
config.includesCallsInRecents = NO;
|
|
28
30
|
|
|
29
31
|
self.provider = [[CXProvider alloc] initWithConfiguration:config];
|
|
30
32
|
[self.provider setDelegate:self queue:nil];
|
|
@@ -32,7 +34,7 @@ RCT_EXPORT_MODULE();
|
|
|
32
34
|
return self;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
//
|
|
37
|
+
// Matches the 5 arguments in your JS/Android implementation
|
|
36
38
|
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
37
39
|
number:(NSString *)number
|
|
38
40
|
name:(NSString *)name
|
|
@@ -48,12 +50,12 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
CXCallUpdate *update = [[CXCallUpdate alloc] init];
|
|
53
|
+
// We use 'name' here so the system UI shows the caller's name instead of just their number
|
|
51
54
|
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:name];
|
|
52
55
|
update.hasVideo = hasVideo;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// However, including it here ensures your JS bridge call matches.
|
|
56
|
+
update.supportsGrouping = NO;
|
|
57
|
+
update.supportsUngrouping = NO;
|
|
58
|
+
update.supportsHolding = NO;
|
|
57
59
|
|
|
58
60
|
[self.provider reportNewIncomingCallWithUUID:uuid
|
|
59
61
|
update:update
|
|
@@ -72,10 +74,15 @@ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
|
|
|
72
74
|
[self.provider reportCallWithUUID:uuid
|
|
73
75
|
endedAtDate:[NSDate date]
|
|
74
76
|
reason:CXCallEndedReasonRemoteEnded];
|
|
75
|
-
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// Placeholder for Android parity
|
|
81
|
+
RCT_EXPORT_METHOD(checkTelecomPermissions:(RCTPromiseResolveBlock)resolve
|
|
82
|
+
reject:(RCTPromiseResolveBlock)reject) {
|
|
83
|
+
resolve(@YES); // iOS permissions are handled via Info.plist and system prompts
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
// MARK: - CXProviderDelegate
|
|
80
87
|
|
|
81
88
|
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
|
|
@@ -92,4 +99,4 @@ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
|
|
|
92
99
|
|
|
93
100
|
- (void)providerDidReset:(CXProvider *)provider { }
|
|
94
101
|
|
|
95
|
-
@end
|
|
102
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rns-nativecall",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "RNS nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"homepage": "https://github.com/raiidr/rns-nativecall",
|
package/withRaiidrVoip.js
CHANGED
|
@@ -8,11 +8,10 @@ function withAndroidConfig(config) {
|
|
|
8
8
|
return withAndroidManifest(config, (config) => {
|
|
9
9
|
const manifest = config.modResults;
|
|
10
10
|
|
|
11
|
-
// 1. Add required permissions
|
|
12
11
|
const permissions = [
|
|
13
12
|
'android.permission.READ_PHONE_NUMBERS',
|
|
14
13
|
'android.permission.CALL_PHONE',
|
|
15
|
-
'android.permission.MANAGE_OWN_CALLS'
|
|
14
|
+
'android.permission.MANAGE_OWN_CALLS'
|
|
16
15
|
];
|
|
17
16
|
|
|
18
17
|
if (!manifest.manifest['uses-permission']) {
|
|
@@ -25,7 +24,6 @@ function withAndroidConfig(config) {
|
|
|
25
24
|
}
|
|
26
25
|
});
|
|
27
26
|
|
|
28
|
-
// 2. Add MyConnectionService service
|
|
29
27
|
const application = manifest.manifest.application[0];
|
|
30
28
|
application.service = application.service || [];
|
|
31
29
|
|
|
@@ -69,15 +67,16 @@ function withIosConfig(config) {
|
|
|
69
67
|
}
|
|
70
68
|
});
|
|
71
69
|
|
|
72
|
-
// 2.
|
|
70
|
+
// 2. Dynamic Microphone Description
|
|
71
|
+
// config.name pulls the name from the user's app.json automatically
|
|
72
|
+
const appName = config.name || 'this app';
|
|
73
73
|
infoPlist.NSMicrophoneUsageDescription =
|
|
74
|
-
infoPlist.NSMicrophoneUsageDescription ||
|
|
74
|
+
infoPlist.NSMicrophoneUsageDescription || `Allow ${appName} to access your microphone for calls.`;
|
|
75
75
|
|
|
76
76
|
return config;
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Export the combined plugin
|
|
81
80
|
module.exports = (config) => {
|
|
82
81
|
return withPlugins(config, [
|
|
83
82
|
withAndroidConfig,
|