From da5b44c0a54fe032f5ef85cbcd61190f8edebb22 Mon Sep 17 00:00:00 2001 From: sravan Date: Mon, 19 Oct 2026 20:40:46 +0530 Subject: [PATCH] Call push: ring ALL of a user's devices, not just the first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/push.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/push.js b/server/push.js index 49774e9..532ede9 100644 --- a/server/push.js +++ b/server/push.js @@ -157,8 +157,13 @@ async function sendCallNotification(userId, data) { // data: { callUUID, room, kind, callerId, callerName, groupId, groupName, title, body, hasVideo } let 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'); - 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 = { type: 'invite', callUUID: data.callUUID, room: data.room, kind: data.kind, @@ -171,12 +176,10 @@ async function sendCallNotification(userId, data) { 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 (_) {} } - return 'voip'; + handledNative = true; } - // Android native ring: send a high-priority DATA message to the user's 'android' tokens so the native-call - // plugin rings even when the app is closed (BzCallMessagingService → full-screen incoming UI). Gated on - // 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. + // Android native ring: a high-priority DATA message to the user's 'android' tokens so the native-call plugin + // rings even when the app is closed (BzCallMessagingService → full-screen incoming UI). Gated on ANDROID_CALLS. const android = toks.filter((t) => t.platform === 'android'); if (android.length && fcmSA && process.env.ANDROID_CALLS === '1') { const cd = { @@ -188,8 +191,11 @@ async function sendCallNotification(userId, data) { 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 (_) {} } - 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, { 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 },