feat(calls): CallKit kill-switch + fix ~1s re-ring on cancel

MIC BROKEN with CallKit: a CallKit call reserves the microphone, so the WebView's
WebRTC can't capture it — calls are unusable until native LiveKit media lands. Add a
server kill-switch (CALLKIT_ENABLED, default OFF) so CallKit can be flipped without an
app rebuild: /api/meetings/config now returns callkit; setupNativeCall bails when off
(-> WebView calls, mic works); push.js only sends VoIP/CallKit pushes when enabled.
Deploying with the flag unset immediately restores working WebView calls.

RE-RING: a late cancel push for an already-declined call hit the plugin's 'unknown
uuid' path and re-reported a fresh incoming call (~1s re-ring). Track endedCalls and
make a late cancel for an already-ended call a no-op.

Server part deploys now (no rebuild); plugin re-ring fix ships with the native build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 17:54:21 +05:30
parent 3e21aa30d7
commit e7c3231c50
5 changed files with 26 additions and 4 deletions
+2 -2
View File
@@ -138,7 +138,7 @@ async function sendCallNotification(userId, data) {
let toks = [];
try { toks = await R.deviceTokens.byUser(userId); } catch (_) { toks = []; }
const voip = toks.filter((t) => t.platform === 'ios-voip');
if (voip.length && apnsCfg) {
if (voip.length && apnsCfg && process.env.CALLKIT_ENABLED === '1') { // kill-switch: off → fall through to a banner
const payload = {
type: 'invite',
callUUID: data.callUUID, room: data.room, kind: data.kind,
@@ -161,7 +161,7 @@ async function sendCallNotification(userId, data) {
// VoIP push so it reaches a killed app that has no WebSocket — the CallKit plugin ends the reported call.
// No-op for non-VoIP devices (their ring is a normal notification that just goes away).
async function sendCallCancel(userId, callUUID) {
if (!apnsCfg || !callUUID) return;
if (!apnsCfg || !callUUID || process.env.CALLKIT_ENABLED !== '1') return;
let toks = [];
try { toks = await R.deviceTokens.byUser(userId); } catch (_) { toks = []; }
for (const t of toks.filter((t) => t.platform === 'ios-voip')) {