feat(healthchat): ring calls via gateway VoIP push

CallKit rejects the NSE hand-off without the filtering entitlement.
This commit is contained in:
Sedat Qaja
2026-10-12 11:42:53 +02:00
parent c93af478a7
commit 570c7534c3
5 changed files with 127 additions and 6 deletions
@@ -32,6 +32,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
private let callController = CXCallController()
private let callProvider: CXProviderProtocol
private let timeProvider: TimeProvider
private let appSettings: AppSettings
private weak var clientProxy: ClientProxyProtocol? {
didSet {
@@ -67,10 +68,11 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
private var declineListenerHandle: TaskHandle?
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil) {
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil, appSettings: AppSettings) {
pushRegistry = PKPushRegistry(queue: nil)
self.timeProvider = timeProvider ?? TimeProvider(clock: ContinuousClock(), now: Date.init)
self.appSettings = appSettings
if let callProvider {
self.callProvider = callProvider
@@ -157,7 +159,16 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
// MARK: - PKPushRegistryDelegate
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { }
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
// HealthChat: stored for the NSE, which requests a VoIP push through the push gateway
// for ringing calls as long as the notification filtering entitlement is missing.
appSettings.pushKitDeviceToken = pushCredentials.token.base64EncodedString()
appSettings.voipPushGatewayNotifyURL = appSettings.pushGatewayNotifyEndpoint.absoluteString
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
appSettings.pushKitDeviceToken = nil
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
guard let roomID = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomID.rawValue] as? String else {
@@ -189,7 +200,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID, isVoiceCall: isVoiceCall)
incomingCallID = callID
guard let expirationDate = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationDate.rawValue] as? Date) else {
guard let expirationDate = Self.expirationDate(from: payload.dictionaryPayload) else {
MXLog.error("Something went wrong, missing expiration timestamp for incoming voip call: \(payload)")
assertionFailure("Incoming voip push is missing the expiration timestamp")
clearIncomingCallState()
@@ -353,6 +364,19 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
/// a call and immediately end it. `callerInfo` names the call after the room so the brief
/// system UI and the Recents entry show the room rather than "Unknown", and the entry can be
/// tapped to call back.
/// CallKit hands the NSE's payload over as-is, so the expiration arrives as a `Date`. A VoIP push
/// requested from the push gateway travels as JSON, where the same key is seconds since 1970.
private static func expirationDate(from payload: [AnyHashable: Any]) -> Date? {
switch payload[ElementCallServiceNotificationKey.expirationDate.rawValue] {
case let date as Date:
date
case let secondsSince1970 as TimeInterval:
Date(timeIntervalSince1970: secondsSince1970)
default:
nil
}
}
private func reportImmediatelyEndedCall(reason: CXCallEndedReason,
callerInfo: (roomID: String, roomDisplayName: String?)? = nil,
completion: @escaping () -> Void) {