feat(calls): server foundation for native CallKit/VoIP calling (phase A)

First slice of the native call feature. Backward-compatible: with no VoIP tokens
registered yet it behaves exactly like today's banner push.

- push.js: sendApnsVoip() sends a PushKit VoIP push (apns-push-type 'voip', topic
  <bundle>.voip, reusing the same .p8) to wake a killed app for CallKit; and
  sendCallNotification() which PREFERS a VoIP push when the user has an 'ios-voip'
  token, else falls back to the normal alert/banner push (Android/web/pre-CallKit iOS).
- routes.js: /api/devices now accepts platform 'ios-voip' (the PushKit token, stored
  alongside the normal alert token in device_tokens).
- calls.js: each call now carries a stable crypto.randomUUID() (CallKit needs a UUID
  to report + later cancel the call); DM and group call notifications route through
  PUSH.sendCallNotification instead of the raw banner push.

Next: the native-call Capacitor plugin (PushKit + CallKit + LiveKit iOS SDK).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 20:57:56 +05:30
parent 37e58f6087
commit fd2eb42e25
3 changed files with 68 additions and 11 deletions
+2 -1
View File
@@ -412,7 +412,8 @@ route('POST', '/api/devices', async (req, res) => {
if (!u) return json(res, 401, { error: 'unauthorized' });
const { platform, token } = await readBody(req);
if (!token || typeof token !== 'string') return json(res, 400, { error: 'token required' });
if (platform !== 'ios' && platform !== 'android') return json(res, 400, { error: 'platform must be ios or android' });
// 'ios-voip' = a PushKit VoIP token (CallKit wake), stored alongside the normal alert token.
if (platform !== 'ios' && platform !== 'android' && platform !== 'ios-voip') return json(res, 400, { error: 'platform must be ios, android or ios-voip' });
try { await R.deviceTokens.register({ id: A.id(), userId: u.id, tenantId: u.team_id, platform, token }); } catch (_) {}
json(res, 200, { ok: true });
});