feat(push): Web Push notifications for backgrounded/closed/mobile tabs
Page-level Notifications can't fire when a tab is frozen/closed (and never on mobile), which is why recipients on another tab/app got nothing. Adds a notification-only service worker (sw.js, no caching) + Web Push: - push.js: optional web-push wrapper (no-op unless web-push installed AND VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY set -> app unaffected if unconfigured). - push_subscriptions table + R.pushSubs repo (upsert by endpoint, prune dead). - /api/push/vapid|subscribe|unsubscribe; DM + group message routes also send a Web Push to recipients. - Client registers /sw.js, subscribes when permission granted; hidden-tab popups are left to push to avoid double-notifying (pushActive flag); SW suppresses the OS popup when a tab is visible. Removes the old code that unregistered SWs. Requires (prod, once): npm install + VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECT env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
// Web Push (background / closed-tab / mobile notifications). Fully optional:
|
||||
// - if the `web-push` package isn't installed, or VAPID env keys aren't set,
|
||||
// isEnabled() is false and every call is a silent no-op (the app is unaffected).
|
||||
// Configure in production by setting:
|
||||
// VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT (e.g. mailto:admin@bizgaze.com)
|
||||
// Generate a key pair once with: npx web-push generate-vapid-keys
|
||||
const R = require('./repos');
|
||||
|
||||
let webpush = null;
|
||||
try { webpush = require('web-push'); } catch (_) { /* package not installed -> push disabled */ }
|
||||
|
||||
const PUBLIC = process.env.VAPID_PUBLIC_KEY || '';
|
||||
const PRIVATE = process.env.VAPID_PRIVATE_KEY || '';
|
||||
const SUBJECT = process.env.VAPID_SUBJECT || 'mailto:support@bizgaze.com';
|
||||
|
||||
let ready = false;
|
||||
if (webpush && PUBLIC && PRIVATE) {
|
||||
try { webpush.setVapidDetails(SUBJECT, PUBLIC, PRIVATE); ready = true; }
|
||||
catch (e) { console.warn('[push] invalid VAPID config:', e.message); }
|
||||
}
|
||||
if (!ready) console.log('[push] Web Push disabled (set web-push + VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY to enable).');
|
||||
|
||||
function isEnabled() { return ready; }
|
||||
function publicKey() { return ready ? PUBLIC : ''; }
|
||||
|
||||
// Fire-and-forget push to every device the user has subscribed. Dead subscriptions
|
||||
// (410 Gone / 404) are pruned. Never throws.
|
||||
async function sendToUser(userId, payload) {
|
||||
if (!ready) return;
|
||||
let subs = [];
|
||||
try { subs = R.pushSubs.byUser(userId); } catch (_) { return; }
|
||||
const data = JSON.stringify(payload || {});
|
||||
for (const s of subs) {
|
||||
const sub = { endpoint: s.endpoint, keys: { p256dh: s.p256dh, auth: s.auth } };
|
||||
try {
|
||||
await webpush.sendNotification(sub, data, { TTL: 600 });
|
||||
} catch (err) {
|
||||
const code = err && err.statusCode;
|
||||
if (code === 404 || code === 410) { try { R.pushSubs.removeByEndpoint(s.endpoint); } catch (_) {} }
|
||||
// other errors (network, 4xx) are ignored — push is best-effort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { isEnabled, publicKey, sendToUser };
|
||||
Reference in New Issue
Block a user