VoIP Add-on
Native incoming call UI with CallKit (iOS) and CallStyle (Android) for voice and video calling apps. No FCM or Google Play Services required.
Native incoming call UI with CallKit (iOS) and CallStyle (Android) for voice and video calling apps.
Works on all devices including Huawei and non-Google Android phones.
Flutter#
The VoIP add-on is a separate package, abrova_push_voip, used alongside abrova_push:
Both install from Abrova Center:
dependencies:
abrova_push:
hosted: https://center.abrova.ir/api/packages/abrova/pub
version: ^0.1.20
abrova_push_voip:
hosted: https://center.abrova.ir/api/packages/abrova/pub
version: ^0.1.1The Android side comes from the Abrova Center Maven repository you added for abrova_push (see Flutter SDK). On iOS, add the native VoIP SDK inside the app target in ios/Podfile:
pod 'AbrovaPushVoip', :git => 'https://github.com/Abrova-co/abrova-push-voip-ios-sdk.git', :tag => '0.1.2'
iOS Setup
Add to your Info.plist:
<key>UIBackgroundModes</key> <array> <string>voip</string> <string>remote-notification</string> </array>
Enable Push Notifications and Background Modes → Voice over IP, Remote notifications in Xcode under Signing & Capabilities. No AppDelegate changes are needed: the plugin handles PushKit registration and CallKit.
Android Setup
No manifest changes are needed. The call activities, receivers and permissions (USE_FULL_SCREEN_INTENT, VIBRATE, FOREGROUND_SERVICE) are declared by the native ir.abrova:abrova-push-voip library and merged automatically.
Initialize VoIP (Flutter)
import 'package:abrova_push/abrova_push.dart';
import 'package:abrova_push_voip/abrova_push_voip.dart';
// Initialize AbrovaPush first
await AbrovaPush.init(AbrovaPushConfig(apiKey: 'ab_live_your_api_key'));
await AbrovaPush.register();
// Then initialize VoIP with its config and callbacks
await AbrovaPushVoIP.init(
config: AbrovaPushVoIPConfig(
appName: 'MyApp',
callTimeout: 30, // Seconds before an unanswered call times out (default: 60)
ringtone: null, // Custom ringtone (optional)
),
onCallAccepted: (callData) {
print('Call accepted: ${callData.callId}');
print('Caller: ${callData.callerName}');
print('Type: ${callData.mediaType}'); // CallMediaType.audio or .video
// Navigate to your call screen
Navigator.push(context, MaterialPageRoute(
builder: (_) => CallScreen(call: callData),
));
},
onCallDeclined: (callData) {
// Notify your server that call was declined
},
onCallTimeout: (callData) {
// Show missed call notification
},
);
// Let the plugin register its PushKit (VoIP) token with Abrova
final deviceId = await AbrovaPush.getDeviceId();
await AbrovaPushVoIP.setApiKey(apiKey: 'ab_live_your_api_key', deviceId: deviceId);Handle Initial Call (Cold Start)
When the app is launched from an accepted call notification:
// Check for initial call when app starts
final initialCall = await AbrovaPushVoIP.getInitialCall();
if (initialCall != null) {
// App was opened from accepting a call
Navigator.push(context, MaterialPageRoute(
builder: (_) => CallScreen(call: initialCall),
));
}End Call
await AbrovaPushVoIP.endCall(callData.callId); // Report call as connected (for CallKit on iOS) await AbrovaPushVoIP.reportCallConnected(callData.callId);
Call Data Model
class CallData {
final String callId; // Unique call identifier
final String callerName; // Display name
final String? callerAvatar; // Avatar URL
final CallMediaType mediaType; // CallMediaType.audio or CallMediaType.video
final Map<String, dynamic> extra; // Extra data
final DateTime receivedAt; // When call was received
}React Native#
The VoIP add-on is a separate package, used alongside abrova-push-react-native:
Install it from Abrova Center, like the core package:
npm install abrova-push-react-native-voip --registry=https://center.abrova.ir/api/packages/abrova/npm/
On iOS, add the native VoIP SDK inside the app target in ios/Podfile, then run pod install:
pod 'AbrovaPushVoip', :git => 'https://github.com/Abrova-co/abrova-push-voip-ios-sdk.git', :tag => '0.1.2'
iOS Setup
Add VoIP background mode to Info.plist:
<key>UIBackgroundModes</key> <array> <string>voip</string> <string>remote-notification</string> </array>
Initialize VoIP (React Native)
import AbrovaPush from 'abrova-push-react-native';
import AbrovaPushVoip, { CallData } from 'abrova-push-react-native-voip';
await AbrovaPushVoip.init({
appName: 'MyApp',
callTimeout: 30,
});
// Let the plugin register its PushKit (VoIP) token with Abrova (iOS)
const deviceId = await AbrovaPush.getDeviceId();
await AbrovaPushVoip.setApiKey({ apiKey: 'ab_live_your_api_key', deviceId: deviceId ?? undefined });
const unsubAccepted = AbrovaPushVoip.onCallAccepted((call: CallData) => {
navigation.navigate('CallScreen', { call });
});
const unsubDeclined = AbrovaPushVoip.onCallDeclined((call: CallData) => {
// Handle decline
});
const unsubTimeout = AbrovaPushVoip.onCallTimeout((call: CallData) => {
// Handle timeout
});
// Clean up
useEffect(() => {
return () => {
unsubAccepted();
unsubDeclined();
unsubTimeout();
};
}, []);Handle Initial Call
const initialCall = await AbrovaPushVoip.getInitialCall();
if (initialCall) {
navigation.navigate('CallScreen', { call: initialCall });
}Call Control
await AbrovaPushVoip.endCall(call.callId); await AbrovaPushVoip.reportCallConnected(call.callId);
TypeScript Types
interface CallData {
callId: string;
callerName: string;
callerId?: string;
callerAvatar?: string;
callType: 'audio' | 'video';
payload?: Record<string, any>;
timestamp: number;
}
interface VoipConfig {
appName: string;
ringtoneUri?: string;
vibrate?: boolean; // default: true
callTimeout?: number; // seconds, default: 30
callIdKey?: string; // payload key overrides
callerNameKey?: string;
callerIdKey?: string;
callerAvatarKey?: string;
callTypeKey?: string;
}Android#
Add the VoIP module to your build.gradle. It comes from the Abrova Center repository (https://center.abrova.ir/api/packages/abrova/maven) you added for the Android SDK:
dependencies {
implementation("ir.abrova:abrova-push-voip:0.1.0")
}Manifest Configuration
No manifest changes are needed. The library's manifest already declares the permissions (VIBRATE, USE_FULL_SCREEN_INTENT, FOREGROUND_SERVICE), IncomingCallActivity, the Android 12+ trampoline activities (CallTrampolineActivity, DeclineCallActivity) and the receivers, and they are merged into your app automatically.
Initialize VoIP (Android)
import ir.abrova.push.voip.AbrovaPushVoip
import ir.abrova.push.voip.VoipCallback
import ir.abrova.push.voip.VoipConfig
import ir.abrova.push.voip.CallData
// Initialize in Application.onCreate()
AbrovaPushVoip.initialize(
context = this,
config = VoipConfig(
appName = "MyApp",
timeoutSeconds = 30,
showMissedCallNotification = true
)
)
// Set callback
AbrovaPushVoip.setCallback(object : VoipCallback {
override fun onCallAccepted(callData: CallData) {
Log.d("VoIP", "Call accepted: ${callData.callId}")
// Start your call activity
startCallActivity(callData)
}
override fun onCallDeclined(callData: CallData) {
Log.d("VoIP", "Call declined: ${callData.callId}")
}
override fun onCallTimeout(callData: CallData) {
Log.d("VoIP", "Call timeout: ${callData.callId}")
}
override fun onError(error: String) {
Log.e("VoIP", "Error: $error")
}
})Handle Incoming Call from Push (Android)
Incoming voip_call messages are picked up automatically: the library registers a receiver for Abrova Push messages that shows the call UI even when your app is in the background. If you also handle messages yourself, you can hand them to the VoIP module:
AbrovaPush.setCallback(object : AbrovaPushCallbackAdapter() {
override fun onMessageReceived(message: AbrovaPushMessage) {
val data = message.data ?: return
if (AbrovaPushVoip.handlePushMessage(data)) return
// Handle other messages
}
})Manual Call Trigger (Android)
val callData = CallData(
callId = "unique-call-id",
callerName = "John Doe",
callerId = "user-123",
callerAvatar = "https://example.com/avatar.jpg",
callType = "video"
)
AbrovaPushVoip.showIncomingCall(callData)Call Control (Android)
// End call
AbrovaPushVoip.endCall(callId)
// Report call as connected
AbrovaPushVoip.reportCallConnected(callId)
// Get initial call (if app was cold started from call)
val initialCall = AbrovaPushVoip.getInitialCall()
initialCall?.let { startCallActivity(it) }iOS#
Add AbrovaPushVoip with Swift Package Manager. In Xcode: File → Add Package Dependencies → enter the repository URL, choose version 0.1.2 or later and add the AbrovaPushVoip library to your app target:
https://github.com/Abrova-co/abrova-push-voip-ios-sdk.git
Capabilities
In Xcode, add these capabilities under Signing & Capabilities:
- Background Modes: Voice over IP, Remote notifications
- Push Notifications
Info.plist
<key>UIBackgroundModes</key> <array> <string>voip</string> <string>remote-notification</string> </array>
Initialize VoIP (iOS)
import AbrovaPushVoip
// In AppDelegate.didFinishLaunchingWithOptions
let config = VoipConfig(
appName: "MyApp",
callTimeout: 30,
supportsVideo: true,
ringtoneName: "ringtone.caf",
iconTemplateName: "CallIcon"
)
AbrovaPushVoip.shared.initialize(config: config)
AbrovaPushVoip.shared.delegate = self
AbrovaPushVoip.shared.registerForVoIPPushes()Implement Delegate (iOS)
extension AppDelegate: AbrovaPushVoipDelegate {
func voip(_ voip: AbrovaPushVoip, didAcceptCall callData: VoipCallData) {
print("Call accepted: \(callData.callId)")
// Navigate to call screen
DispatchQueue.main.async {
self.showCallScreen(callData)
}
}
func voip(_ voip: AbrovaPushVoip, didDeclineCall callData: VoipCallData) {
print("Call declined: \(callData.callId)")
// Notify server
}
func voip(_ voip: AbrovaPushVoip, didTimeoutCall callData: VoipCallData) {
print("Call timeout: \(callData.callId)")
// Show missed call
}
func voip(_ voip: AbrovaPushVoip, didFailWithError error: String) {
print("VoIP error: \(error)")
}
}Handle Initial Call (iOS)
if let initialCall = AbrovaPushVoip.shared.getInitialCall() {
showCallScreen(initialCall)
AbrovaPushVoip.shared.clearInitialCall()
}Call Control (iOS)
AbrovaPushVoip.shared.endCall(callId: callData.callId) AbrovaPushVoip.shared.reportCallConnected(callId: callData.callId)
Get VoIP Token (iOS)
// Read the current PushKit (VoIP) token
if let token = AbrovaPushVoip.shared.getVoIPToken() {
print("VoIP token: \(token)")
}The native AbrovaPushVoip SDK does not upload this token to Abrova (the Flutter and React Native plugins do, through setApiKey). Abrova sends VoIP pushes to the VoIP token stored for the device, which the core AbrovaPush SDK uploads when it is initialized with usePushKit: true.
Web#
VoIP calling features are only available for native mobile platforms (iOS, Android, Flutter, React Native). Web applications should use WebRTC directly for voice/video calls.
Server-Side: Send VoIP Call#
Node.js SDK
import { AbrovaPush } from '@abrova/push-node';
const abrovaPush = new AbrovaPush({
apiKey: 'ab_live_your_api_key',
serverSecret: 'ab_srv_your_server_secret',
});
await abrovaPush.push.sendToDevice({
deviceId: 'DEVICE_ID',
title: 'Incoming call',
body: 'John Doe is calling',
data: {
type: 'voip_call',
callId: 'unique-call-123',
callerName: 'John Doe',
callerId: 'user-456',
callerAvatar: 'https://example.com/avatar.jpg',
callType: 'video',
},
});REST API
curl -X POST https://push-api.abrova.ir/push/send \
-H "x-api-key: ab_live_your_api_key" \
-H "x-server-secret: ab_srv_your_server_secret" \
-H "Content-Type: application/json" \
-d '{
"deviceId": "DEVICE_ID",
"title": "Incoming call",
"body": "John Doe is calling",
"data": {
"type": "voip_call",
"callId": "unique-call-123",
"callerName": "John Doe",
"callerId": "user-456",
"callerAvatar": "https://example.com/avatar.jpg",
"callType": "video"
}
}'Payload Fields#
| Option | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | Must be "voip_call" (system trigger) |
| callId | string | No | Unique call identifier; a random ID is generated if missing |
| callerName | string | No | Display name of caller (falls back to "Unknown") |
| callerId | string | No | User ID of caller |
| callerAvatar | string | No | Avatar URL (shown in notification and call screen) |
| callType | string | No | "audio" or "video" (default: audio) |
The type: "voip_call" field is the system trigger — it tells the SDK to show the incoming call UI. This value is fixed and cannot be changed. The other fields (callerName, callerId, etc.) are configurable via VoipConfig payload key mappings. The request itself also needs title and body (or a templateId) like any push.
How It Works#
- Server sends VoIP notification — Your server sends a push with
type: "voip_call"to the device - Native call UI appears — iOS shows CallKit, Android shows CallStyle notification (Android 12+) or full-screen activity
- User accepts or declines — Your callback is triggered with the call data
- Navigate to your call screen — Use WebRTC or your preferred calling solution for the actual call