feat(ios): add VoIP push support for PushKit / CallKit#211
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #211 +/- ##
==========================================
- Coverage 90.17% 88.83% -1.34%
==========================================
Files 9 9
Lines 529 591 +62
==========================================
+ Hits 477 525 +48
- Misses 52 66 +14
Continue to review full report in Codecov by Harness.
|
There was a problem hiding this comment.
Pull request overview
Adds first-class support for iOS VoIP pushes (PushKit/CallKit) by introducing a dedicated VoIP APNs push path and routing data_message: ios_voip devices through it, with accompanying tests and config documentation updates.
Changes:
- Add
DataMessageType::IosVoipand route VoIP devices through direct APNs regardless ofnotify_via. - Implement
push_notification_voip_apnsto send APNs requests withapns-push-type: voipand CallKit-related custom payload fields. - Add test coverage for VoIP payload generation and fallback behavior when
sender_display_name/event_idare missing.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/models.rs |
Introduces IosVoip device message type detection via data_message: ios_voip. |
src/api.rs |
Routes VoIP pushes to direct APNs and calls the new VoIP pusher function. |
src/pusher.rs |
Adds VoIP-specific APNs push function and blocks VoIP from going through FCM. |
src/settings.rs |
Allows parsing apns_push_type: voip from config. |
src/error.rs |
Adds From<a2::Error> mapping to HedwigError for APNs payload/build errors. |
tests/pusher.rs |
Adds tests validating VoIP payload fields and fallback behavior. |
config.sample.yaml |
Documents VoIP routing behavior and required APNs configuration. |
Cargo.toml / Cargo.lock |
Adds uuid dependency for generated call IDs when event_id is absent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Require app_id to end in '.voip' so the APNs topic always matches the PushKit registration - Add ErrCode::VoipNotSupported instead of BadJson when a VoIP push is routed to FCM - Log a2 errors in From<a2::Error> like the other error impls - Fall back to 'Unknown' caller name instead of an empty string - Document the intentionally empty handle and hardcoded isVideo - Remove the server-side notify_via override; the app must register VoIP pushers with notify_via: apns - Disallow 'voip' as a general apns_push_type config value
The flutter_callkit_incoming plugin parses the push payload id with UUID(uuidString:), so passing the raw Matrix event_id ($abc...) crashed the app on every VoIP push. Derive a stable UUIDv5 from the event_id instead, preserving CallKit deduplication.
Homeservers forward custom pusher keys inside device.data, never as top-level device fields, so a client-registered pusher could not actually select APNs delivery. Honour notify_via from device.data (keeping top-level for compatibility) and always route ios_voip devices via APNs, since PushKit tokens are not valid FCM tokens.
| } | ||
| DataMessageType::IosVoip => { | ||
| // VoIP pushes are routed through push_notification_voip_apns, not FCM. | ||
| return Err(HedwigError { |
There was a problem hiding this comment.
This will lead to retrying the request push_max_retries times. You could do something like:
impl ErrCode {
/// Whether retrying a push that failed with this code could ever succeed.
#[must_use]
pub fn is_permanent(&self) -> bool {
matches!(self, ErrCode::BadJson | ErrCode::VoipNotSupported | ErrCode::APNSNotConfigured)
}
}
And use it to avoid retrying such requests that will just fail every time
| sender: &Arc<dyn APNSSender + Send + Sync>, | ||
| settings: &Settings, | ||
| ) -> Result<(), HedwigError> { | ||
| if !device.app_id.starts_with(&settings.hedwig.app_id) { |
There was a problem hiding this comment.
this check is already done in 2 other places, I think it's worth extracting it into a small helper and use it everywhere now
Closes FM-638
I've tested this with the app and it works, the call shows up natively in iOS.