callbundle_ios
The iOS implementation of callbundle.
Table of Contents
- Usage
- Architecture
- VoIP Certificate Setup (PEM File)
- PushKit Integration
- CallKit Integration
- Audio Session Management
- Cold-Start Persistence
- Caller Avatar
- Thread Safety
- Permissions
- Requirements
Usage
This package is endorsed — simply add callbundle to your pubspec.yaml and this package is included automatically on iOS.
dependencies:
callbundle: ^1.0.0
Add the VoIP background mode to your Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
</array>
Architecture
| Component | File | Responsibility |
|---|---|---|
CallBundlePlugin |
CallbundleIosPlugin.swift |
MethodChannel handler, singleton, event dispatch |
CallKitController |
CallKitController.swift |
CXProvider + CXCallController for native call UI |
PushKitHandler |
PushKitHandler.swift |
PKPushRegistry delegate, VoIP token management |
AudioSessionManager |
AudioSessionManager.swift |
AVAudioSession with .mixWithOthers |
CallStore |
CallStore.swift |
Thread-safe call tracking + cold-start persistence |
MissedCallNotificationManager |
MissedCallNotificationManager.swift |
UNUserNotificationCenter for missed calls (with avatar attachment) |
VoIP Certificate Setup (PEM File)
iOS requires a VoIP push certificate to send VoIP pushes via Apple Push Notification service (APNs). This section covers creating the VoIP certificate and exporting it as a PEM file for your server.
Step 1: Create VoIP Services Certificate
- Go to Apple Developer — Certificates
- Click + to create a new certificate
- Under Services, select VoIP Services Certificate
- Click Continue
- Select your App ID (must match your app's bundle identifier)
- Click Continue
- Upload a Certificate Signing Request (CSR):
- Open Keychain Access on your Mac
- Menu: Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority
- Enter your email, select Saved to disk, click Continue
- Save the
.certSigningRequestfile
- Upload the CSR and click Continue
- Download the generated
.cerfile
Step 2: Export as PEM File
- Double-click the downloaded
.cerfile to install it in Keychain Access - In Keychain Access, find the certificate under My Certificates:
- It will be named VoIP Services: com.yourcompany.yourapp
- Right-click the certificate → Export → choose
.p12format - Set an export password (you'll need it in the next step)
- Convert
.p12to.pemusing Terminal:
# Extract the certificate
openssl pkcs12 -in voip_cert.p12 -out voip_cert.pem -nodes -clcerts
# Or split into certificate and key files (some servers require this)
openssl pkcs12 -in voip_cert.p12 -out voip_cert_only.pem -nodes -nokeys
openssl pkcs12 -in voip_cert.p12 -out voip_key.pem -nodes -nocerts
Step 3: Configure Your Server
Your push server needs the PEM file to send VoIP pushes to APNs. Here's an example using curl:
# Send a VoIP push to APNs (Development)
curl -v \
--cert voip_cert.pem \
--header "apns-topic: com.yourcompany.yourapp.voip" \
--header "apns-push-type: voip" \
--header "apns-priority: 10" \
--header "apns-expiration: 0" \
--data '{"callId":"abc-123","callerName":"John Doe","handle":"+1234567890","callerAvatar":"https://example.com/photo.jpg"}' \
https://api.sandbox.push.apple.com/3/device/<VOIP_DEVICE_TOKEN>
# Production
# Replace api.sandbox.push.apple.com with api.push.apple.com
APNs Payload Format
The plugin expects these fields in the VoIP push payload:
{
"callId": "unique-call-id",
"callerName": "John Doe",
"handle": "+1234567890",
"hasVideo": false,
"callerAvatar": "https://example.com/photos/john.jpg"
}
| Field | Type | Required | Description |
|---|---|---|---|
callId |
String |
Yes | Unique call identifier |
callerName |
String |
Yes | Displayed on the CallKit screen |
handle |
String |
No | Phone number or SIP address |
hasVideo |
Bool |
No | Show video call UI (default: false) |
callerAvatar |
String |
No | URL for caller profile photo (used in missed call notifications) |
Alternative field names are also supported: caller_name, phone, has_video, caller_avatar.
Step 4: Token-Based Authentication (Alternative)
Instead of PEM certificates, you can use APNs token-based authentication (.p8 key). This is recommended for new projects:
- Go to Apple Developer — Keys
- Click + → Enable Apple Push Notifications service (APNs) → Download the
.p8file - Note the Key ID and your Team ID
- Use these with your server's APNs library (no PEM needed)
# Example with curl + JWT token (p8-based auth)
curl -v \
--header "authorization: bearer <JWT_TOKEN>" \
--header "apns-topic: com.yourcompany.yourapp.voip" \
--header "apns-push-type: voip" \
--header "apns-priority: 10" \
--data '{"callId":"abc-123","callerName":"John Doe"}' \
https://api.push.apple.com/3/device/<VOIP_DEVICE_TOKEN>
Important Notes
- VoIP certificates are separate from regular APNs certificates
- The APNs topic for VoIP pushes must end with
.voip(e.g.,com.yourapp.voip) - VoIP certificates expire after 1 year — set a reminder to renew
- Development (sandbox) and Production use different APNs endpoints
- The VoIP token from
CallBundle.getVoipToken()is device-specific and changes on reinstall
PushKit Integration
PushKit VoIP push is handled inside the plugin. No AppDelegate code needed.
When a VoIP push arrives:
PushKitHandlerreceives the payload viaPKPushRegistryDelegatereportNewIncomingCallis called synchronously (required by iOS — app is terminated if not)- CallKit shows the native incoming call screen
- User interaction → event delivered to Dart via MethodChannel
Getting the VoIP Token
final token = await CallBundle.getVoipToken();
if (token != null) {
// Send this hex string token to your push server
await registerTokenWithServer(token);
}
The token is a hex-encoded string of the device's PushKit credentials. It's updated automatically — listen for onVoipTokenUpdated events for real-time updates.
How PushKit Differs from FCM
| Feature | PushKit (VoIP) | FCM |
|---|---|---|
| Wake on delivery | Always (even killed) | Not guaranteed |
| Background processing | Guaranteed | Limited |
| CallKit requirement | Must report call synchronously | Not required |
| Token type | Separate VoIP token | FCM token |
| Best for | iOS incoming calls | Android + cross-platform |
Recommendation: Use PushKit for iOS incoming calls and FCM for Android.
CallKit Integration
Full CXProvider delegate implementation:
- Incoming calls:
reportNewIncomingCallwith caller name, handle, and video support - Outgoing calls:
CXStartCallActionfor the green status bar indicator - Call connected/ended: State management via
CXCallController isUserInitiated: Every event correctly flags whether the user or the system/app initiated it
Programmatic vs User-Initiated Ends
The plugin tracks programmaticEndUUIDs — when your code calls CallBundle.endCall(), the resulting CXEndCallAction is correctly flagged as isUserInitiated: false. This eliminates the _isEndingCallKitProgrammatically flag pattern.
Audio Session Management
AudioSessionManager configures AVAudioSession with .mixWithOthers:
- Category:
.playAndRecordwith.defaultToSpeakerand.mixWithOthers - Activation: On call connect, deactivation on call end
- No conflicts: Prevents HMS/100ms audio session from being killed by CallKit
Cold-Start Persistence
CallStore handles the case where the user answers a call before the Dart engine is ready:
- VoIP push arrives → CallKit shows incoming call
- User taps Accept →
CXAnswerCallActionfires - If Dart not ready →
CallStore.savePendingAccept()(backed byUserDefaults.synchronize()) - Dart calls
CallBundle.configure()→deliverPendingEvents()delivers stored event - No hardcoded delays — events delivered as soon as Dart is ready
Call metadata (extra, callerName, handle, callerAvatar) is preserved through the store.
Caller Avatar
When callerAvatar is provided (via NativeCallParams or the VoIP push payload), the plugin uses it for missed call notifications:
MissedCallNotificationManagerdownloads the image from the URL asynchronously- The image is saved to a temp file and attached via
UNNotificationAttachment - iOS displays the avatar as the notification thumbnail
- If the download fails, the notification is shown without an avatar (graceful fallback)
Note: CallKit's incoming call UI uses the contact's photo from
Contacts.framework— thecallerAvatarfield does not affect the CallKit screen.
Thread Safety
All CallStore operations use a serial DispatchQueue:
private let queue = DispatchQueue(label: "com.callbundle.callstore", qos: .userInitiated)
This ensures thread-safe access from multiple callbacks (PushKit, CallKit, MethodChannel).
Permissions
checkPermissions: ReadsUNNotificationSettingswithout requesting — no system dialogrequestPermissions: CallsUNUserNotificationCenter.requestAuthorization()— triggers system dialog- Battery optimization: Returns
true(not applicable on iOS)
Requirements
| Requirement | Value |
|---|---|
| iOS | 13.0+ |
| Swift | 5.0+ |
| CocoaPods | 1.10+ |
iOS Frameworks Used
| Framework | Purpose |
|---|---|
CallKit |
Native incoming/outgoing call UI |
PushKit |
VoIP push notification delivery |
AVFoundation |
Audio session management |
UserNotifications |
Missed call notifications |
Links
Libraries
- callbundle_ios
- iOS implementation of the CallBundle plugin.