rns-nativecall 0.0.1

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/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # RNS Native Call
2
+
3
+ RNS Native Call is a React Native package that provides fully native VoIP call handling on Android and iOS. It supports self-managed calls, custom ringtone handling, video calls, and allows dismissing native call UI programmatically. Fully TypeScript-ready.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ * Display incoming calls with native UI.
10
+ * Support for video and audio calls.
11
+ * Self-managed calls on Android (prevents polluting the device call log).
12
+ * Prevents iOS call log from being saved using `includesCallsInRecents = NO`.
13
+ * Control ringtone per call.
14
+ * Subscribe to call events (accepted, rejected, failed).
15
+ * Cross-platform (Android & iOS) ready.
16
+ * TypeScript types included.
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install rns-nativecall
24
+ # or
25
+ yarn add rns-nativecall
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Expo Integration
31
+
32
+ If you are using **Expo managed workflow**, the package provides a plugin that automatically modifies the `AndroidManifest.xml` for required permissions and services.
33
+
34
+ In your `app.json` / `app.config.js`:
35
+
36
+ ```js
37
+ import withRaiidrVoip from 'rns-nativecall/withRaiidrVoip';
38
+
39
+ export default {
40
+ expo: {
41
+ name: 'YourApp',
42
+ slug: 'your-app',
43
+ plugins: [
44
+ withRaiidrVoip
45
+ ],
46
+ },
47
+ };
48
+ ```
49
+
50
+ > The plugin ensures the following Android permissions are added automatically:
51
+ >
52
+ > ```xml
53
+ > <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
54
+ > <uses-permission android:name="android.permission.CALL_PHONE"/>
55
+ > ```
56
+ >
57
+ > It also registers the `MyConnectionService` for Telecom integration:
58
+ >
59
+ > ```xml
60
+ > <service
61
+ > android:name="com.rnsnativecall.MyConnectionService"
62
+ > android:permission="android.permission.BIND_CONNECTION_SERVICE"
63
+ > android:exported="true">
64
+ > <intent-filter>
65
+ > <action android:name="android.telecom.ConnectionService"/>
66
+ > </intent-filter>
67
+ > </service>
68
+ > ```
69
+
70
+ ---
71
+
72
+ ## iOS Setup
73
+
74
+ 1. Make sure you have the pod installed:
75
+
76
+ ```bash
77
+ cd ios
78
+ pod install
79
+ ```
80
+
81
+ 2. CallKit is automatically used, and call log entries are prevented using:
82
+
83
+ ```objc
84
+ config.includesCallsInRecents = NO;
85
+ ```
86
+
87
+ > The native iOS module handles showing the call UI and managing call events.
88
+
89
+ ---
90
+
91
+ ## Usage
92
+
93
+ ```ts
94
+ import CallHandler from 'rns-nativecall';
95
+
96
+ // Subscribe to call events
97
+ const unsubscribe = CallHandler.subscribe(
98
+ (data) => console.log('Call Accepted', data),
99
+ (data) => console.log('Call Rejected', data),
100
+ (data) => console.log('Call Failed', data)
101
+ );
102
+
103
+ // Display an incoming call
104
+ await CallHandler.displayCall(
105
+ 'uuid-string',
106
+ '+1234567890',
107
+ 'John Doe',
108
+ true, // hasVideo
109
+ true // shouldRing
110
+ );
111
+
112
+ // Dismiss call UI programmatically
113
+ CallHandler.destroyNativeCallUI('uuid-string');
114
+
115
+ // Remove event listeners when done
116
+ unsubscribe();
117
+ ```
118
+
119
+ ---
120
+
121
+ ## API
122
+
123
+ ### `displayCall(uuid, number, name, hasVideo?, shouldRing?) => Promise<boolean>`
124
+
125
+ Displays native incoming call UI.
126
+
127
+ * **uuid**: Unique call identifier.
128
+ * **number**: Caller number.
129
+ * **name**: Caller display name.
130
+ * **hasVideo**: Boolean, true for video call.
131
+ * **shouldRing**: Boolean, true to play native ringtone.
132
+
133
+ Returns a promise that resolves to `true` if UI was successfully displayed.
134
+
135
+ ### `destroyNativeCallUI(uuid)`
136
+
137
+ Dismisses the native call UI.
138
+
139
+ * **uuid**: Call identifier.
140
+
141
+ ### `subscribe(onAccept, onReject, onFailed) => () => void`
142
+
143
+ Subscribe to native call events.
144
+
145
+ * **onAccept**: Callback for accepted calls.
146
+ * **onReject**: Callback for rejected calls.
147
+ * **onFailed**: Optional callback for failed calls.
148
+
149
+ Returns a function to unsubscribe all listeners.
150
+
151
+ ---
152
+
153
+ ## Permissions
154
+
155
+ ### Android
156
+
157
+ * `READ_PHONE_NUMBERS`
158
+ * `CALL_PHONE`
159
+
160
+ The Expo plugin ensures these are automatically added to `AndroidManifest.xml`.
161
+
162
+ ### iOS
163
+
164
+ * Uses CallKit (`CXProvider`) internally.
165
+ * Prevents call log pollution using `includesCallsInRecents = NO`.
166
+
167
+ ---
168
+
169
+ ## TypeScript Support
170
+
171
+ Types are included. Example:
172
+
173
+ ```ts
174
+ import CallHandler, { CallData, CallAcceptedCallback } from 'rns-nativecall';
175
+
176
+ const onAccept: CallAcceptedCallback = (data: CallData) => {
177
+ console.log('Accepted:', data.callUUID);
178
+ };
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Notes
184
+
185
+ * Android uses `Connection.CAPABILITY_SELF_MANAGED` to prevent calls from showing in the system call log.
186
+ * iOS uses `CXCallEndedReasonRemoteEnded` for clean UI dismissal without showing failed call overlays.
187
+
188
+ ---
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,117 @@
1
+ ///Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallModule.kt
2
+ package com.rnsnativecall
3
+
4
+ import android.content.ComponentName
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.net.Uri
8
+ import android.os.Bundle
9
+ import android.telecom.PhoneAccount
10
+ import android.telecom.PhoneAccountHandle
11
+ import android.telecom.TelecomManager
12
+ import android.telecom.DisconnectCause
13
+ import com.facebook.react.bridge.*
14
+ import com.facebook.react.modules.core.DeviceEventManagerModule
15
+
16
+ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
17
+
18
+ init {
19
+ instance = this
20
+ registerPhoneAccount()
21
+ }
22
+
23
+ override fun getName() = "CallModule"
24
+
25
+ private fun registerPhoneAccount() {
26
+ val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
27
+ val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
28
+ val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
29
+
30
+ // Combine all critical capabilities into one mask this is important!!!
31
+ val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
32
+ PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
33
+
34
+ val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Raiidr")
35
+ .setCapabilities(capabilities)
36
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
37
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
38
+ .build()
39
+
40
+ telecomManager.registerPhoneAccount(phoneAccount)
41
+ }
42
+
43
+ @ReactMethod
44
+ fun displayIncomingCall(
45
+ uuid: String,
46
+ number: String,
47
+ name: String,
48
+ hasVideo: Boolean,
49
+ playRing: Boolean,
50
+ promise: Promise
51
+ ) {
52
+ val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
53
+ val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
54
+ val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
55
+
56
+ val extras = Bundle().apply {
57
+ putParcelable(
58
+ TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
59
+ Uri.fromParts("sip", number, null)
60
+ )
61
+ putString(TelecomManager.EXTRA_CALL_SUBJECT, name)
62
+ putString("EXTRA_CALL_UUID", uuid)
63
+ putBoolean("EXTRA_PLAY_RING", playRing)
64
+ putBoolean(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, hasVideo)
65
+ }
66
+
67
+ try {
68
+ telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
69
+ promise.resolve(true)
70
+ } catch (e: Exception) {
71
+ promise.reject("CALL_ERROR", e.message)
72
+ }
73
+ }
74
+
75
+ @ReactMethod
76
+ fun endNativeCall(uuid: String) {
77
+ val connection = MyConnectionService.getConnection(uuid)
78
+ connection?.let {
79
+ it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
80
+ it.destroy()
81
+ MyConnectionService.removeConnection(uuid)
82
+ }
83
+ }
84
+
85
+ @ReactMethod
86
+ fun checkTelecomPermissions(promise: Promise) {
87
+ val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
88
+ val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
89
+ val phoneAccountHandle = PhoneAccountHandle(componentName, "RaiidrVoip")
90
+
91
+ val account = telecomManager.getPhoneAccount(phoneAccountHandle)
92
+ if (account != null && account.isEnabled) {
93
+ promise.resolve(true)
94
+ } else {
95
+ val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
96
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
97
+ reactApplicationContext.startActivity(intent)
98
+ promise.resolve(false)
99
+ }
100
+ }
101
+
102
+ @ReactMethod fun addListener(eventName: String) {}
103
+ @ReactMethod fun removeListeners(count: Int) {}
104
+
105
+ companion object {
106
+ private var instance: CallModule? = null
107
+
108
+ fun sendEventToJS(eventName: String, uuid: String?) {
109
+ val params = Arguments.createMap().apply {
110
+ putString("callUUID", uuid)
111
+ }
112
+ instance?.reactApplicationContext
113
+ ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
114
+ ?.emit(eventName, params)
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,17 @@
1
+ /////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallPackage.kt
2
+ package com.rnsnativecall
3
+
4
+ import com.facebook.react.ReactPackage
5
+ import com.facebook.react.bridge.NativeModule
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class CallPackage : ReactPackage {
10
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
11
+ return listOf(CallModule(reactContext))
12
+ }
13
+
14
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
15
+ return emptyList()
16
+ }
17
+ }
@@ -0,0 +1,100 @@
1
+ /////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/MyCallConnection.kt
2
+ package com.rnsnativecall
3
+
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.media.AudioAttributes
7
+ import android.media.MediaPlayer
8
+ import android.media.RingtoneManager
9
+ import android.telecom.Connection
10
+ import android.telecom.DisconnectCause
11
+
12
+ class MyCallConnection(
13
+ private val context: Context,
14
+ private val callUUID: String?,
15
+ private val playRing: Boolean,
16
+ private val onAcceptCallback: (String?) -> Unit,
17
+ private val onRejectCallback: (String?) -> Unit
18
+ ) : Connection() {
19
+
20
+ private var mediaPlayer: MediaPlayer? = null
21
+
22
+ init {
23
+ connectionProperties = PROPERTY_SELF_MANAGED
24
+ audioModeIsVoip = true
25
+
26
+ if (playRing) {
27
+ startRingtone()
28
+ }
29
+ }
30
+
31
+ private fun startRingtone() {
32
+ val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
33
+
34
+ mediaPlayer = MediaPlayer().apply {
35
+ setDataSource(context, uri)
36
+ setAudioAttributes(
37
+ AudioAttributes.Builder()
38
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
39
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
40
+ .build()
41
+ )
42
+ isLooping = true
43
+ prepare()
44
+ start()
45
+ }
46
+ }
47
+
48
+ private fun stopRingtone() {
49
+ mediaPlayer?.stop()
50
+ mediaPlayer?.release()
51
+ mediaPlayer = null
52
+ }
53
+
54
+ private fun cleanUp() {
55
+ stopRingtone()
56
+ callUUID?.let { MyConnectionService.removeConnection(it) }
57
+ }
58
+
59
+ override fun onAnswer() {
60
+ stopRingtone()
61
+
62
+ // Bring app to foreground
63
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
64
+ launchIntent?.apply {
65
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
66
+ context.startActivity(this)
67
+ }
68
+
69
+ // Notify JS
70
+ onAcceptCallback(callUUID)
71
+
72
+ // 🔥 IMMEDIATELY detach from Telecom
73
+ setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
74
+ cleanUp()
75
+ destroy()
76
+ }
77
+
78
+ override fun onReject() {
79
+ stopRingtone()
80
+ setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
81
+ cleanUp()
82
+ destroy()
83
+ onRejectCallback(callUUID)
84
+ }
85
+
86
+ override fun onDisconnect() {
87
+ stopRingtone()
88
+ setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
89
+ cleanUp()
90
+ destroy()
91
+ onRejectCallback(callUUID)
92
+ }
93
+
94
+ override fun onAbort() {
95
+ stopRingtone()
96
+ setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
97
+ cleanUp()
98
+ destroy()
99
+ }
100
+ }
@@ -0,0 +1,70 @@
1
+ /////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/MyConnectionService.kt
2
+ package com.rnsnativecall
3
+
4
+ import android.telecom.Connection
5
+ import android.telecom.ConnectionRequest
6
+ import android.telecom.ConnectionService
7
+ import android.telecom.PhoneAccountHandle
8
+ import android.telecom.TelecomManager
9
+ import java.util.concurrent.ConcurrentHashMap
10
+
11
+ class MyConnectionService : ConnectionService() {
12
+
13
+ companion object {
14
+ private val activeConnections = ConcurrentHashMap<String, MyCallConnection>()
15
+
16
+ fun getConnection(uuid: String) = activeConnections[uuid]
17
+ fun addConnection(uuid: String, connection: MyCallConnection) {
18
+ activeConnections[uuid] = connection
19
+ }
20
+ fun removeConnection(uuid: String) {
21
+ activeConnections.remove(uuid)
22
+ }
23
+ }
24
+
25
+ override fun onCreateIncomingConnection(
26
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
27
+ request: ConnectionRequest?
28
+ ): Connection {
29
+
30
+ // 1️⃣ Read extras FIRST
31
+ val extras = request?.extras
32
+ val callUUID = extras?.getString("EXTRA_CALL_UUID")
33
+ val playRing = extras?.getBoolean("EXTRA_PLAY_RING", true) ?: true
34
+ val callerName = extras?.getString(TelecomManager.EXTRA_CALL_SUBJECT)
35
+
36
+ // 2️⃣ Create connection
37
+ val connection = MyCallConnection(
38
+ context = applicationContext,
39
+ callUUID = callUUID,
40
+ playRing = playRing,
41
+ onAcceptCallback = { uuid ->
42
+ CallModule.sendEventToJS("onCallAccepted", uuid)
43
+ },
44
+ onRejectCallback = { uuid ->
45
+ CallModule.sendEventToJS("onCallRejected", uuid)
46
+ }
47
+ )
48
+
49
+ // 3️⃣ Set caller display name (THIS FIXES MISALIGNMENT)
50
+ callerName?.let {
51
+ connection.setCallerDisplayName(
52
+ it,
53
+ TelecomManager.PRESENTATION_ALLOWED
54
+ )
55
+ }
56
+
57
+ // 4️⃣ Standard Telecom setup
58
+ connection.connectionCapabilities =
59
+ Connection.CAPABILITY_MUTE or Connection.CAPABILITY_SUPPORT_HOLD
60
+
61
+ connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
62
+ connection.setInitializing()
63
+ connection.setRinging()
64
+
65
+ // 5️⃣ Track connection
66
+ callUUID?.let { addConnection(it, connection) }
67
+
68
+ return connection
69
+ }
70
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,6 @@
1
+ ////Users/bush/Desktop/Apps/Raiidr/package/app.plugin.js
2
+ const withRaiidrVoip = require('./withRaiidrVoip');
3
+
4
+ module.exports = function (config) {
5
+ return withRaiidrVoip(config);
6
+ };
package/index.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ ///Users/bush/Desktop/Apps/Raiidr/package/index.d.ts
2
+ export interface CallData {
3
+ callUUID: string;
4
+ }
5
+
6
+ export type CallAcceptedCallback = (data: CallData) => void;
7
+ export type CallRejectedCallback = (data: CallData) => void;
8
+ export type CallFailedCallback = (data: CallData) => void;
9
+
10
+ export interface CallHandlerType {
11
+ /**
12
+ * Display an incoming call UI.
13
+ * @param uuid Unique call identifier
14
+ * @param number Caller number
15
+ * @param name Caller display name
16
+ * @param hasVideo True if video call
17
+ * @param shouldRing True to play native ringtone
18
+ * @returns Promise resolving to true if successfully displayed
19
+ */
20
+ displayCall(
21
+ uuid: string,
22
+ number: string,
23
+ name: string,
24
+ hasVideo?: boolean,
25
+ shouldRing?: boolean
26
+ ): Promise<boolean>;
27
+
28
+ /**
29
+ * Dismiss native call UI
30
+ * @param uuid Call identifier
31
+ */
32
+ destroyNativeCallUI(uuid: string): void;
33
+
34
+ /**
35
+ * Subscribe to call events
36
+ * @param onAccept Callback for accepted calls
37
+ * @param onReject Callback for rejected calls
38
+ * @param onFailed Optional callback for failed calls
39
+ * @returns Function to unsubscribe all listeners
40
+ */
41
+ subscribe(
42
+ onAccept: CallAcceptedCallback,
43
+ onReject: CallRejectedCallback,
44
+ onFailed?: CallFailedCallback
45
+ ): () => void;
46
+ }
47
+
48
+ declare const CallHandler: CallHandlerType;
49
+
50
+ export default CallHandler;
package/index.js ADDED
@@ -0,0 +1,102 @@
1
+ ///Users/bush/Desktop/Apps/Raiidr/package/index.js
2
+ import {
3
+ NativeModules,
4
+ NativeEventEmitter,
5
+ PermissionsAndroid,
6
+ Platform,
7
+ Linking,
8
+ Alert
9
+ } from 'react-native';
10
+
11
+ const { CallModule } = NativeModules;
12
+ const callEventEmitter = CallModule ? new NativeEventEmitter(CallModule) : null;
13
+
14
+ const REQUIRED_PERMISSIONS = Platform.OS === 'android' ? [
15
+ PermissionsAndroid.PERMISSIONS.READ_PHONE_NUMBERS,
16
+ PermissionsAndroid.PERMISSIONS.CALL_PHONE,
17
+ ] : [];
18
+
19
+ export async function ensureAndroidPermissions() {
20
+ if (Platform.OS !== 'android') return true;
21
+ const result = await PermissionsAndroid.requestMultiple(REQUIRED_PERMISSIONS);
22
+ const deniedPermissions = Object.entries(result)
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;
35
+ }
36
+
37
+ 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
+ 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
+ }
51
+
52
+ // 1. Android Specific Checks
53
+ if (Platform.OS === 'android') {
54
+ const hasPerms = await ensureAndroidPermissions();
55
+ if (!hasPerms) return false;
56
+
57
+ const isAccountEnabled = await CallModule.checkTelecomPermissions();
58
+ if (!isAccountEnabled) return false;
59
+ }
60
+
61
+ const cleanUuid = uuid.toLowerCase().trim();
62
+
63
+ // 2. Cross-platform execution
64
+ try {
65
+ // We now pass 5 arguments to the native side
66
+ // uuid, number, name, hasVideo, shouldRing
67
+ const success = await CallModule.displayIncomingCall(
68
+ cleanUuid,
69
+ number,
70
+ name,
71
+ hasVideo,
72
+ shouldRing
73
+ );
74
+ return success;
75
+ } catch (e) {
76
+ console.log("Native Call Error:", e);
77
+ return false;
78
+ }
79
+ },
80
+
81
+ destroyNativeCallUI: (uuid) => {
82
+ if (CallModule && CallModule.endNativeCall) {
83
+ CallModule.endNativeCall(uuid.toLowerCase());
84
+ }
85
+ },
86
+
87
+ subscribe: (onAccept, onReject, onFailed) => {
88
+ if (!callEventEmitter) {
89
+ return () => { };
90
+ }
91
+
92
+ const acceptSub = callEventEmitter.addListener('onCallAccepted', (data) => onAccept(data));
93
+ const rejectSub = callEventEmitter.addListener('onCallRejected', (data) => onReject(data));
94
+ const failSub = onFailed ? callEventEmitter.addListener('onCallFailed', (data) => onFailed(data)) : null;
95
+
96
+ return () => {
97
+ acceptSub.remove();
98
+ rejectSub.remove();
99
+ if (failSub) failSub.remove();
100
+ };
101
+ }
102
+ };
@@ -0,0 +1,7 @@
1
+ #import <CallKit/CallKit.h>
2
+ #import <React/RCTBridgeModule.h>
3
+ #import <React/RCTEventEmitter.h>
4
+
5
+ @interface CallModule : RCTEventEmitter <RCTBridgeModule, CXProviderDelegate>
6
+ @property(nonatomic, strong) CXProvider *provider;
7
+ @end
@@ -0,0 +1,95 @@
1
+ #import "CallModule.h"
2
+
3
+ @implementation CallModule
4
+
5
+ RCT_EXPORT_MODULE();
6
+
7
+ - (NSArray<NSString *> *)supportedEvents {
8
+ return @[ @"onCallAccepted", @"onCallRejected", @"onCallFailed" ];
9
+ }
10
+
11
+ - (instancetype)init {
12
+ self = [super init];
13
+ if (self) {
14
+ #pragma clang diagnostic push
15
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
16
+ CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:@"Raiidr"];
17
+ #pragma clang diagnostic pop
18
+
19
+ config.supportsVideo = YES;
20
+ config.maximumCallGroups = 1;
21
+ config.maximumCallsPerCallGroup = 1;
22
+ config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)];
23
+ config.includesCallsInRecents = NO; // <-- prevent log
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";
28
+
29
+ self.provider = [[CXProvider alloc] initWithConfiguration:config];
30
+ [self.provider setDelegate:self queue:nil];
31
+ }
32
+ return self;
33
+ }
34
+
35
+ // Updated with showRing (BOOL)
36
+ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
37
+ number:(NSString *)number
38
+ name:(NSString *)name
39
+ hasVideo:(BOOL)hasVideo
40
+ showRing:(BOOL)showRing
41
+ resolve:(RCTPromiseResolveBlock)resolve
42
+ reject:(RCTPromiseRejectBlock)reject)
43
+ {
44
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
45
+ if (!uuid) {
46
+ reject(@"INVALID_UUID", @"The provided UUID string is invalid", nil);
47
+ return;
48
+ }
49
+
50
+ CXCallUpdate *update = [[CXCallUpdate alloc] init];
51
+ update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:name];
52
+ update.hasVideo = hasVideo;
53
+
54
+ // Note: CallKit handles ringing based on the CXProviderConfiguration.
55
+ // iOS doesn't allow toggling 'ring' per-call via reportNewIncomingCall.
56
+ // However, including it here ensures your JS bridge call matches.
57
+
58
+ [self.provider reportNewIncomingCallWithUUID:uuid
59
+ update:update
60
+ completion:^(NSError *_Nullable error) {
61
+ if (error) {
62
+ reject(@"CALL_ERROR", error.localizedDescription, error);
63
+ } else {
64
+ resolve(@YES);
65
+ }
66
+ }];
67
+ }
68
+
69
+ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
70
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
71
+ if (uuid) {
72
+ [self.provider reportCallWithUUID:uuid
73
+ endedAtDate:[NSDate date]
74
+ reason:CXCallEndedReasonRemoteEnded];
75
+
76
+ }
77
+ }
78
+
79
+ // MARK: - CXProviderDelegate
80
+
81
+ - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
82
+ [action fulfill];
83
+ [self sendEventWithName:@"onCallAccepted"
84
+ body:@{@"callUUID" : [action.callUUID.UUIDString lowercaseString]}];
85
+ }
86
+
87
+ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
88
+ [action fulfill];
89
+ [self sendEventWithName:@"onCallRejected"
90
+ body:@{@"callUUID" : [action.callUUID.UUIDString lowercaseString]}];
91
+ }
92
+
93
+ - (void)providerDidReset:(CXProvider *)provider { }
94
+
95
+ @end
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "rns-nativecall",
3
+ "version": "0.0.1",
4
+ "description": "Raiidr nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "react-native": "index.js",
8
+ "scripts": {
9
+ "p": "npm publish --access public"
10
+ },
11
+ "expo": {
12
+ "plugins": [
13
+ "./app.plugin.js"
14
+ ]
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "nativecall",
19
+ "call",
20
+ "native",
21
+ "call keep",
22
+ "android",
23
+ "expo",
24
+ "incognito",
25
+ "dialog suppression",
26
+ "keyboard stability",
27
+ "lifecycle events"
28
+ ],
29
+ "files": [
30
+ "android",
31
+ "ios",
32
+ "app.plugin.js",
33
+ "index.d.ts",
34
+ "index.js",
35
+ "package.json",
36
+ "react-native.config",
37
+ "README.md",
38
+ "rns-nativecall.podspec",
39
+ "withRaiidrVoip"
40
+ ],
41
+ "peerDependencies": {
42
+ "react-native": "*",
43
+ "expo": "*"
44
+ },
45
+ "dependencies": {
46
+ "@expo/config-plugins": "~10.1.1"
47
+ }
48
+ }
@@ -0,0 +1,18 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = package["name"]
7
+ s.version = package["version"]
8
+ s.summary = package["description"] || "Custom native call for React Native"
9
+ s.license = package["license"] || "MIT"
10
+ s.author = package["author"] || "Unknown"
11
+ s.homepage = package["homepage"] || "https://github.com/your/repo"
12
+ s.source = { :git => "https://github.com/raiidr/rns-nativecall.git", :tag => "#{s.version}" }
13
+
14
+ s.platforms = { :ios => "11.0" }
15
+ s.source_files = "ios/**/*.{h,m}"
16
+
17
+ s.dependency "React-Core"
18
+ end