fix(calls): cancel the CallKit ring when the caller ends before answer

Bug: a killed/backgrounded callee is woken only for the CallKit ring and has no
WebSocket yet, so the existing dm-call active:false (WS-only) never reaches it and
it keeps ringing after the caller hangs up.

Fix: send a 'cancel' VoIP push on call teardown.
- push.js: sendCallCancel() sends a {type:'cancel',callUUID} VoIP push to the user's
  ios-voip tokens; invites now carry type:'invite'.
- calls.js: endDmCallByRoom + endGroupCallByRoom fire sendCallCancel to the rung users.
- NativeCallPlugin: on a cancel push, end the reported call (reportCall endedAt); if the
  invite was never seen, report-then-end to satisfy iOS's 'report a call per VoIP push'.

Server part deploys now; the plugin part needs the next Codemagic build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 11:09:07 +05:30
parent cd7ab74eed
commit 3e21aa30d7
3 changed files with 37 additions and 1 deletions
@@ -118,6 +118,25 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
var dict: [String: Any] = [:]
for (k, v) in payload.dictionaryPayload { if let ks = k as? String { dict[ks] = v } }
let uuid = UUID(uuidString: (dict["callUUID"] as? String) ?? "") ?? UUID()
// CANCEL: the caller hung up / declined / it timed out before we answered stop ringing. iOS still
// requires a reported call for every VoIP push, so if we never saw the invite, report then end it.
if (dict["type"] as? String) == "cancel" {
if calls[uuid] != nil {
provider?.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded)
calls.removeValue(forKey: uuid)
completion()
} else {
let u = CXCallUpdate()
u.remoteHandle = CXHandle(type: .generic, value: "Call")
provider?.reportNewIncomingCall(with: uuid, update: u) { [weak self] _ in
self?.provider?.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded)
completion()
}
}
return
}
let callerName = (dict["callerName"] as? String) ?? (dict["groupName"] as? String) ?? "Incoming call"
let hasVideo = (dict["hasVideo"] as? Bool) ?? false
calls[uuid] = dict