callbundle_ios 1.0.14
callbundle_ios: ^1.0.14 copied to clipboard
iOS implementation of the CallBundle plugin using CallKit. Handles PushKit, audio session, and missed call notifications inside the plugin.
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 |