High-performance React Native module for handling native VoIP call UI on Android and iOS.
npm install rns-nativecallA professional VoIP incoming call handler for React Native with full Android & iOS native UI integration.
Designed for production-grade apps requiring CallKit, lockscreen handling, headless execution, and single-call enforcement.
---
- ๐ฆ Expo Ready โ Zero manual native setup via config plugin
- โ๏ธ Single Call Gate โ Automatically blocks concurrent calls and emits BUSY events
- ๐ง Headless Mode โ Works when the app is killed, backgrounded, or screen locked
- ๐ฑ Native UI โ Full-screen Android Activity & iOS CallKit
- ๐ System-Level Reliability โ No JS race conditions, no ghost calls
---
``bash`
npx expo install rns-nativecall expo-build-properties react-native-uuid
or
`bash`
npm install rns-nativecall expo-build-properties react-native-uuid
---
Add the plugin to app.json or app.config.js:
`json`
{
"expo": {
"plugins": [
"rns-nativecall",
[
"expo-build-properties",
{
"android": {
"enableProguardInReleaseBuilds": true,
"extraProguardRules": "-keep class com.rnsnativecall.* { ; }\n-keep class com.facebook.react.HeadlessJsTaskService { *; }"
}
}
],
[
"expo-notifications",
{
"icon": "./assets/notification_icon.png",
"color": "#218aff",
"iosDisplayInForeground": true,
"androidPriority": "high",
"androidVibrate": true,
"androidSound": true,
"androidImportance": "high",
"androidLightColor": "#218aff",
"androidVisibility": "public"
}
]
]
}
}
---
This enables background handling, busy-state signaling, and cold-start execution.
`javascript
import { AppRegistry } from 'react-native';
import App from './App';
import { CallHandler } from 'rns-nativecall';
CallHandler.registerHeadlessTask(async (data, eventType) => {
if (eventType === 'BUSY') {
console.log("User already in call:", data.callUuid);
return;
}
if (eventType === 'INCOMING_CALL') {
console.log("Incoming call payload:", data);
}
});
AppRegistry.registerComponent('main', () => App);
`
---
`javascript`
CallHandler.subscribe(
async (data) => {
console.log("CALL ACCEPTED", data);
},
async (data) => {
console.log("CALL REJECTED", data);
},
(data) => {
console.log("CALL FAILED", data);
}
);
---
`javascript
import React, { useEffect } from 'react';
import { CallHandler } from 'rns-nativecall';
export default function App() {
useEffect(() => {
const unsubscribe = CallHandler.subscribe(
(data) => {
console.log("Call Accepted:", data.callUuid);
},
(data) => {
console.log("Call Rejected:", data.callUuid);
}
);
return () => unsubscribe();
}, []);
const showCall = () => {
CallHandler.displayCall("unique-uuid", "Caller Name", "video");
};
return
}
`
---
| Method | Platform | Description |
|------|---------|-------------|
| registerHeadlessTask(cb) | All | Registers background task (INCOMING_CALL, BUSY, ABORTED_CALL) |displayCall(uuid, name, type)
| | All | Shows native incoming call UI |destroyNativeCallUI(uuid)
| | All | Ends native UI / CallKit session |subscribe(onAccept, onReject, onFail)
| | All | Listen to call actions |showMissedCall(uuid, name, type)
| | Android | Persistent missed call notification |showOnGoingCall(uuid, name, type)
| | Android | Persistent ongoing call notification |
---
| Method | Platform | Description |
|------|---------|-------------|
| getInitialCallData() | All | Retrieve payload from cold start |checkCallValidity(uuid)
| | All | Prevent ghost calls |checkCallStatus(uuid)
| | All | Sync UI with native state |
---
| Method | Description |
|------|-------------|
| checkOverlayPermission() | Check lockscreen overlay permission |requestNotificationPermission()
| | Check notification permission |requestOverlayPermission()
| | Open overlay settings |checkFullScreenPermission()
| | Android 14+ full screen intent |requestFullScreenSettings()
| | Android 14+ permission screen |
---
via headless task.---
๐งช Full Example
`js
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Alert } from 'react-native';
import { CallHandler } from 'rns-nativecall';
import uuid from 'react-native-uuid';export default function App() {
const [activeCall, setActiveCall] = useState(null);
useEffect(() => {
// 1. Handle app launched from a notification "Answer" click
CallHandler.getInitialCallData().then((data) => {
if (data && data.default) {
console.log("App launched from call:", data.default);
setActiveCall(data.default);
}
});
// 2. Subscribe to foreground events
const unsubscribe = CallHandler.subscribe(
(data) => {
console.log("Call Accepted:", data.callUuid);
setActiveCall(data);
// Logic: Open your Video/Audio Call UI here
},
(data) => {
console.log("Call Rejected/Ended:", data.callUuid);
setActiveCall(null);
}
);
return () => unsubscribe();
}, []);
const startTestCall = async () => {
// 1. Check/Request Notifications first (Standard Popup)
const hasNotify = await CallHandler.requestNotificationPermission();
if (!hasNotify) {
Alert.alert("Permission Required", "Notifications must be enabled to receive calls.");
return;
}
// 2. Check Overlay (Special System Setting)
const hasOverlay = await CallHandler.checkOverlayPermission();
if (!hasOverlay) {
Alert.alert(
"Display Over Other Apps",
"To see calls while using other apps or when locked, please enable the 'Overlay' permission.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Go to Settings",
onPress: () => CallHandler.requestOverlayPermission()
}
]
);
return; // Stop here; the user is now in Settings
}
// 3. Success! Both permissions are active
CallHandler.displayCall(
uuid.v4(),
"John Doe",
"video"
);
};
const endCallManually = () => {
if (activeCall) {
CallHandler.stopForegroundService();
setActiveCall(null);
}
};
return (
RNS Native Call Pro
{activeCall ? (
Active Call with: {activeCall.name}
End Call
) : (
Simulate Incoming Call
)}
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF' },
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
callBox: { padding: 20, backgroundColor: '#e1f5fe', borderRadius: 10, alignItems: 'center' },
btnStart: { backgroundColor: '#4CAF50', padding: 15, borderRadius: 5 },
btnEnd: { backgroundColor: '#F44336', padding: 15, borderRadius: 5, marginTop: 10 },
btnText: { color: 'white', fontWeight: 'bold' }
});
``---
MIT License
---
Built for production VoIP apps, not demos.