rns-nativecall 1.3.1 → 1.3.3

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 CHANGED
@@ -1,11 +1,17 @@
1
1
  # rns-nativecall
2
2
 
3
- A professional VoIP incoming call handler for React Native. Features a "Single Call Gate" on Android to manage busy states and full CallKit integration for iOS.
3
+ A **professional VoIP incoming call handler for React Native** with full **Android & iOS native UI integration**.
4
+ Designed for production-grade apps requiring **CallKit**, **lockscreen handling**, **headless execution**, and **single-call enforcement**.
5
+
6
+ ---
4
7
 
5
8
  ## 🚀 Highlights
6
- - **Expo Support**: Built-in config plugin handles all native setup.
7
- - **Single Call Gate**: Automatically detects if the user is in a call and flags secondary calls as "BUSY".
8
- - **Headless Mode**: Works even when the app is killed or the screen is locked.
9
+
10
+ - 📦 **Expo Ready** Zero manual native setup via config plugin
11
+ - ☎️ **Single Call Gate** Automatically blocks concurrent calls and emits `BUSY` events
12
+ - 🧠 **Headless Mode** – Works when the app is killed, backgrounded, or screen locked
13
+ - 📱 **Native UI** – Full-screen Android Activity & iOS CallKit
14
+ - 🔔 **System-Level Reliability** – No JS race conditions, no ghost calls
9
15
 
10
16
  ---
11
17
 
@@ -13,15 +19,20 @@ A professional VoIP incoming call handler for React Native. Features a "Single C
13
19
 
14
20
  ```bash
15
21
  npx expo install rns-nativecall expo-build-properties react-native-uuid
16
-
17
22
  ```
23
+
18
24
  or
25
+
19
26
  ```bash
20
27
  npm install rns-nativecall expo-build-properties react-native-uuid
21
-
22
28
  ```
23
- ---
24
- Add the plugin to your app.json or app.config.js:
29
+
30
+ ---
31
+
32
+ ## ⚙️ Expo Configuration
33
+
34
+ Add the plugin to **app.json** or **app.config.js**:
35
+
25
36
  ```json
26
37
  {
27
38
  "expo": {
@@ -54,11 +65,15 @@ Add the plugin to your app.json or app.config.js:
54
65
  }
55
66
  }
56
67
  ```
68
+
57
69
  ---
58
- ### 🛠 Usage
59
70
 
60
- 1. Initialize Headless Task (index.js)
61
- Register the task at the very top of your entry file. This handles background calls and "Busy" signals for secondary callers.
71
+ ## 🛠 Usage
72
+
73
+ ### 1️⃣ Register Headless Task (index.js)
74
+
75
+ This enables background handling, busy-state signaling, and cold-start execution.
76
+
62
77
  ```javascript
63
78
  import { AppRegistry } from 'react-native';
64
79
  import App from './App';
@@ -66,51 +81,41 @@ import { CallHandler } from 'rns-nativecall';
66
81
 
67
82
  CallHandler.registerHeadlessTask(async (data, eventType) => {
68
83
  if (eventType === 'BUSY') {
69
- // User is already on a call. Notify the second caller via WebSocket/API.
70
- console.log("System Busy for UUID:", data.callUuid);
84
+ console.log("User already in call:", data.callUuid);
71
85
  return;
72
86
  }
73
87
 
74
88
  if (eventType === 'INCOMING_CALL') {
75
- // App is waking up for a new call.
76
- // Trigger your custom UI or logic here.
89
+ console.log("Incoming call payload:", data);
77
90
  }
78
91
  });
79
92
 
80
93
  AppRegistry.registerComponent('main', () => App);
81
94
  ```
82
- ## Handling Events (Index.js)
95
+
96
+ ---
97
+
98
+ ## 🎧 Foreground Event Handling (index.js)
83
99
 
84
100
  ```javascript
85
- // Index.js Setup Foreground Listeners
86
101
  CallHandler.subscribe(
87
-
88
102
  async (data) => {
89
- try {
90
- console.log("APP IS OPEN", data)
91
- // navigate here
92
- } catch (error) {
93
- console.log("pending_call_uuid", error)
94
- }
103
+ console.log("CALL ACCEPTED", data);
95
104
  },
96
-
97
105
  async (data) => {
98
- try {
99
- console.log("CALL DECLINE", data)
100
- // update the caller here call decline
101
- } catch (error) {
102
- console.log("Onreject/ cancel call error", error)
103
- }
106
+ console.log("CALL REJECTED", data);
104
107
  },
105
-
106
- (data) => { console.log("failded", data) }
108
+ (data) => {
109
+ console.log("CALL FAILED", data);
110
+ }
107
111
  );
108
-
109
112
  ```
110
- ## Handling Events (App.js)
111
113
 
112
- ```javascript
114
+ ---
113
115
 
116
+ ## 🎬 App-Level Integration (App.js)
117
+
118
+ ```javascript
114
119
  import React, { useEffect } from 'react';
115
120
  import { CallHandler } from 'rns-nativecall';
116
121
 
@@ -119,18 +124,15 @@ export default function App() {
119
124
  const unsubscribe = CallHandler.subscribe(
120
125
  (data) => {
121
126
  console.log("Call Accepted:", data.callUuid);
122
- // Navigate to your call screen
123
127
  },
124
128
  (data) => {
125
129
  console.log("Call Rejected:", data.callUuid);
126
- // Send 'Hangup' signal to your server
127
130
  }
128
131
  );
129
132
 
130
133
  return () => unsubscribe();
131
134
  }, []);
132
135
 
133
- // To manually trigger the UI (e.g., from an FCM data message)
134
136
  const showCall = () => {
135
137
  CallHandler.displayCall("unique-uuid", "Caller Name", "video");
136
138
  };
@@ -138,51 +140,62 @@ export default function App() {
138
140
  return <YourUI />;
139
141
  }
140
142
  ```
143
+
141
144
  ---
142
- ## 📖 rns-nativecall API Reference
143
145
 
146
+ ## 📖 API Reference
144
147
 
145
148
  ### Core Methods
149
+
146
150
  | Method | Platform | Description |
147
- | :--- | :--- | :--- |
148
- | **registerHeadlessTask(callback)** | All | Registers the background task. `eventType` is 'INCOMING_CALL', 'BUSY', or 'ABORTED_CALL'. |
149
- | **displayCall(uuid, name, type)** | All | Launches full-screen Activity (Android) or reports to CallKit (iOS). |
150
- | **destroyNativeCallUI(uuid)** | All | Stops ringtone/Activity (Android) or ends CallKit session (iOS). |
151
- | **subscribe(onAccept, onReject, onFailed)** | All | Listens for Answer/Decline actions and system-level bridge errors. |
152
- | **showMissedCall(uuid, name, type)** | Android | Posts a persistent notification in the device tray for missed calls. |
153
- | **showOnGoingCall(uuid, name, type)** | Android | Show a persistent notification in the device tray for on going calls. |
154
-
155
- ### Data & State Management
151
+ |------|---------|-------------|
152
+ | `registerHeadlessTask(cb)` | All | Registers background task (`INCOMING_CALL`, `BUSY`, `ABORTED_CALL`) |
153
+ | `displayCall(uuid, name, type)` | All | Shows native incoming call UI |
154
+ | `destroyNativeCallUI(uuid)` | All | Ends native UI / CallKit session |
155
+ | `subscribe(onAccept, onReject, onFail)` | All | Listen to call actions |
156
+ | `showMissedCall(uuid, name, type)` | Android | Persistent missed call notification |
157
+ | `showOnGoingCall(uuid, name, type)` | Android | Persistent ongoing call notification |
158
+
159
+ ---
160
+
161
+ ### Data & State
162
+
156
163
  | Method | Platform | Description |
157
- | :--- | :--- | :--- |
158
- | **getInitialCallData()** | All | Retrieves the call payload if the app was cold-started via a notification. |
159
- | **checkCallValidity(uuid)** | All | Returns `{isValid, isCanceled}` to prevent ghost or aborted calls. |
160
- | **checkCallStatus(uuid)** | All | Returns `{isCanceled, isActive, shouldDisplay}` for UI syncing. |
164
+ |------|---------|-------------|
165
+ | `getInitialCallData()` | All | Retrieve payload from cold start |
166
+ | `checkCallValidity(uuid)` | All | Prevent ghost calls |
167
+ | `checkCallStatus(uuid)` | All | Sync UI with native state |
168
+
169
+ ---
161
170
 
162
171
  ### Android Permissions
163
- | Method | Platform | Description |
164
- | :--- | :--- | :--- |
165
- | **checkOverlayPermission()** | Android | Returns `true` if app can draw over other apps (Heads-up UI). |
166
- | **checkFullScreenPermission()** | Android | (Android 14+) Checks if app can trigger full-screen intents. |
167
- | **requestOverlayPermission()** | Android | Navigates user to "Draw over other apps" system settings. |
168
- | **requestFullScreenSettings()** | Android | (Android 14+) Navigates user to "Full Screen Intent" settings. |
172
+
173
+ | Method | Description |
174
+ |------|-------------|
175
+ | `checkOverlayPermission()` | Check lockscreen overlay permission |
176
+ | `requestOverlayPermission()` | Open overlay settings |
177
+ | `checkFullScreenPermission()` | Android 14+ full screen intent |
178
+ | `requestFullScreenSettings()` | Android 14+ permission screen |
169
179
 
170
180
  ---
171
181
 
172
- # Implementation Notes
182
+ ## 🧠 Implementation Notes
173
183
 
174
- 1. Android Overlay:
175
- For your React Native call screen to show up when the phone is locked, the user must grant the "Overlay Permission". Use 'checkOverlayPermission()' and 'requestOverlayPermission()' during your app's onboarding or call initiation.
184
+ ### Android Overlay
185
+ Overlay permission is required to display calls on the lockscreen.
186
+ Request during onboarding or before first call.
176
187
 
177
- 2. iOS CallKit:
178
- On iOS, 'displayCall' uses the native system CallKit UI. This works automatically in the background and on the lockscreen without extra overlay permissions.
188
+ ### iOS CallKit
189
+ Uses system CallKit UI. Works automatically in background and lockscreen.
190
+
191
+ ### Single Call Gate
192
+ If a call is active, all subsequent calls emit `BUSY` via headless task.
179
193
 
180
- 3. Single Call Gate:
181
- The library automatically prevents multiple overlapping native UIs. If a call is already active, subsequent calls will trigger the 'BUSY' event in your Headless Task.
182
194
  ---
183
195
 
184
- ## FULL Example Use Case
185
- ```javascript
196
+ ## 🧪 Full Example
197
+
198
+ ```js
186
199
  import React, { useEffect, useState } from 'react';
187
200
  import { StyleSheet, Text, View, TouchableOpacity, Alert } from 'react-native';
188
201
  import { CallHandler } from 'rns-nativecall';
@@ -275,5 +288,13 @@ const styles = StyleSheet.create({
275
288
  btnText: { color: 'white', fontWeight: 'bold' }
276
289
  });
277
290
  ```
291
+
292
+ ---
293
+
278
294
  ## 🛡 License
295
+
296
+ MIT License
297
+
279
298
  ---
299
+
300
+ Built for **production VoIP apps**, not demos.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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",
@@ -20,7 +20,8 @@
20
20
  "url": "git+https://github.com/raiidr/rns-nativecall.git"
21
21
  },
22
22
  "scripts": {
23
- "p": "npm publish --access public"
23
+ "p": "npm publish --access public",
24
+ "i": "npm publish --access public"
24
25
  },
25
26
  "keywords": [
26
27
  "react-native",
@@ -47,8 +48,6 @@
47
48
  "react-native": ">=0.60.0"
48
49
  },
49
50
  "dependencies": {
50
- "@expo/config-plugins": "^9.0.0",
51
- "react-native-uuid": "^2.0.3",
52
- "expo-build-properties": "~0.14.8"
51
+ "@expo/config-plugins": "^9.0.0"
53
52
  }
54
53
  }
@@ -13,7 +13,6 @@ function withMainActivityDataFix(config) {
13
13
  'import android.Manifest'
14
14
  ];
15
15
 
16
- // Add imports if they don't exist
17
16
  imports.forEach(imp => {
18
17
  if (!contents.includes(imp)) {
19
18
  contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
@@ -21,16 +20,21 @@ function withMainActivityDataFix(config) {
21
20
  });
22
21
 
23
22
  const onCreateCode = `
24
- override fun onCreate(savedInstanceState: Bundle?) {
23
+ override fun onCreate(savedInstanceState: Bundle?) {
25
24
  super.onCreate(savedInstanceState)
26
25
 
27
- // Request Notification Permissions for Android 13+ (Required for Pill UI)
26
+ // Request Permissions for Android 13+ and Calling Roles
27
+ val permissions = mutableListOf<String>()
28
28
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
29
- ActivityCompat.requestPermissions(
30
- this,
31
- arrayOf(Manifest.permission.POST_NOTIFICATIONS),
32
- 101
33
- )
29
+ permissions.add(Manifest.permission.POST_NOTIFICATIONS)
30
+ }
31
+ // MANAGE_OWN_CALLS is required for Foreground Service Type 'phoneCall' on SDK 34+
32
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
33
+ permissions.add(Manifest.permission.MANAGE_OWN_CALLS)
34
+ }
35
+
36
+ if (permissions.isNotEmpty()) {
37
+ ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 101)
34
38
  }
35
39
 
36
40
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@@ -69,35 +73,45 @@ function withMainActivityDataFix(config) {
69
73
  return config;
70
74
  });
71
75
  }
76
+
72
77
  function withNotificationColor(config) {
73
78
  // 1. Find the color in app.json (fallback to #218aff)
74
79
  const notificationPlugin = config.plugins?.find(p => Array.isArray(p) && p[0] === 'expo-notifications');
75
80
  const iconColor = notificationPlugin?.[1]?.color || '#218aff';
76
81
 
77
82
  return withAndroidStyles(config, (config) => {
83
+ // Corrected from config.config.modResults to config.modResults
78
84
  config.modResults = AndroidConfig.Styles.assignStylesValue(config.modResults, {
79
85
  name: 'notification_icon_color',
80
86
  value: iconColor,
81
- parent: 'AppTheme', // or your primary theme
87
+ parent: 'AppTheme',
82
88
  type: 'color',
83
89
  });
84
90
  return config;
85
91
  });
86
92
  }
87
- /** 2. ANDROID MANIFEST CONFIG **/
93
+
88
94
  function withAndroidConfig(config) {
89
95
  return withAndroidManifest(config, (config) => {
90
96
  const manifest = config.modResults;
97
+
98
+ // Ensure manifest structure exists
99
+ if (!manifest.manifest) manifest.manifest = {};
100
+ if (!manifest.manifest.application) manifest.manifest.application = [{}];
101
+
91
102
  const application = manifest.manifest.application[0];
92
103
 
93
- // Ensure MainActivity flags
94
- const mainActivity = application.activity.find((a) => a.$['android:name'] === '.MainActivity');
95
- if (mainActivity) {
96
- mainActivity.$['android:launchMode'] = 'singleTop';
97
- mainActivity.$['android:showWhenLocked'] = 'true';
98
- mainActivity.$['android:turnScreenOn'] = 'true';
104
+ // 1. Ensure MainActivity flags
105
+ if (application.activity) {
106
+ const mainActivity = application.activity.find((a) => a.$['android:name'] === '.MainActivity');
107
+ if (mainActivity) {
108
+ mainActivity.$['android:launchMode'] = 'singleTop';
109
+ mainActivity.$['android:showWhenLocked'] = 'true';
110
+ mainActivity.$['android:turnScreenOn'] = 'true';
111
+ }
99
112
  }
100
113
 
114
+ // 2. Fix Permissions (The 'some' error fix)
101
115
  const permissions = [
102
116
  'android.permission.VIBRATE',
103
117
  'android.permission.FOREGROUND_SERVICE',
@@ -107,18 +121,24 @@ function withAndroidConfig(config) {
107
121
  'android.permission.POST_NOTIFICATIONS',
108
122
  'android.permission.WAKE_LOCK',
109
123
  'android.permission.DISABLE_KEYGUARD',
124
+ 'android.permission.MANAGE_OWN_CALLS',
110
125
  ];
111
126
 
112
- manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
127
+ // Initialize uses-permission if it doesn't exist
128
+ if (!Array.isArray(manifest.manifest['uses-permission'])) {
129
+ manifest.manifest['uses-permission'] = [];
130
+ }
131
+
113
132
  permissions.forEach((perm) => {
114
- if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
133
+ if (!manifest.manifest['uses-permission'].some((p) => p.$ && p.$['android:name'] === perm)) {
115
134
  manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
116
135
  }
117
136
  });
118
137
 
119
- application.service = application.service || [];
120
- application.activity = application.activity || [];
121
- application.receiver = application.receiver || [];
138
+ // 3. Fix Services
139
+ if (!Array.isArray(application.service)) {
140
+ application.service = [];
141
+ }
122
142
 
123
143
  const services = [
124
144
  { name: 'com.rnsnativecall.CallMessagingService', exported: 'false', filter: 'com.google.firebase.MESSAGING_EVENT' },
@@ -128,7 +148,7 @@ function withAndroidConfig(config) {
128
148
  ];
129
149
 
130
150
  services.forEach(svc => {
131
- if (!application.service.some(s => s.$['android:name'] === svc.name)) {
151
+ if (!application.service.some(s => s.$ && s.$['android:name'] === svc.name)) {
132
152
  const entry = { $: { 'android:name': svc.name, 'android:exported': svc.exported || 'false' } };
133
153
  if (svc.type) entry.$['android:foregroundServiceType'] = svc.type;
134
154
  if (svc.filter) {
@@ -138,13 +158,18 @@ function withAndroidConfig(config) {
138
158
  }
139
159
  });
140
160
 
161
+ // 4. Fix Activities
162
+ if (!Array.isArray(application.activity)) {
163
+ application.activity = [];
164
+ }
165
+
141
166
  const activities = [
142
167
  { name: 'com.rnsnativecall.AcceptCallActivity', theme: '@android:style/Theme.Translucent.NoTitleBar', launchMode: 'singleInstance' },
143
168
  { name: 'com.rnsnativecall.NotificationOverlayActivity', theme: '@android:style/Theme.NoTitleBar.Fullscreen', launchMode: 'singleInstance' }
144
169
  ];
145
170
 
146
171
  activities.forEach(act => {
147
- if (!application.activity.some(a => a.$['android:name'] === act.name)) {
172
+ if (!application.activity.some(a => a.$ && a.$['android:name'] === act.name)) {
148
173
  application.activity.push({
149
174
  $: {
150
175
  'android:name': act.name,
@@ -160,28 +185,28 @@ function withAndroidConfig(config) {
160
185
  }
161
186
  });
162
187
 
163
- if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
188
+ // 5. Fix Receiver (The other likely 'some' error source)
189
+ if (!Array.isArray(application.receiver)) {
190
+ application.receiver = [];
191
+ }
192
+
193
+ if (!application.receiver.some(r => r.$ && r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
164
194
  application.receiver.push({ $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' } });
165
195
  }
166
196
 
167
197
  return config;
168
198
  });
169
199
  }
170
- /** 2. IOS APP DELEGATE MOD (The fix for Lock Screen Answer) **/
200
+
171
201
  function withIosAppDelegateMod(config) {
172
202
  return withAppDelegate(config, (config) => {
173
203
  let contents = config.modResults.contents;
174
-
175
- // 1. Surgical Import: Add 'import React' at the very top if missing
176
204
  if (!contents.includes('import React')) {
177
205
  contents = 'import React\n' + contents;
178
206
  }
179
207
 
180
- // 2. Check for the continue userActivity method
181
208
  if (!contents.includes('continue userActivity')) {
182
- // Method is missing, inject it before the final closing brace of the class
183
209
  const swiftLinkingCode = `
184
- // Universal Links
185
210
  public override func application(
186
211
  _ application: UIApplication,
187
212
  continue userActivity: NSUserActivity,
@@ -191,36 +216,26 @@ function withIosAppDelegateMod(config) {
191
216
  return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
192
217
  }
193
218
  `;
194
- // This regex finds the last '}' in the file (closing the AppDelegate class)
195
219
  contents = contents.replace(/\n}\s*$/, `\n${swiftLinkingCode}\n}`);
196
- } else if (!contents.includes('RCTLinkingManager.application')) {
197
- // Method exists but is missing our logic, inject it inside
198
- contents = contents.replace(
199
- /continue userActivity: NSUserActivity,[\s\S]*?\) -> Bool \{/,
200
- `continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {\n let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)`
201
- );
202
220
  }
203
-
204
221
  config.modResults.contents = contents;
205
222
  return config;
206
223
  });
207
224
  }
208
- /** 3. IOS INFO.PLIST CONFIG (Existing) **/
225
+
209
226
  function withIosConfig(config, props = {}) {
210
227
  return withInfoPlist(config, (config) => {
211
228
  const infoPlist = config.modResults;
212
229
  if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
213
-
214
230
  ['voip', 'remote-notification'].forEach(mode => {
215
231
  if (!infoPlist.UIBackgroundModes.includes(mode)) {
216
232
  infoPlist.UIBackgroundModes.push(mode);
217
233
  }
218
234
  });
219
-
220
235
  return config;
221
236
  });
222
237
  }
223
- // Main Plugin Entry
238
+
224
239
  module.exports = (config, props) => {
225
240
  return withPlugins(config, [
226
241
  withAndroidConfig,