rns-nativecall 0.6.2 → 0.6.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,174 +1,94 @@
1
- # RNS Native Call
1
+ # rns-nativecall
2
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, video calls, and allows dismissing native call UI programmatically.
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.
4
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
- * Subscribe to call events (accepted, rejected, failed).
14
- * Cross-platform (Android & iOS) ready.
5
+ ## 🚀 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.
15
9
 
16
10
  ---
17
11
 
18
- ## Installation
12
+ ## 📦 Installation
19
13
 
20
14
  ```bash
21
15
  npm install rns-nativecall
22
- # or
23
- yarn add rns-nativecall
24
- ```
25
-
26
- ---
27
-
28
- ## Expo Integration
29
16
 
30
- If you are using **Expo managed workflow**, the package provides a plugin that automatically modifies the `AndroidManifest.xml` for required permissions and services.
31
-
32
- In your `app.json` / `app.config.js`:
33
-
34
- ```js
35
- export default {
36
- expo: {
37
- name: 'YourApp',
38
- slug: 'your-app',
39
- plugins: [
40
- "rns-nativecall"
41
- ],
42
- },
43
- };
44
17
  ```
45
-
46
- > The plugin ensures the following Android permissions are added automatically:
47
- >
48
- > ```xml
49
- > <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
50
- > <uses-permission android:name="android.permission.CALL_PHONE"/>
51
- > ```
52
- >
53
- > It also registers the `MyConnectionService` for Telecom integration:
54
- >
55
- > ```xml
56
- > <service
57
- > android:name="com.rnsnativecall.MyConnectionService"
58
- > android:permission="android.permission.BIND_CONNECTION_SERVICE"
59
- > android:exported="true">
60
- > <intent-filter>
61
- > <action android:name="android.telecom.ConnectionService"/>
62
- > </intent-filter>
63
- > </service>
64
- > ```
65
-
66
- ---
67
-
68
- ## iOS Setup
69
-
70
- 1. Make sure you have the pod installed:
71
-
72
- ```bash
73
- cd ios
74
- pod install
18
+ ---
19
+ Add the plugin to your app.json or app.config.js:
20
+ ```json
21
+ {
22
+ "expo": {
23
+ "plugins": ["rns-nativecall"]
24
+ }
25
+ }
75
26
  ```
76
-
77
- 2. CallKit is automatically used, so you need to enable it in your project, thats all and call log entries are prevented using:
78
-
79
- ```objc
80
- config.includesCallsInRecents = NO;
81
- ```
82
-
83
- > The native iOS module handles showing the call UI and managing call events.
84
-
85
27
  ---
86
-
87
- ## Usage
88
-
89
- ```ts
90
- import CallHandler from 'rns-nativecall';
91
-
92
- // Subscribe to call events
93
- const unsubscribe = CallHandler.subscribe(
94
- (data) => console.log('Call Accepted', data),
95
- (data) => console.log('Call Rejected', data),
96
- (data) => console.log('Call Failed', data)
97
- );
98
-
99
- // Display an incoming call
100
- await CallHandler.displayCall(
101
- 'uuid-string', // use uuid.v4();
102
- '+1234567890',
103
- 'John Doe',
104
- true, // hasVideo
105
- true // shouldRing
106
- );
107
-
108
- // Dismiss call UI programmatically
109
- CallHandler.destroyNativeCallUI('uuid-string');
110
-
111
- // Remove event listeners when done
112
- unsubscribe();
28
+ ### 🛠 Usage
29
+
30
+ 1. Initialize Headless Task (index.js)
31
+ Register the task at the very top of your entry file. This handles background calls and "Busy" signals for secondary callers.
32
+ ```javascript
33
+ import { AppRegistry } from 'react-native';
34
+ import App from './App';
35
+ import { CallHandler } from 'rns-nativecall';
36
+
37
+ CallHandler.registerHeadlessTask(async (data, eventType) => {
38
+ if (eventType === 'BUSY') {
39
+ // User is already on a call. Notify the second caller via WebSocket/API.
40
+ console.log("System Busy for UUID:", data.callUuid);
41
+ return;
42
+ }
43
+
44
+ if (eventType === 'INCOMING_CALL') {
45
+ // App is waking up for a new call.
46
+ // Trigger your custom UI or logic here.
47
+ }
48
+ });
49
+
50
+ AppRegistry.registerComponent('main', () => App);
51
+ ```
52
+ ### 2. Handling Events (App.js)
53
+ Use the subscribe method to handle user interaction (Accept/Reject) from the native UI.
54
+ ```javascript
55
+ import React, { useEffect } from 'react';
56
+ import { CallHandler } from 'rns-nativecall';
57
+
58
+ export default function App() {
59
+ useEffect(() => {
60
+ const unsubscribe = CallHandler.subscribe(
61
+ (data) => {
62
+ console.log("Call Accepted:", data.callUuid);
63
+ // Navigate to your call screen
64
+ },
65
+ (data) => {
66
+ console.log("Call Rejected:", data.callUuid);
67
+ // Send 'Hangup' signal to your server
68
+ }
69
+ );
70
+
71
+ return () => unsubscribe();
72
+ }, []);
73
+
74
+ // To manually trigger the UI (e.g., from an FCM data message)
75
+ const showCall = () => {
76
+ CallHandler.displayCall("unique-uuid", "Caller Name", "video");
77
+ };
78
+
79
+ return <YourUI />;
80
+ }
113
81
  ```
114
-
115
- ---
116
-
117
- ## API
118
-
119
- ### `displayCall(uuid, number, name, hasVideo?, shouldRing?) => Promise<boolean>`
120
-
121
- Displays native incoming call UI.
122
-
123
- * **uuid**: Unique call identifier.
124
- * **number**: Caller number.
125
- * **name**: Caller display name.
126
- * **hasVideo**: Boolean, true for video call.
127
- * **shouldRing**: Boolean, true to play native ringtone.
128
-
129
- Returns a promise that resolves to `true` if UI was successfully displayed.
130
-
131
- ### `destroyNativeCallUI(uuid)`
132
-
133
- Dismisses the native call UI.
134
-
135
- * **uuid**: Call identifier.
136
-
137
- ### `subscribe(onAccept, onReject, onFailed) => () => void`
138
-
139
- Subscribe to native call events.
140
-
141
- * **onAccept**: Callback for accepted calls.
142
- * **onReject**: Callback for rejected calls.
143
- * **onFailed**: Optional callback for failed calls.
144
-
145
- Returns a function to unsubscribe all listeners.
146
-
147
82
  ---
148
-
149
- ## Permissions
150
-
151
- ### Android
152
-
153
- * `READ_PHONE_NUMBERS`
154
- * `CALL_PHONE`
155
-
156
- The Expo plugin ensures these are automatically added to `AndroidManifest.xml`.
157
-
158
- ### iOS
159
-
160
- * Uses CallKit (`CXProvider`) internally.
161
- * Prevents call log pollution using `includesCallsInRecents = NO`.
162
-
83
+ ### 📖 API Reference
84
+ | Method | Description |
85
+ | :--- | :--- |
86
+ | **registerHeadlessTask(callback)** | Registers background logic. `eventType` is `'INCOMING_CALL'` or `'BUSY'`. |
87
+ | **displayCall(uuid, name, type)** | Shows the native call UI. Type is `'audio'` or `'video'`. |
88
+ | **destroyNativeCallUI(uuid)** | Dismisses the native call interface. |
89
+ | **getInitialCallData()** | Returns call data if the app was launched by clicking "Answer". |
90
+ | **subscribe(onAccept, onReject)** | Listens for native button presses (Answer/End). |
163
91
  ---
164
92
 
165
- ## Notes
166
-
167
- * Android uses `Connection.CAPABILITY_SELF_MANAGED` to prevent calls from showing in the system call log.
168
- * iOS uses `CXCallEndedReasonRemoteEnded` for clean UI dismissal without showing failed call overlays.
169
-
93
+ ## 🛡 License
170
94
  ---
171
-
172
- ## License
173
-
174
- MIT
@@ -68,7 +68,4 @@ dependencies {
68
68
  // Glide for profile pictures
69
69
  implementation "com.github.bumptech.glide:glide:4.15.1"
70
70
  annotationProcessor "com.github.bumptech.glide:compiler:4.15.1"
71
-
72
- implementation "androidx.core:core-ktx:1.12.0"
73
- implementation "androidx.appcompat:appcompat:1.6.1"
74
71
  }
@@ -5,51 +5,78 @@ import android.app.NotificationManager
5
5
  import android.content.Context
6
6
  import android.content.Intent
7
7
  import android.os.Bundle
8
+ import android.view.WindowManager
9
+ import android.app.KeyguardManager
8
10
 
9
11
  class AcceptCallActivity : Activity() {
12
+
10
13
  override fun onCreate(savedInstanceState: Bundle?) {
11
14
  super.onCreate(savedInstanceState)
12
-
13
- val dataMap = mutableMapOf<String, String>()
14
- val extras = intent.extras
15
15
 
16
- // Extract every single extra passed in from NativeCallManager
17
- extras?.keySet()?.forEach { key ->
18
- val value = extras.get(key)
19
- if (value != null) {
20
- dataMap[key] = value.toString()
21
- }
22
- }
16
+ // Wake + lock flags
17
+ window.addFlags(
18
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
19
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
20
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
21
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
22
+ )
23
23
 
24
- val uuid = dataMap["callUuid"]
24
+ // Optionally dismiss keyguard
25
+ val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
26
+ keyguardManager.requestDismissKeyguard(this, null)
25
27
 
26
- if (uuid != null) {
27
- // Kill background processes
28
- CallMessagingService.stopBackupTimer(uuid)
29
- val stopHeadlessIntent = Intent(applicationContext, CallHeadlessTask::class.java)
30
- applicationContext.stopService(stopHeadlessIntent)
28
+ processCallIntent(intent)
29
+ }
31
30
 
32
- // Stop Ringtone & Notification
33
- NativeCallManager.stopRingtone()
34
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
35
- notificationManager.cancel(uuid.hashCode())
31
+ override fun onNewIntent(intent: Intent) {
32
+ super.onNewIntent(intent)
33
+ setIntent(intent)
34
+ processCallIntent(intent)
35
+ }
36
36
 
37
- // SYNC WITH JS
38
- CallModule.setPendingCallData(dataMap)
39
- CallModule.sendEventToJS("onCallAccepted", dataMap)
37
+ private fun processCallIntent(intent: Intent) {
38
+ NativeCallManager.stopRingtone()
39
+
40
+ val extras = intent.extras
41
+ val uuid = extras?.getString("callUuid")
42
+ val name = extras?.getString("name") ?: "Someone"
43
+ val callType = extras?.getString("callType") ?: "audio"
40
44
 
41
- // OPEN THE APP
42
- val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
43
- launchIntent?.apply {
44
- // Pass ALL data forward to the main app
45
- putExtras(extras ?: Bundle())
46
- putExtra("navigatingToCall", true)
47
-
48
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
45
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
46
+ uuid?.let { notificationManager.cancel(it.hashCode()) }
47
+
48
+ val dataMap = mutableMapOf<String, String>()
49
+ extras?.keySet()?.forEach { key ->
50
+ extras.get(key)?.let { dataMap[key] = it.toString() }
51
+ }
52
+
53
+ if (CallModule.isReady()) {
54
+ CallModule.sendEventToJS("onCallAccepted", dataMap)
55
+ openMainApp(extras)
56
+ finish()
57
+ } else {
58
+ CallModule.setPendingCallData("onCallAccepted_pending", dataMap)
59
+ if (uuid != null) {
60
+ NativeCallManager.connecting(this, uuid, name, callType)
61
+ }
62
+ // Register callback to auto‑open app once RN is ready
63
+ CallModule.registerOnReadyCallback {
64
+ runOnUiThread {
65
+ openMainApp(extras)
66
+ finish()
67
+ }
49
68
  }
50
- startActivity(launchIntent)
51
69
  }
52
-
53
- finish()
54
70
  }
55
- }
71
+
72
+ // ✅ Now a proper member function
73
+ private fun openMainApp(extras: Bundle?) {
74
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
75
+ launchIntent?.apply {
76
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
77
+ putExtras(extras ?: Bundle())
78
+ putExtra("navigatingToCall", true)
79
+ startActivity(this)
80
+ }
81
+ }
82
+ }
@@ -4,44 +4,49 @@ import android.content.BroadcastReceiver
4
4
  import android.content.Context
5
5
  import android.content.Intent
6
6
  import android.app.NotificationManager
7
+ import android.app.KeyguardManager
7
8
 
8
9
  class CallActionReceiver : BroadcastReceiver() {
9
10
  override fun onReceive(context: Context, intent: Intent) {
10
- val uuid = intent.getStringExtra("EXTRA_CALL_UUID") ?: return
11
- val action = intent.action ?: return
12
-
13
- // 1. KILL THE BACKUP TIMER (Stop MessagingService from re-triggering)
14
- CallMessagingService.stopBackupTimer(uuid)
11
+ NativeCallManager.stopRingtone()
15
12
 
16
- // 2. STOP THE HEADLESS SERVICE (Stop JS from initializing WebRTC)
17
- val stopHeadlessIntent = Intent(context, CallHeadlessTask::class.java)
18
- context.stopService(stopHeadlessIntent)
13
+ val uuid = intent.getStringExtra("EXTRA_CALL_UUID") ?: return
14
+ val action = intent.action ?: ""
19
15
 
20
- // 3. STOP NATIVE UI
21
- NativeCallManager.stopRingtone()
22
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
23
- notificationManager.cancel(uuid.hashCode())
16
+ val notificationManager =
17
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
24
18
 
25
19
  val dataMap = mutableMapOf<String, String>()
26
20
  intent.extras?.keySet()?.forEach { key ->
27
21
  intent.extras?.get(key)?.let { dataMap[key] = it.toString() }
28
22
  }
29
23
 
30
- if (action.contains("ACTION_ACCEPT")) {
31
- CallModule.setPendingCallData(dataMap)
32
- CallModule.sendEventToJS("onCallAccepted", dataMap)
33
-
34
- val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
35
- launchIntent?.apply {
36
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
37
- putExtras(intent.extras ?: android.os.Bundle())
38
- putExtra("navigatingToCall", true)
24
+ val name = intent.extras?.getString("name") ?: "Someone"
25
+ val callType = intent.extras?.getString("callType") ?: "audio"
26
+
27
+ val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
28
+
29
+ if (action.contains("ACTION_REJECT")) {
30
+ if (CallModule.isReady()) {
31
+ CallModule.sendEventToJS("onCallRejected", mapOf("callUuid" to uuid))
32
+ notificationManager.cancel(uuid.hashCode())
33
+ } else {
34
+ // Queue pending event
35
+ CallModule.setPendingCallData("onCallRejected_pending", mapOf("callUuid" to uuid))
36
+ // Update notification pill to "Aborting…" state
37
+ NativeCallManager.aborting(context, uuid, name, callType)
38
+
39
+ // Register callback to auto‑open app once RN is ready
40
+ CallModule.registerOnReadyCallback {
41
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
42
+ launchIntent?.apply {
43
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
44
+ putExtras(intent.extras ?: android.os.Bundle())
45
+ putExtra("navigatingToCall", true)
46
+ context.startActivity(this)
47
+ }
48
+ }
39
49
  }
40
- context.startActivity(launchIntent)
41
-
42
- } else if (action.contains("ACTION_REJECT")) {
43
- // Tell JS call is dead
44
- CallModule.sendEventToJS("onCallRejected", dataMap)
45
50
  }
46
51
  }
47
- }
52
+ }
@@ -1,64 +1,36 @@
1
1
  package com.rnsnativecall
2
2
 
3
- import android.app.Notification
4
- import android.app.NotificationChannel
5
- import android.app.NotificationManager
6
- import android.content.Context
7
3
  import android.content.Intent
8
- import android.content.pm.ServiceInfo
9
- import android.os.Build
10
- import androidx.core.app.NotificationCompat
11
4
  import com.facebook.react.HeadlessJsTaskService
12
5
  import com.facebook.react.bridge.Arguments
13
6
  import com.facebook.react.jstasks.HeadlessJsTaskConfig
14
7
  import com.facebook.react.jstasks.LinearCountingRetryPolicy
15
8
 
16
- class CallHeadlessTask : HeadlessJsTaskService() {
9
+ import com.rnsnativecall.CallState
17
10
 
18
- override fun onCreate() {
19
- super.onCreate()
20
- val channelId = "call_service"
21
- val notificationId = 1 // Constant ID for the foreground service
22
11
 
23
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
24
- val channel = NotificationChannel(
25
- channelId,
26
- "Call Connection Service",
27
- NotificationManager.IMPORTANCE_LOW
28
- )
29
- val manager = getSystemService(NotificationManager::class.java)
30
- manager.createNotificationChannel(channel)
31
- }
12
+ class CallHeadlessTask : HeadlessJsTaskService() {
32
13
 
33
- // IMPORTANT: Must have a setSmallIcon or the service will crash on modern Android
34
- val notification: Notification = NotificationCompat.Builder(this, channelId)
35
- .setContentTitle("Incoming call...")
36
- .setContentText("Establishing connection")
37
- .setSmallIcon(android.R.drawable.sym_action_call) // Built-in icon as fallback
38
- .setPriority(NotificationCompat.PRIORITY_LOW)
39
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
40
- .build()
14
+ override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
15
+ val extras = intent?.extras ?: return null
16
+ val uuid = extras.getString("callUuid")
17
+ val isBusySignal = extras.getBoolean("isBusySignal", false)
41
18
 
42
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
43
- // Android 14+ requires the FOREGROUND_SERVICE_TYPE_PHONE_CALL type
44
- startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL)
45
- } else {
46
- startForeground(notificationId, notification)
47
- }
19
+ // GATE: If the call was canceled before we even started the task,
20
+ // we don't need to show the UI OR send a busy signal.
21
+ if (CallState.isCanceled(uuid)) {
22
+ return null
48
23
  }
49
24
 
50
- override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
51
- val extras = intent?.extras
52
- return if (extras != null) {
53
- // Linear retry helps if the JS bridge is busy or mid-reload
54
- val retryPolicy = LinearCountingRetryPolicy(3, 1000)
55
- HeadlessJsTaskConfig(
56
- "ColdStartCallTask",
57
- Arguments.fromBundle(extras),
58
- 60000, // Timeout after 60s
59
- true, // Allow task to run in foreground
60
- retryPolicy
61
- )
62
- } else null
63
- }
25
+ // Use a shorter timeout for Busy Signals (30s) vs real calls (60s)
26
+ val timeout = if (isBusySignal) 30000L else 60000L
27
+
28
+ return HeadlessJsTaskConfig(
29
+ "ColdStartCallTask",
30
+ Arguments.fromBundle(extras),
31
+ timeout,
32
+ true, // Allow in foreground
33
+ LinearCountingRetryPolicy(3, 1000)
34
+ )
35
+ }
64
36
  }