v0.12.8 FCM bug fix

This commit is contained in:
2026-03-23 19:34:13 -04:00
parent eca93aae28
commit 01f37e60be
25 changed files with 2769 additions and 29 deletions

View File

@@ -40,18 +40,35 @@ async function sendPushToUser(schema, userId, payload) {
try {
await messaging.send({
token: sub.fcm_token,
// Top-level notification ensures FCM/Chrome can display even if the SW
// onBackgroundMessage handler has trouble — mirrors the working fcm-app pattern.
notification: {
title: payload.title || 'New Message',
body: payload.body || '',
},
// Extra fields for SW click-routing (url, groupId)
data: {
title: payload.title || 'New Message',
body: payload.body || '',
url: payload.url || '/',
groupId: payload.groupId ? String(payload.groupId) : '',
},
android: { priority: 'high' },
android: {
priority: 'high',
notification: { sound: 'default' },
},
apns: {
headers: { 'apns-priority': '10' },
payload: { aps: { contentAvailable: true } },
payload: { aps: { sound: 'default', badge: 1, contentAvailable: true } },
},
webpush: {
headers: { Urgency: 'high' },
notification: {
icon: '/icons/icon-192.png',
badge: '/icons/icon-192-maskable.png',
tag: payload.groupId ? `rosterchirp-group-${payload.groupId}` : 'rosterchirp-message',
renotify: true,
},
fcm_options: { link: payload.url || '/' },
},
webpush: { headers: { Urgency: 'high' } },
});
} catch (err) {
// Remove stale tokens
@@ -117,9 +134,9 @@ router.post('/unsubscribe', authMiddleware, async (req, res) => {
});
// Send a test push to the requesting user's own device — for diagnosing FCM setup.
// mode=data (default): data-only message handled by the service worker onBackgroundMessage.
// mode=browser: webpush.notification message handled by Chrome directly (bypasses SW).
// Use mode=browser to check if FCM delivery itself works when the SW is not involved.
// mode=notification (default): notification+data message — same path as real messages.
// mode=browser: webpush.notification only — Chrome shows it directly, SW not involved.
// Use mode=browser to verify FCM delivery works independently of the service worker.
router.post('/test', authMiddleware, async (req, res) => {
try {
const subs = await query(req.schema,
@@ -137,38 +154,46 @@ router.post('/test', authMiddleware, async (req, res) => {
return res.status(503).json({ error: 'Firebase Admin not initialised on server — check FIREBASE_SERVICE_ACCOUNT in .env' });
}
const mode = req.query.mode === 'browser' ? 'browser' : 'data';
const mode = req.query.mode === 'browser' ? 'browser' : 'notification';
const results = [];
for (const sub of subs) {
try {
const message = {
token: sub.fcm_token,
android: { priority: 'high' },
android: {
priority: 'high',
notification: { sound: 'default' },
},
apns: {
headers: { 'apns-priority': '10' },
payload: { aps: { contentAvailable: true } },
payload: { aps: { sound: 'default', badge: 1, contentAvailable: true } },
},
webpush: {
headers: { Urgency: 'high' },
notification: {
icon: '/icons/icon-192.png',
badge: '/icons/icon-192-maskable.png',
tag: 'rosterchirp-test',
},
},
webpush: { headers: { Urgency: 'high' } },
};
if (mode === 'browser') {
// Chrome displays the notification directly — onBackgroundMessage does NOT fire.
// Use this to verify FCM delivery works independently of the service worker.
message.webpush.notification = {
title: 'RosterChirp Test (browser)',
body: 'FCM delivery confirmed — Chrome handled this directly.',
icon: '/icons/icon-192.png',
};
message.webpush.notification.title = 'RosterChirp Test (browser)';
message.webpush.notification.body = 'FCM delivery confirmed — Chrome handled this directly.';
message.webpush.fcm_options = { link: '/' };
} else {
// data-only — service worker onBackgroundMessage must show the notification.
message.data = {
title: 'RosterChirp Test',
body: 'Push notifications are working!',
url: '/',
groupId: '',
// notification+data — same structure as real messages.
// SW onBackgroundMessage fires and shows the notification.
message.notification = {
title: 'RosterChirp Test',
body: 'Push notifications are working!',
};
message.data = { url: '/', groupId: '' };
message.webpush.fcm_options = { link: '/' };
}
await messaging.send(message);
@@ -183,4 +208,22 @@ router.post('/test', authMiddleware, async (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Debug endpoint (admin-only) — lists all FCM subscriptions for this schema
router.get('/debug', authMiddleware, async (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' });
try {
const subs = await query(req.schema, `
SELECT ps.id, ps.user_id, ps.device, ps.fcm_token,
u.name, u.email
FROM push_subscriptions ps
JOIN users u ON u.id = ps.user_id
WHERE ps.fcm_token IS NOT NULL
ORDER BY u.name, ps.device
`);
const fcmConfigured = !!(process.env.FIREBASE_API_KEY && process.env.FIREBASE_SERVICE_ACCOUNT);
const firebaseAdminReady = !!getMessaging();
res.json({ subscriptions: subs, fcmConfigured, firebaseAdminReady });
} catch (e) { res.status(500).json({ error: e.message }); }
});
module.exports = { router, sendPushToUser };