rns-nativecall 0.9.8 → 1.0.0

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
@@ -49,8 +49,8 @@ CallHandler.registerHeadlessTask(async (data, eventType) => {
49
49
 
50
50
  AppRegistry.registerComponent('main', () => App);
51
51
  ```
52
- ### 2. Handling Events (Index.js/App.js)
53
- Use the subscribe method to handle user interaction (Accept/Reject) from the native UI.
52
+ ## Handling Events (Index.js)
53
+
54
54
  ```javascript
55
55
  // Index.js Setup Foreground Listeners
56
56
  CallHandler.subscribe(
@@ -79,8 +79,7 @@ CallHandler.subscribe(
79
79
 
80
80
  ````
81
81
 
82
- ## App.js version
83
-
82
+ ## Handling Events (App.js)
84
83
 
85
84
  ```javascript
86
85
  import React, { useEffect } from 'react';
@@ -112,15 +111,129 @@ export default function App() {
112
111
  ```
113
112
  ---
114
113
  ### 📖 API Reference
115
- | Method | Description |
116
- | :--- | :--- |
117
- | **registerHeadlessTask(callback)** | Registers background logic. `eventType` is `INCOMING_CALL` or `BUSY`. |
118
- | **displayCall(uuid, name, type)** | Shows the native call UI. Type is `audio` or `video`. |
119
- | **checkCallValidity(uuid)** | Returns boolen value for `isValid` and `isCanceled` |
120
- | **destroyNativeCallUI(uuid)** | Dismisses the native call interface. |
121
- | **getInitialCallData()** | Returns call data if the app was launched by clicking `Answer`. |
122
- | **subscribe(onAccept, onReject)** | Listens for native button presses `(Answer/End)`. |
114
+ # rns-nativecall API Reference
115
+
116
+ | Method | Platform | Description |
117
+ | :--- | :--- | :--- |
118
+ | **registerHeadlessTask(callback)** | Android | Registers background logic. `eventType` is `INCOMING_CALL` or `BUSY`. |
119
+ | **checkOverlayPermission()** | Android | Returns true if the app can draw over other apps (Required for Android Lockscreen). |
120
+ | **requestOverlayPermission()** | Android | Opens System Settings to let the user enable "Draw over other apps". |
121
+ | **displayCall(uuid, name, type)** | All | Shows the native call UI (Standard Notification on Android / CallKit on iOS). |
122
+ | **checkCallValidity(uuid)** | All | Returns boolean values for `isValid` and `isCanceled`. |
123
+ | **stopForegroundService()** | Android | Stops the ongoing service and clears the persistent notification/pill. |
124
+ | **destroyNativeCallUI(uuid)** | All | Dismisses the native call interface and stops the ringtone. |
125
+ | **getInitialCallData()** | All | Returns call data if the app was launched by clicking `Answer` from a killed state. |
126
+ | **subscribe(onAccept, onReject)** | All | Listens for native button presses (Answer/End). |
127
+
128
+ ---
129
+
130
+ # Implementation Notes
131
+
132
+ 1. Android Persistence:
133
+ Because this library uses a Foreground Service on Android, the notification will persist and show a "Call Pill" in the status bar. To remove this after the call ends or connects, you MUST call 'CallHandler.stopForegroundService()'.
134
+
135
+ 2. Android Overlay:
136
+ 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.
137
+
138
+ 3. iOS CallKit:
139
+ On iOS, 'displayCall' uses the native system CallKit UI. This works automatically in the background and on the lockscreen without extra overlay permissions.
140
+
141
+ 4. Single Call Gate:
142
+ 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.
123
143
  ---
124
144
 
145
+ ## FULL Example Use Case
146
+ ```javascript
147
+ import React, { useEffect, useState } from 'react';
148
+ import { StyleSheet, Text, View, TouchableOpacity, Alert } from 'react-native';
149
+ import { CallHandler } from 'rns-nativecall';
150
+
151
+ export default function App() {
152
+ const [activeCall, setActiveCall] = useState(null);
153
+
154
+ useEffect(() => {
155
+ // 1. Handle app launched from a notification "Answer" click
156
+ CallHandler.getInitialCallData().then((data) => {
157
+ if (data && data.default) {
158
+ console.log("App launched from call:", data.default);
159
+ setActiveCall(data.default);
160
+ }
161
+ });
162
+
163
+ // 2. Subscribe to foreground events
164
+ const unsubscribe = CallHandler.subscribe(
165
+ (data) => {
166
+ console.log("Call Accepted:", data.callUuid);
167
+ setActiveCall(data);
168
+ // Logic: Open your Video/Audio Call UI here
169
+ },
170
+ (data) => {
171
+ console.log("Call Rejected/Ended:", data.callUuid);
172
+ setActiveCall(null);
173
+ }
174
+ );
175
+
176
+ return () => unsubscribe();
177
+ }, []);
178
+
179
+ const startTestCall = async () => {
180
+ // Android Only: Check for overlay permission to show UI over lockscreen
181
+ const hasPermission = await CallHandler.checkOverlayPermission();
182
+ if (!hasPermission) {
183
+ Alert.alert(
184
+ "Permission Required",
185
+ "Please enable 'Draw over other apps' to see calls while the phone is locked.",
186
+ [
187
+ { text: "Cancel" },
188
+ { text: "Settings", onPress: () => CallHandler.requestOverlayPermission() }
189
+ ]
190
+ );
191
+ return;
192
+ }
193
+
194
+ // Trigger the native UI
195
+ CallHandler.displayCall(
196
+ "test-uuid-" + Date.now(),
197
+ "John Doe",
198
+ "video"
199
+ );
200
+ };
201
+
202
+ const endCallManually = () => {
203
+ if (activeCall) {
204
+ CallHandler.stopForegroundService();
205
+ setActiveCall(null);
206
+ }
207
+ };
208
+
209
+ return (
210
+ <View style={styles.container}>
211
+ <Text style={styles.title}>RNS Native Call Pro</Text>
212
+
213
+ {activeCall ? (
214
+ <View style={styles.callBox}>
215
+ <Text>Active Call with: {activeCall.name}</Text>
216
+ <TouchableOpacity style={styles.btnEnd} onPress={endCallManually}>
217
+ <Text style={styles.btnText}>End Call</Text>
218
+ </TouchableOpacity>
219
+ </View>
220
+ ) : (
221
+ <TouchableOpacity style={styles.btnStart} onPress={startTestCall}>
222
+ <Text style={styles.btnText}>Simulate Incoming Call</Text>
223
+ </TouchableOpacity>
224
+ )}
225
+ </View>
226
+ );
227
+ }
228
+
229
+ const styles = StyleSheet.create({
230
+ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF' },
231
+ title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
232
+ callBox: { padding: 20, backgroundColor: '#e1f5fe', borderRadius: 10, alignItems: 'center' },
233
+ btnStart: { backgroundColor: '#4CAF50', padding: 15, borderRadius: 5 },
234
+ btnEnd: { backgroundColor: '#F44336', padding: 15, borderRadius: 5, marginTop: 10 },
235
+ btnText: { color: 'white', fontWeight: 'bold' }
236
+ });
237
+ ```
125
238
  ## 🛡 License
126
239
  ---
@@ -52,6 +52,9 @@ class CallForegroundService : Service() {
52
52
  ): Int {
53
53
  val data = intent?.extras
54
54
  val name = data?.getString("name") ?: "Someone"
55
+ val uuid = data?.getString("callUuid") ?: "default_uuid"
56
+
57
+ val notificationId = uuid.hashCode()
55
58
 
56
59
  createNotificationChannel()
57
60
 
@@ -68,12 +71,12 @@ class CallForegroundService : Service() {
68
71
 
69
72
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
70
73
  startForeground(
71
- NOTIFICATION_ID,
74
+ notificationId,
72
75
  notification,
73
76
  ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL,
74
77
  )
75
78
  } else {
76
- startForeground(NOTIFICATION_ID, notification)
79
+ startForeground(notificationId, notification)
77
80
  }
78
81
 
79
82
  // Launch the Headless Task
@@ -95,26 +98,13 @@ class CallForegroundService : Service() {
95
98
  e.printStackTrace()
96
99
  }
97
100
 
98
- // // Trigger the incoming call notification UI handled by NativeCallManager
99
- // try {
100
- // val map = mutableMapOf<String, String>()
101
- // data?.let { b ->
102
- // for (key in b.keySet()) {
103
- // map[key] = b.get(key)?.toString() ?: ""
104
- // }
105
- // }
106
- // NativeCallManager.handleIncomingPush(this, map)
107
- // } catch (e: Exception) {
108
- // e.printStackTrace()
109
- // }
110
-
111
101
  // Auto-stop after 30s
112
- Handler(Looper.getMainLooper()).postDelayed({
113
- try {
114
- stopSelf()
115
- } catch (e: Exception) {
116
- }
117
- }, 30000)
102
+ // Handler(Looper.getMainLooper()).postDelayed({
103
+ // try {
104
+ // stopSelf()
105
+ // } catch (e: Exception) {
106
+ // }
107
+ // }, 30000)
118
108
 
119
109
  return START_NOT_STICKY
120
110
  }
@@ -1,8 +1,8 @@
1
1
  package com.rnsnativecall
2
2
 
3
3
  import android.app.ActivityManager
4
- import android.app.NotificationManager
5
4
  import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
6
  import android.app.PendingIntent
7
7
  import android.content.Context
8
8
  import android.content.Intent
@@ -10,14 +10,12 @@ import android.os.Build
10
10
  import android.os.Bundle
11
11
  import androidx.core.app.NotificationCompat
12
12
  import androidx.core.content.ContextCompat
13
+ import com.facebook.react.HeadlessJsTaskService
13
14
  import com.google.firebase.messaging.FirebaseMessagingService
14
15
  import com.google.firebase.messaging.RemoteMessage
15
- import com.facebook.react.HeadlessJsTaskService
16
-
17
16
  import com.rnsnativecall.CallState
18
17
 
19
18
  class CallMessagingService : FirebaseMessagingService() {
20
-
21
19
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
22
20
  val data = remoteMessage.data
23
21
  val context = applicationContext
@@ -26,78 +24,62 @@ class CallMessagingService : FirebaseMessagingService() {
26
24
  val type = data["type"] ?: ""
27
25
 
28
26
  if (type == "CANCEL") {
29
- NativeCallManager.stopRingtone()
30
- // Pass context here to persist the cancellation
31
- CallState.markCanceled(uuid, context)
32
-
33
- if (CallState.getCurrent() == uuid) {
34
- CallState.clear(uuid, context)
35
- }
36
-
37
- NativeCallManager.dismissIncomingCall(context, uuid)
38
- showMissedCallNotification(context, data, uuid)
39
- return
40
- }
41
-
42
- // Inside onMessageReceived
43
- if (!CallState.setCurrent(uuid)) {
44
- // We are busy! Start a SILENT headless task to send the WebSocket busy msg
45
- val busyIntent = Intent(context, CallHeadlessTask::class.java).apply {
46
- putExtras(Bundle().apply {
47
- data.forEach { (k, v) -> putString(k, v) }
48
- putBoolean("isBusySignal", true)
49
- })
50
- }
51
- context.startService(busyIntent)
52
- return
53
- }
27
+ NativeCallManager.stopRingtone()
28
+ // Pass context here to persist the cancellation
29
+ CallState.markCanceled(uuid, context)
54
30
 
31
+ if (CallState.getCurrent() == uuid) {
32
+ CallState.clear(uuid, context)
33
+ }
55
34
 
35
+ NativeCallManager.dismissIncomingCall(context, uuid)
36
+ showMissedCallNotification(context, data, uuid)
37
+ return
38
+ }
56
39
 
40
+ // Inside onMessageReceived
41
+ if (!CallState.setCurrent(uuid)) {
42
+ // We are busy! Start a SILENT headless task to send the WebSocket busy msg
43
+ val busyIntent =
44
+ Intent(context, CallHeadlessTask::class.java).apply {
45
+ putExtras(
46
+ Bundle().apply {
47
+ data.forEach { (k, v) -> putString(k, v) }
48
+ putBoolean("isBusySignal", true)
49
+ },
50
+ )
51
+ }
52
+ context.startService(busyIntent)
53
+ return
54
+ }
57
55
 
58
56
  if (isAppInForeground(context)) {
59
57
  // Foreground → send event directly
60
58
  CallModule.sendEventToJS("onCallReceived", data)
61
59
  } else {
62
- // Show incoming call notification instantly
63
- //NativeCallManager.handleIncomingPush(context, data) /// i will use headless instead
64
- // Background → start foreground service + headless task
65
- // val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
66
- // putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
67
- // }
68
- // ContextCompat.startForegroundService(context, serviceIntent)
69
-
70
- // val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
71
- // putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
72
- // }
73
- // try {
74
- // context.startService(headlessIntent)
75
- // HeadlessJsTaskService.acquireWakeLockNow(context)
76
- // } catch (e: Exception) {
77
- // e.printStackTrace()
78
- // }
79
-
80
-
81
- // Background → start foreground service (which in turn starts headless)
82
- val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
83
- putExtras(Bundle().apply {
84
- data.forEach { (k, v) -> putString(k, v) }
85
- })
86
- }
87
-
88
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
89
- context.startForegroundService(serviceIntent)
90
- } else {
91
- context.startService(serviceIntent)
92
- }
93
-
60
+ // Background start foreground service (which in turn starts headless)
61
+ val serviceIntent =
62
+ Intent(context, CallForegroundService::class.java).apply {
63
+ putExtra("callUuid", uuid) // Key: Pass the UUID here
64
+ putExtras(
65
+ Bundle().apply {
66
+ data.forEach { (k, v) -> putString(k, v) }
67
+ },
68
+ )
69
+ }
70
+
71
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
72
+ context.startForegroundService(serviceIntent)
73
+ } else {
74
+ context.startService(serviceIntent)
75
+ }
94
76
  }
95
77
  }
96
78
 
97
79
  private fun showMissedCallNotification(
98
80
  context: Context,
99
81
  data: Map<String, String>,
100
- uuid: String
82
+ uuid: String,
101
83
  ) {
102
84
  val name = data["name"] ?: "Unknown"
103
85
  val callType = data["callType"] ?: "video"
@@ -107,40 +89,46 @@ if (!CallState.setCurrent(uuid)) {
107
89
  context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
108
90
 
109
91
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
110
- val channel = NotificationChannel(
111
- channelId,
112
- "Missed Calls",
113
- NotificationManager.IMPORTANCE_DEFAULT
114
- ).apply { description = "Missed call notifications" }
92
+ val channel =
93
+ NotificationChannel(
94
+ channelId,
95
+ "Missed Calls",
96
+ NotificationManager.IMPORTANCE_DEFAULT,
97
+ ).apply { description = "Missed call notifications" }
115
98
  notificationManager.createNotificationChannel(channel)
116
99
  }
117
100
 
118
101
  val iconResId =
119
- context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
102
+ context.resources
103
+ .getIdentifier("ic_missed_call", "drawable", context.packageName)
120
104
  .takeIf { it != 0 } ?: android.R.drawable.sym_call_missed
121
105
  val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
122
106
 
123
107
  val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
124
108
  launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
125
- val pendingIntent = PendingIntent.getActivity(
126
- context,
127
- uuid.hashCode(),
128
- launchIntent,
129
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
130
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
131
- else
132
- PendingIntent.FLAG_UPDATE_CURRENT
133
- )
134
-
135
- val builder = NotificationCompat.Builder(context, channelId)
136
- .setSmallIcon(iconResId)
137
- .setContentTitle("$appName Missed $callType call")
138
- .setContentText("You missed a call from $name")
139
- .setPriority(NotificationCompat.PRIORITY_HIGH)
140
- .setAutoCancel(true)
141
- .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
142
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
143
- .setContentIntent(pendingIntent)
109
+ val pendingIntent =
110
+ PendingIntent.getActivity(
111
+ context,
112
+ uuid.hashCode(),
113
+ launchIntent,
114
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
115
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
116
+ } else {
117
+ PendingIntent.FLAG_UPDATE_CURRENT
118
+ },
119
+ )
120
+
121
+ val builder =
122
+ NotificationCompat
123
+ .Builder(context, channelId)
124
+ .setSmallIcon(iconResId)
125
+ .setContentTitle("$appName • Missed $callType call")
126
+ .setContentText("You missed a call from $name")
127
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
128
+ .setAutoCancel(true)
129
+ .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
130
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
131
+ .setContentIntent(pendingIntent)
144
132
 
145
133
  notificationManager.notify(uuid.hashCode(), builder.build())
146
134
  }
@@ -152,7 +140,7 @@ if (!CallState.setCurrent(uuid)) {
152
140
  val packageName = context.packageName
153
141
  return appProcesses.any {
154
142
  it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
155
- it.processName == packageName
143
+ it.processName == packageName
156
144
  }
157
145
  }
158
146
  }
@@ -19,7 +19,7 @@ object NativeCallManager {
19
19
  // because the system NotificationManager handles the sound.
20
20
 
21
21
  // Incrementing version to V3 to force fresh channel settings on the device
22
- const val channelId = "CALL_CHANNEL_V6_URGENT"
22
+ const val channelId = "CALL_CHANNEL_V9_URGENT"
23
23
  private var currentCallData: Map<String, String>? = null
24
24
 
25
25
  fun getCurrentCallData(): Map<String, String>? = currentCallData
@@ -139,8 +139,15 @@ object NativeCallManager {
139
139
  ) {
140
140
  this.currentCallData = null
141
141
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
142
+
142
143
  if (uuid != null) {
144
+ // 1. Kill the notification UI
143
145
  notificationManager.cancel(uuid.hashCode())
146
+
147
+ // 2. IMPORTANT: Stop the Foreground Service process
148
+ // This ensures the "Pill" in the status bar goes away
149
+ val serviceIntent = Intent(context, CallForegroundService::class.java)
150
+ context.stopService(serviceIntent)
144
151
  }
145
152
  }
146
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.9.8",
3
+ "version": "1.0.0",
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",