rns-nativecall 1.1.0 → 1.1.2

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
@@ -130,32 +130,31 @@ export default function App() {
130
130
  ### 📖 API Reference
131
131
  # rns-nativecall API Reference
132
132
 
133
- | Method | Platform | Description |
134
- | :--- | :--- | :--- |
135
- | **registerHeadlessTask(callback)** | Android | Registers background logic. `eventType` is `INCOMING_CALL` or `BUSY`. |
136
- | **checkOverlayPermission()** | Android | Returns true if the app can draw over other apps (Required for Android Lockscreen). |
137
- | **requestOverlayPermission()** | Android | Opens System Settings to let the user enable "Draw over other apps". |
138
- | **displayCall(uuid, name, type)** | All | Shows the native call UI (Standard Notification on Android / CallKit on iOS). |
139
- | **checkCallValidity(uuid)** | All | Returns boolean values for `isValid` and `isCanceled`. |
140
- | **stopForegroundService()** | Android | Stops the ongoing service and clears the persistent notification/pill. |
141
- | **destroyNativeCallUI(uuid)** | All | Dismisses the native call interface and stops the ringtone. |
142
- | **getInitialCallData()** | All | Returns call data if the app was launched by clicking `Answer` from a killed state. |
143
- | **subscribe(onAccept, onReject)** | All | Listens for native button presses (Answer/End). |
133
+ | Method | Platform | Description |
134
+ |:-----------------------------|:------------|:--------------------------------------------------------------------------------------|
135
+ | checkFullScreenPermission() | Android 14+ | Returns true if the app is allowed to show calls over the lockscreen (FSI). |
136
+ | openFullScreenSettings() | Android 14+ | Deep-links the user to the specific system page to enable Full-Screen Intent. |
137
+ | checkOverlayPermission() | Android | Returns true if the app can draw over other apps (Overlay/Draw over apps). |
138
+ | requestOverlayPermission() | Android | Opens System Settings to let the user enable "Draw over other apps". |
139
+ | displayCall(uuid, name, type)| All | Launches the native call UI (CallStyle on Android / CallKit on iOS). |
140
+ | checkCallValidity(uuid) | All | Returns boolean values for `isValid` and `isCanceled` to prevent "ghost" calls. |
141
+ | checkCallStatus(uuid) | All | Detailed check of a call's status; useful for determining if UI should remain active. |
142
+ | reportRemoteEnded(uuid, res) | All | Notifies the native side that the caller hung up (prevents infinite ringing). |
143
+ | destroyNativeCallUI(uuid) | All | Forcefully dismisses the native call interface and stops the ringtone immediately. |
144
+ | getInitialCallData() | All | Returns call data if the app was launched by clicking "Answer" from a killed state. |
145
+ | subscribe(onAccept, onReject)| All | Listens for native button presses (Answer/End/Failed) and returns a cleanup function. |
144
146
 
145
147
  ---
146
148
 
147
149
  # Implementation Notes
148
150
 
149
- 1. Android Persistence:
150
- 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()'.
151
-
152
- 2. Android Overlay:
151
+ 1. Android Overlay:
153
152
  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.
154
153
 
155
- 3. iOS CallKit:
154
+ 2. iOS CallKit:
156
155
  On iOS, 'displayCall' uses the native system CallKit UI. This works automatically in the background and on the lockscreen without extra overlay permissions.
157
156
 
158
- 4. Single Call Gate:
157
+ 3. Single Call Gate:
159
158
  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.
160
159
  ---
161
160
 
@@ -61,7 +61,6 @@ class CallModule(
61
61
  }
62
62
  }
63
63
 
64
- // Inside your CallModule class
65
64
  @ReactMethod
66
65
  fun checkOverlayPermission(promise: Promise) {
67
66
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -71,6 +70,16 @@ class CallModule(
71
70
  }
72
71
  }
73
72
 
73
+ @ReactMethod
74
+ fun reportRemoteEnded(
75
+ uuid: String,
76
+ reason: String,
77
+ promise: Promise,
78
+ ) {
79
+ NativeCallManager.dismissIncomingCall(reactApplicationContext, uuid)
80
+ promise.resolve(true)
81
+ }
82
+
74
83
  @ReactMethod
75
84
  fun requestOverlayPermission() {
76
85
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -86,10 +95,39 @@ class CallModule(
86
95
  }
87
96
  }
88
97
 
89
- /**
90
- * Combined Validity Check:
91
- * Used by Headless Task to see if it should proceed with the UI.
92
- */
98
+ @ReactMethod
99
+ fun checkFullScreenIntentPermission(promise: Promise) {
100
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
101
+ val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
102
+ promise.resolve(notificationManager.canUseFullScreenIntent())
103
+ } else {
104
+ // Automatically true for Android 13 and below
105
+ promise.resolve(true)
106
+ }
107
+ }
108
+
109
+ @ReactMethod
110
+ fun openFullScreenIntentSettings() {
111
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
112
+ try {
113
+ val intent =
114
+ Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
115
+ data = Uri.fromParts("package", reactApplicationContext.packageName, null)
116
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
117
+ }
118
+ reactApplicationContext.startActivity(intent)
119
+ } catch (e: Exception) {
120
+ // Fallback for some OEM skins that might not support the direct intent
121
+ val intent =
122
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
123
+ data = Uri.fromParts("package", reactApplicationContext.packageName, null)
124
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
125
+ }
126
+ reactApplicationContext.startActivity(intent)
127
+ }
128
+ }
129
+ }
130
+
93
131
  @ReactMethod
94
132
  fun checkCallValidity(
95
133
  uuid: String,
@@ -107,22 +145,6 @@ class CallModule(
107
145
  promise.resolve(map)
108
146
  }
109
147
 
110
- @ReactMethod
111
- fun stopForegroundService(promise: Promise) {
112
- try {
113
- // Using the current react context to stop the service
114
- val intent = Intent(reactApplicationContext, CallForegroundService::class.java)
115
- reactApplicationContext.stopService(intent)
116
- promise.resolve(true)
117
- } catch (e: Exception) {
118
- promise.reject("SERVICE_STOP_ERROR", e.message)
119
- }
120
- }
121
-
122
- /**
123
- * General Status Check:
124
- * Useful for checking if the UI is still relevant.
125
- */
126
148
  @ReactMethod
127
149
  fun checkCallStatus(
128
150
  uuid: String,
@@ -14,13 +14,15 @@ import androidx.core.app.Person
14
14
  import androidx.core.content.ContextCompat
15
15
 
16
16
  object NativeCallManager {
17
- const val channelId = "CALL_CHANNEL_V15_URGENT"
17
+ const val channelId = "CALL_CHANNEL_V0_URGENT"
18
18
  private var currentCallData: Map<String, String>? = null
19
19
 
20
20
  @JvmStatic internal var pendingCallNotification: Notification? = null
21
21
 
22
22
  @JvmStatic internal var pendingNotificationId: Int? = null
23
23
 
24
+ fun getCurrentCallData(): Map<String, String>? = currentCallData
25
+
24
26
  fun handleIncomingPush(
25
27
  context: Context,
26
28
  data: Map<String, String>,
@@ -100,6 +102,7 @@ object NativeCallManager {
100
102
  .setOngoing(true)
101
103
  .setFullScreenIntent(fullScreenPendingIntent, true)
102
104
  .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, rejectPendingIntent, answerPendingIntent))
105
+ .setAutoCancel(false)
103
106
  .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
104
107
 
105
108
  val notification = builder.build()
@@ -121,6 +124,7 @@ object NativeCallManager {
121
124
  ) {
122
125
  pendingCallNotification = null
123
126
  pendingNotificationId = null
127
+ currentCallData = null
124
128
  val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
125
129
  uuid?.let { manager.cancel(it.hashCode()) }
126
130
  context.stopService(Intent(context, CallUiForegroundService::class.java))
@@ -3,22 +3,24 @@ package com.rnsnativecall
3
3
  import android.content.BroadcastReceiver
4
4
  import android.content.Context
5
5
  import android.content.Intent
6
+ import android.util.Log
6
7
 
7
8
  class UnlockReceiver : BroadcastReceiver() {
8
9
  override fun onReceive(
9
10
  context: Context,
10
11
  intent: Intent,
11
12
  ) {
12
- android.util.Log.d("UnlockReceiver", "Device Unlocked! Action: ${intent.action}")
13
-
14
13
  if (intent.action == Intent.ACTION_USER_PRESENT) {
15
- // val activeData = NativeCallManager.getCurrentCallData()
14
+ Log.d("UnlockReceiver", "Device Unlocked - Re-triggering Notification Pill")
15
+
16
+ val activeData = NativeCallManager.getCurrentCallData()
16
17
 
17
- // if (activeData != null) {
18
- // NativeCallManager.handleIncomingPush(context, activeData)
19
- // } else {
20
- // android.util.Log.d("UnlockReceiver", "No active call data found to re-trigger.")
21
- // }
18
+ if (activeData != null) {
19
+ // We don't launch the app!
20
+ // We just tell the Manager to show the notification again.
21
+ // This forces the "Heads-Up" (Pill) to appear on the home screen.
22
+ NativeCallManager.handleIncomingPush(context, activeData)
23
+ }
22
24
  }
23
25
  }
24
26
  }
package/index.d.ts CHANGED
@@ -48,7 +48,7 @@ export interface CallHandlerType {
48
48
  /**
49
49
  * Registers the background "Headless" task.
50
50
  * This is the bridge between a Native push notification and your React Native UI.
51
- * * @param callback Async function receiving call data and the event type.
51
+ * @param callback Async function receiving call data and the event type.
52
52
  */
53
53
  registerHeadlessTask(
54
54
  callback: (data: CallData, eventType: CallEventType) => Promise<void>
@@ -57,7 +57,7 @@ export interface CallHandlerType {
57
57
  /**
58
58
  * Displays the full-screen Native Incoming Call UI.
59
59
  * Usually called inside registerHeadlessTask after validating the call.
60
- * * @param uuid The unique call ID.
60
+ * @param uuid The unique call ID.
61
61
  * @param name Name to display on the call screen.
62
62
  * @param callType Defaults to 'audio'.
63
63
  * @returns Promise resolving to true if the UI was successfully launched.
@@ -65,7 +65,7 @@ export interface CallHandlerType {
65
65
  displayCall(
66
66
  uuid: string,
67
67
  name: string,
68
- callType: 'audio' | 'video',
68
+ callType?: 'audio' | 'video',
69
69
  ): Promise<boolean>;
70
70
 
71
71
  /** * Checks if a call is still valid and has not been canceled.
@@ -78,12 +78,36 @@ export interface CallHandlerType {
78
78
  */
79
79
  checkCallStatus(uuid: string): Promise<CallStatus>;
80
80
 
81
- requestOverlayPermission(): Promise<boolean>;
81
+ /**
82
+ * [Android 14+] Checks if the user has granted the special "Full Screen Intent" permission.
83
+ * This is required for the call to show over the lockscreen.
84
+ */
85
+ checkFullScreenPermission(): Promise<boolean>;
82
86
 
83
- reportRemoteEnded(uuid: string, reason: number): void;
87
+ /**
88
+ * [Android 14+] Opens the specific system settings page for the user to
89
+ * manually enable "Full Screen Intent" if it was revoked.
90
+ */
91
+ openFullScreenSettings(): void;
84
92
 
93
+ /**
94
+ * Checks if the app has permission to draw over other apps (Overlay).
95
+ * Necessary for showing the "Pill" or answer buttons while the phone is unlocked.
96
+ */
85
97
  checkOverlayPermission(): Promise<boolean>;
86
98
 
99
+ /**
100
+ * Opens system settings to request the "Draw over other apps" (Overlay) permission.
101
+ */
102
+ requestOverlayPermission(): void;
103
+
104
+ /**
105
+ * Reports to the native side that the remote party has terminated the call.
106
+ * @param uuid The unique call ID.
107
+ * @param reason Custom string reason (e.g., "RemoteEnded", "TimedOut").
108
+ */
109
+ reportRemoteEnded(uuid: string, reason?: string): Promise<void>;
110
+
87
111
  /**
88
112
  * Forcefully dismisses the native call UI, stops the ringtone, and clears state.
89
113
  * Use this when the call is hung up or timed out.
@@ -105,7 +129,7 @@ export interface CallHandlerType {
105
129
 
106
130
  /**
107
131
  * Subscribes to user interactions on the Native Call UI.
108
- * * @param onAccept Callback when user presses the Answer button.
132
+ * @param onAccept Callback when user presses the Answer button.
109
133
  * @param onReject Callback when user presses the Decline button.
110
134
  * @param onFailed Callback for system errors (optional).
111
135
  * @returns A cleanup function to unsubscribe.
package/index.js CHANGED
@@ -2,12 +2,18 @@ import {
2
2
  NativeModules,
3
3
  NativeEventEmitter,
4
4
  AppRegistry,
5
+ Platform,
5
6
  } from 'react-native';
6
7
 
7
8
  const { CallModule } = NativeModules;
8
9
  const callEventEmitter = CallModule ? new NativeEventEmitter(CallModule) : null;
9
10
 
10
11
  export const CallHandler = {
12
+ /**
13
+ * REGISTER HEADLESS TASK
14
+ * This is the "Engine" that listens for incoming push notifications
15
+ * while the app is killed or in the background.
16
+ */
11
17
  registerHeadlessTask: (onAction) => {
12
18
  AppRegistry.registerHeadlessTask('ColdStartCallTask', () => async (data) => {
13
19
  const { callUuid, isBusySignal } = data;
@@ -19,64 +25,81 @@ export const CallHandler = {
19
25
  return;
20
26
  }
21
27
 
22
- // CallModule.checkCallValidity is now accessible via the native bridge
28
+ // Check if the call is still valid before showing UI
23
29
  const status = await CallModule.checkCallValidity(uuid);
24
30
  if (!status.isValid) {
25
31
  if (onAction) await onAction(data, 'ABORTED_CALL');
26
32
  return;
27
33
  }
28
34
 
29
- if (onAction) {
30
- await onAction(data, 'INCOMING_CALL');
31
- }
35
+ if (onAction) await onAction(data, 'INCOMING_CALL');
32
36
  } catch (error) {
33
37
  console.error('[RNSNativeCall] Headless Task Error:', error);
34
38
  }
35
39
  });
36
40
  },
37
41
 
38
- // --- Added missing bridge methods ---
39
- checkCallValidity: async (uuid) => {
40
- if (!CallModule?.checkCallValidity) return { isValid: false, isCanceled: true };
41
- return await CallModule.checkCallValidity(uuid.toLowerCase().trim());
42
- },
43
-
44
- checkCallStatus: async (uuid) => {
45
- if (!CallModule?.checkCallStatus) return { isCanceled: true, isActive: false, shouldDisplay: false };
46
- return await CallModule.checkCallStatus(uuid.toLowerCase().trim());
47
- },
48
- // ------------------------------------
42
+ // --- PERMISSION HEALTH CHECKS ---
49
43
 
50
- reportRemoteEnded: async (uuid, endReason) => {
51
- if (!CallModule?.reportRemoteEnded) return;
52
- await CallModule.reportRemoteEnded(uuid.toLowerCase().trim(), endReason);
44
+ checkFullScreenPermission: async () => {
45
+ if (Platform.OS !== 'android' || !CallModule?.checkFullScreenIntentPermission) return true;
46
+ return await CallModule.checkFullScreenIntentPermission();
53
47
  },
54
48
 
55
- requestOverlayPermission: async () => {
56
- if (!CallModule?.requestOverlayPermission) return false;
57
- return await CallModule.requestOverlayPermission();
49
+ openFullScreenSettings: () => {
50
+ if (Platform.OS === 'android' && CallModule?.openFullScreenIntentSettings) {
51
+ CallModule.openFullScreenIntentSettings();
52
+ }
58
53
  },
59
54
 
60
55
  checkOverlayPermission: async () => {
61
- if (!CallModule?.checkOverlayPermission) return false;
56
+ if (Platform.OS !== 'android' || !CallModule?.checkOverlayPermission) return true;
62
57
  return await CallModule.checkOverlayPermission();
63
58
  },
64
59
 
60
+ requestOverlayPermission: () => {
61
+ if (Platform.OS === 'android' && CallModule?.requestOverlayPermission) {
62
+ CallModule.requestOverlayPermission();
63
+ }
64
+ },
65
+
66
+ // --- CALL LIFECYCLE ACTIONS ---
67
+
65
68
  displayCall: async (uuid, name, callType = "audio") => {
66
69
  if (!CallModule) return false;
67
70
  return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
68
71
  },
69
72
 
73
+ /** Stops the UI and the Foreground Service */
74
+ destroyNativeCallUI: (uuid) => {
75
+ if (CallModule?.endNativeCall) {
76
+ CallModule.endNativeCall(uuid.toLowerCase().trim());
77
+ }
78
+ },
79
+
80
+ /** Reports to the native side that the other person hung up */
81
+ reportRemoteEnded: async (uuid, endReason = "RemoteEnded") => {
82
+ if (CallModule?.reportRemoteEnded) {
83
+ await CallModule.reportRemoteEnded(uuid.toLowerCase().trim(), endReason);
84
+ }
85
+ },
86
+
70
87
  stopForegroundService: async () => {
71
88
  if (CallModule?.stopForegroundService) {
72
89
  await CallModule.stopForegroundService();
73
90
  }
74
91
  },
75
92
 
76
- destroyNativeCallUI: (uuid) => {
77
- if (CallModule?.endNativeCall) {
78
- CallModule.endNativeCall(uuid.toLowerCase().trim());
79
- }
93
+ // --- STATE VERIFICATION ---
94
+
95
+ checkCallValidity: async (uuid) => {
96
+ if (!CallModule?.checkCallValidity) return { isValid: false, isCanceled: true };
97
+ return await CallModule.checkCallValidity(uuid.toLowerCase().trim());
98
+ },
99
+
100
+ checkCallStatus: async (uuid) => {
101
+ if (!CallModule?.checkCallStatus) return { isCanceled: true, isActive: false, shouldDisplay: false };
102
+ return await CallModule.checkCallStatus(uuid.toLowerCase().trim());
80
103
  },
81
104
 
82
105
  getInitialCallData: async () => {
@@ -84,6 +107,8 @@ export const CallHandler = {
84
107
  return await CallModule.getInitialCallData();
85
108
  },
86
109
 
110
+ // --- EVENT SUBSCRIPTION ---
111
+
87
112
  subscribe: (onAccept, onReject, onFailed) => {
88
113
  if (!callEventEmitter) return () => { };
89
114
  const subs = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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",