Call push: ring ALL of a user's devices, not just the first

sendCallNotification returned after the iOS VoIP branch, so a user signed in
on both an iPhone (ios-voip token) and an Android phone only got the call on
iOS — the Android invite was never sent, so a swiped-away Android phone never
rang (confirmed on-device: only type=cancel reached Android, never
type=invite, since sendCallCancel does fan out to all platforms). Now send to
iOS VoIP AND Android FCM, and only fall back to the banner when no native
device was waked. Server-only; no app rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
SravanandClaude Opus 4.8 committed 2026-10-19 20:40:46 +05:30
1 parent cab0c79f57
commit da5b44c0a5
1 file changed
+13 -7
+13 -7
View File
@@ -157,8 +157,13 @@ async function sendCallNotification(userId, data) {
// data: { callUUID, room, kind, callerId, callerName, groupId, groupName, title, body, hasVideo } // data: { callUUID, room, kind, callerId, callerName, groupId, groupName, title, body, hasVideo }
let toks = []; let toks = [];
try { toks = await R.deviceTokens.byUser(userId); } catch (_) { toks = []; } try { toks = await R.deviceTokens.byUser(userId); } catch (_) { toks = []; }
// Ring EVERY native device the user has — not just the first. (A user signed in on both an iPhone and an
// Android phone must ring on both; the old early-return after the iOS branch meant the Android invite was
// never sent, so a swiped-away Android phone never rang while the iPhone did.)
let handledNative = false;
// iOS CallKit via VoIP push.
const voip = toks.filter((t) => t.platform === 'ios-voip'); const voip = toks.filter((t) => t.platform === 'ios-voip');
if (voip.length && apnsCfg && process.env.CALLKIT_ENABLED === '1') { // kill-switch: off → fall through to a banner if (voip.length && apnsCfg && process.env.CALLKIT_ENABLED === '1') {
const payload = { const payload = {
type: 'invite', type: 'invite',
callUUID: data.callUUID, room: data.room, kind: data.kind, callUUID: data.callUUID, room: data.room, kind: data.kind,
@@ -171,12 +176,10 @@ async function sendCallNotification(userId, data) {
for (const t of voip) { for (const t of voip) {
try { const r = await sendApnsVoip(t.token, payload); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {} try { const r = await sendApnsVoip(t.token, payload); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {}
} }
return 'voip'; handledNative = true;
} }
// Android native ring: send a high-priority DATA message to the user's 'android' tokens so the native-call // Android native ring: a high-priority DATA message to the user's 'android' tokens so the native-call plugin
// plugin rings even when the app is closed (BzCallMessagingService → full-screen incoming UI). Gated on // rings even when the app is closed (BzCallMessagingService → full-screen incoming UI). Gated on ANDROID_CALLS.
// ANDROID_CALLS so the app can ship with the plugin dormant and be switched on server-side, like CALLKIT_ENABLED
// for iOS. Returns early (no banner) so the same token isn't ALSO buzzed by the fallback push below.
const android = toks.filter((t) => t.platform === 'android'); const android = toks.filter((t) => t.platform === 'android');
if (android.length && fcmSA && process.env.ANDROID_CALLS === '1') { if (android.length && fcmSA && process.env.ANDROID_CALLS === '1') {
const cd = { const cd = {
@@ -188,8 +191,11 @@ async function sendCallNotification(userId, data) {
for (const t of android) { for (const t of android) {
try { const r = await sendFcmData(t.token, cd); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {} try { const r = await sendFcmData(t.token, cd); if (r && r.dead) { try { await R.deviceTokens.removeByToken(t.token); } catch (_) {} } } catch (_) {}
} }
return 'fcm-call'; handledNative = true;
} }
// Native devices got the call-wake above; the banner is only a fallback for web push + non-native devices
// (sending it too would double-notify the native devices).
if (handledNative) return 'native';
await sendToUser(userId, { await sendToUser(userId, {
title: data.title, body: data.body, kind: data.kind, id: data.callerId || data.groupId, title: data.title, body: data.body, kind: data.kind, id: data.callerId || data.groupId,
room: data.room, tag: 'call:' + data.room, data: { kind: data.kind, id: data.callerId || data.groupId, room: data.room, call: 1 }, room: data.room, tag: 'call:' + data.room, data: { kind: data.kind, id: data.callerId || data.groupId, room: data.room, call: 1 },