vonage_voice 1.0.0
vonage_voice: ^1.0.0 copied to clipboard
Native Vonage voice calling for Flutter. Includes full background support, native call UI (CallKit/ConnectionService), and in-call controls.
vonage_voice #
A Flutter plugin for making and receiving voice calls using the Vonage Client SDK. It integrates natively with Android's Telecom ConnectionService and iOS CallKit, delivering full-screen incoming call screens, background call delivery via FCM/PushKit, and complete in-call controls β all through a clean Dart API.
β¨ Features #
- π Place and receive VoIP calls on Android and iOS
- π Background incoming call delivery via FCM (Android) and PushKit (iOS)
- π± Native call screen β Android Telecom
ConnectionService+ iOS CallKit - π Secure JWT-based authentication (no credentials stored in the app)
- π Mute / unmute microphone during a live call
- π Toggle speakerphone and Bluetooth audio routing
- β¨οΈ Send DTMF tones for IVR navigation
- π‘ Rich
CallEventstream covering incoming, ringing, connected, reconnecting, ended, missed, and more - β‘ Drop-in migration path from the Twilio Voice Flutter plugin (
TwilioVoice.instanceβVonageVoice.instance)
ποΈ Architecture Flow #
ββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
β Your Backend β ββJWTββΆ β Flutter App β ββCallββΆβ Vonage Platform β
ββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
β² β β
β βΌ βΌ
(Mint Auth Token) (Native SDK / Telecom) (Route Media / Push)
The Flutter client never talks to Vonage directly for authentication. Your own backend mints a short-lived JWT using your Vonage Application ID and private.key, then returns it to the app. The app passes that token to VonageVoice.instance.setTokens() to establish the session.
π οΈ Vonage Dashboard Setup #
Before writing any code, configure a Vonage application:
- Create a Vonage Developer account at dashboard.nexmo.com.
- Go to Applications β Create a new application.
- Give it a name, then toggle Voice capability on.
- Set your Answer URL and Event URL (HTTP endpoints on your backend that Vonage calls to fetch your NCCO and receive call state webhooks).
- Click Generate public and private key β this downloads
private.key. Store it securely on your backend; never ship it in a mobile app. - Save the application and note your Application ID.
π¦ Installation #
flutter pub add vonage_voice
Android Setup #
The plugin's AndroidManifest.xml is merged automatically, so manifest entries and static permissions are already declared. You must still complete the Firebase configuration, request runtime permissions, and enable system-level settings during your app's onboarding flow.
1. Firebase Configuration
Add the google-services plugin to your app-level build.gradle (required for FCM incoming calls):
// android/app/build.gradle
apply plugin: 'com.google.gms.google-services'
And in your project-level build.gradle:
// android/build.gradle
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
Add google-services.json β Download it from your Firebase project and place it at android/app/google-services.json. This enables FCM so incoming calls are delivered when the app is in the background or killed.
The plugin requires minSdkVersion 24 or higher. Set this in
android/app/build.gradle:defaultConfig { minSdkVersion 24 }
2. Runtime Permissions
The permissions below are declared in the plugin's manifest but must be requested at runtime by your app. Missing any of them will silently break call delivery or audio.
| Permission | API Level | Purpose |
|---|---|---|
RECORD_AUDIO |
All | Microphone access β required for call audio |
READ_PHONE_STATE |
All | Integration with Android Telecom services |
CALL_PHONE |
All | Required for placing outgoing calls through Telecom |
MANAGE_OWN_CALLS |
All | Essential for self-managed ConnectionService integration |
READ_PHONE_NUMBERS |
All | Device and OEM compatibility with PhoneAccount flows |
POST_NOTIFICATIONS |
API 33+ (Android 13+) | Required for incoming call and missed call notifications |
Request them during your onboarding flow using the plugin helpers:
await VonageVoice.instance.requestMicAccess();
await VonageVoice.instance.requestReadPhoneStatePermission();
await VonageVoice.instance.requestCallPhonePermission();
await VonageVoice.instance.requestManageOwnCallsPermission();
await VonageVoice.instance.requestReadPhoneNumbersPermission();
await VonageVoice.instance.requestNotificationPermission();
3. System Settings
Beyond runtime permissions, several OS-level settings must be enabled for the native call stack to function reliably. These cannot be granted silently β each one opens a system settings page for the user to confirm.
| Setting | Why It Is Required | Plugin Method |
|---|---|---|
| Phone Account Registration | Android Telecom requires a registered PhoneAccount to route calls through ConnectionService |
registerPhoneAccount() |
| Phone Account Enabled | Users can disable the account in system settings; calls will not surface if it is disabled | isPhoneAccountEnabled() / openPhoneAccountSettings() |
| Battery Optimization Exemption | Android will kill the app's background process on restricted OEMs, preventing incoming call delivery | requestBatteryOptimizationExemption() |
| Overlay Permission | Improves lock-screen launch reliability on Samsung, MIUI, and similar OEM skins | openOverlaySettings() |
β οΈ OEM Background Restrictions: Devices from Vivo, OPPO, Xiaomi, realme, and Samsung ship aggressive battery managers that will kill your app's background process even with
FOREGROUND_SERVICEdeclared. Battery optimization exemption is not optional on these devices β without it, incoming calls will not be delivered in the killed state.
π‘ Tip: Check each setting before prompting β only open the system page if the current state requires action. This avoids unnecessary interruptions during onboarding.
4. Onboarding Code Example
Call this function once during your login or onboarding flow, before calling setTokens():
Future<void> prepareAndroidOnboarding() async {
// Step 1 β Runtime permissions
await VonageVoice.instance.requestMicAccess();
await VonageVoice.instance.requestReadPhoneStatePermission();
await VonageVoice.instance.requestCallPhonePermission();
await VonageVoice.instance.requestManageOwnCallsPermission();
await VonageVoice.instance.requestReadPhoneNumbersPermission();
await VonageVoice.instance.requestNotificationPermission();
// Step 2 β Phone account (required for Telecom integration)
if (!await VonageVoice.instance.hasRegisteredPhoneAccount()) {
await VonageVoice.instance.registerPhoneAccount();
}
if (!await VonageVoice.instance.isPhoneAccountEnabled()) {
await VonageVoice.instance.openPhoneAccountSettings();
}
// Step 3 β Battery optimization (critical for background/killed-state delivery)
if (await VonageVoice.instance.isBatteryOptimized()) {
await VonageVoice.instance.requestBatteryOptimizationExemption();
}
// Step 4 β Overlay permission (Samsung/MIUI lock-screen reliability)
if (!await VonageVoice.instance.canDrawOverlays()) {
await VonageVoice.instance.openOverlaySettings();
}
}
iOS Setup #
1. Add capabilities in Xcode for your Runner target:
- Background Modes β enable
Voice over IPandRemote notifications - Push Notifications
2. Add keys to Info.plist:
<!-- ios/Runner/Info.plist -->
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for voice calls.</string>
3. Minimum deployment target: iOS 15.0.
iOS 15 is required by the Vonage Client SDK. The Vonage Voice SDK 2.x (Swift Package and CocoaPod) declares an iOS 15 minimum, so this plugin targets iOS 15.0 on both integration paths. iOS 13/14 are no longer supported.
The plugin depends on VonageClientSDKVoice ~> 2.3, CallKit, PushKit, and
AVFoundation β all pulled in automatically by whichever integration path you use.
4. Choose an integration path β CocoaPods or Swift Package Manager.
The plugin supports both; pick whichever your app uses. You do not need to change any Dart or Swift code β the choice only affects how the native dependency is resolved.
CocoaPods (default)
No extra steps. flutter build ios / flutter run resolves the plugin and the
Vonage SDK through your app's Podfile automatically. Ensure your app's
Podfile targets iOS 15:
# ios/Podfile
platform :ios, '15.0'
Swift Package Manager
Enable Flutter's SPM integration once, then build as usual:
flutter config --enable-swift-package-manager
flutter build ios
Flutter resolves the plugin's Package.swift and pulls the official Vonage
Swift Package (VonageClientSDKVoice) automatically. You can confirm it
appears under Package Dependencies in Xcode's Project Navigator.
To switch back to CocoaPods at any time:
flutter config --no-enable-swift-package-manager
Migration note for existing users: nothing changes unless you opt into SPM β CocoaPods remains fully supported. The only behavioral change is the iOS 15 minimum, which comes from the Vonage SDK, not from SPM.
π Quick Start #
1. Initialize β Register JWT and device token #
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:vonage_voice/vonage_voice.dart';
Future<void> registerForCalls() async {
final jwt = await yourBackend.getVonageJwt();
String? deviceToken;
if (Platform.isAndroid) {
deviceToken = await FirebaseMessaging.instance.getToken();
}
await VonageVoice.instance.setTokens(
accessToken: jwt,
deviceToken: deviceToken,
isSandbox: false, // set true for iOS development/debug builds
);
}
2. Listen to call events #
VonageVoice.instance.callEventsListener.listen((CallEvent event) {
switch (event) {
case CallEvent.incoming:
final call = VonageVoice.instance.call.activeCall;
print('Incoming call from ${call?.fromFormatted}');
break;
case CallEvent.connected:
print('Call connected');
break;
case CallEvent.callEnded:
print('Call ended');
break;
case CallEvent.missedCall:
print('Missed call');
break;
default:
break;
}
});
3. Place an outgoing call #
await VonageVoice.instance.call.place(
from: 'alice', // your Vonage user identity
to: '+14155551234', // E.164 phone number or Vonage user identity
extraOptions: { // optional β forwarded to your NCCO answer URL
'displayName': 'Alice',
},
);
4. Answer / hang up #
// Answer an incoming call
await VonageVoice.instance.call.answer();
// Hang up (works for both inbound and outbound)
await VonageVoice.instance.call.hangUp();
5. In-call controls #
// Mute / unmute microphone
await VonageVoice.instance.call.toggleMute(true);
await VonageVoice.instance.call.toggleMute(false);
// Toggle speakerphone
await VonageVoice.instance.call.toggleSpeaker(true);
await VonageVoice.instance.call.toggleSpeaker(false);
6. Handle token refresh #
// Called when FCM (Android) or PushKit (iOS) rotates the push token
VonageVoice.instance.setOnDeviceTokenChanged((String newToken) {
myBackend.updatePushToken(newToken);
});
// Proactively refresh an expiring JWT (keep session alive)
final freshJwt = await yourBackend.getVonageJwt();
await VonageVoice.instance.refreshSession(accessToken: freshJwt);
7. Sign out #
await VonageVoice.instance.unregister();
π API Reference #
VonageVoice.instance β Session management #
| Method | Parameters | Returns | Description |
|---|---|---|---|
setTokens |
accessToken (required), deviceToken, isSandbox |
Future<bool?> |
Register Vonage JWT and push token. Must be called before any call activity. |
refreshSession |
accessToken (required) |
Future<bool?> |
Refresh an expiring JWT without tearing down the session. |
unregister |
β | Future<bool?> |
Unregister push token and end the Vonage session. |
getDeviceId |
β | Future<String?> |
The Vonage device ID assigned during registration, or null if not yet registered. Useful for targeting a specific device from your backend. |
callEventsListener |
β | Stream<CallEvent> |
Stream of typed call state events from the native layer. |
setOnDeviceTokenChanged |
OnDeviceTokenChanged callback |
void |
Fires when the native push token is rotated by FCM or PushKit. |
showMissedCallNotifications |
bool (setter) |
void |
Enable/disable local missed call notifications. |
VonageVoice.instance.call β Active call controls #
| Method | Parameters | Returns | Description |
|---|---|---|---|
place |
from (required), to (required), extraOptions |
Future<bool?> |
Place an outbound call. |
answer |
β | Future<bool?> |
Answer the current incoming call. |
hangUp |
β | Future<bool?> |
Hang up the active call. |
toggleMute |
isMuted: bool |
Future<bool?> |
Mute or unmute the microphone. |
isMuted |
β | Future<bool?> |
Returns true if the microphone is currently muted. |
toggleSpeaker |
speakerIsOn: bool |
Future<bool?> |
Route audio to/from speakerphone. |
activeCall |
β | ActiveCall? |
The current call's state. Returns null when idle. |
ActiveCall β Call state object #
| Property | Type | Description |
|---|---|---|
from |
String |
Raw caller identity (client: prefix stripped). |
fromFormatted |
String |
Human-readable caller number or identity. |
to |
String |
Raw callee identity. |
toFormatted |
String |
Human-readable callee number or identity. |
callDirection |
CallDirection |
.incoming or .outgoing. |
initiated |
DateTime? |
When the call connected. Set after CallEvent.connected. |
customParams |
Map<String, dynamic>? |
Custom params forwarded from your Vonage backend NCCO. |
CallEvent β Event enum values #
| Value | Description |
|---|---|
incoming |
Incoming call invite received |
ringing |
Outbound call is ringing |
connected |
Call media connected β both parties can speak |
reconnecting |
Call media is recovering from a network interruption |
reconnected |
Call media reconnected |
callEnded |
Call ended (local hangup or remote disconnect) |
mute / unmute |
Microphone muted/unmuted |
speakerOn / speakerOff |
Speakerphone toggled |
bluetoothOn / bluetoothOff |
Bluetooth audio routing changed |
answer |
Incoming call answered |
declined |
Incoming call declined by remote |
missedCall |
Incoming call ended before answered |
deviceLimitExceeded |
Registration failed β max devices limit reached |
permission |
A runtime permission result received |
audioRouteChanged |
Audio route changed (iOS only) |
π§― Troubleshooting (iOS) #
"CocoaPods could not find compatible versions for VonageClientSDKVoice"
Your app's Podfile.lock is pinned to an older SDK. Run
pod update VonageClientSDKVoice (or pod install --repo-update) in
ios/.
SPM: Package Dependencies doesn't appear / plugin not found
Make sure SPM is enabled (flutter config --enable-swift-package-manager),
then flutter clean && flutter pub get && flutter build ios.
Deployment target errors (iOS 13/iOS 14)
The Vonage 2.x SDK requires iOS 15. Set platform :ios, '15.0' in your
Podfile and raise the Runner target to iOS 15 in Xcode.
Switching between CocoaPods and SPM
Toggle with flutter config --enable-swift-package-manager /
--no-enable-swift-package-manager, then flutter clean before rebuilding.
Only one path is active per build β there is no duplicate-symbol risk.
β FAQ #
Do I have to migrate to Swift Package Manager? No. CocoaPods is fully supported and remains the default. SPM is additive.
Will enabling SPM change plugin behavior? No. Both paths resolve the same Vonage SDK generation and register the same plugin class β the choice only affects dependency resolution.
Known limitation: iOS 13 and 14 are no longer supported (Vonage 2.x SDK requirement).
π€ Contributing #
This is an open-source, community-driven project. We welcome contributions, bug reports, and feature requests! Feel free to fork the repository, open an issue, or submit a pull request.
π Issues & Feedback #
If you encounter any issues or build errors, please open an issue on GitHub.
Disclaimer: This package is a community-driven project and is not an official SDK provided or maintained by Vonage.
Built with β€οΈ by Ashiqu Ali