diff --git a/backend/src/routes/push.js b/backend/src/routes/push.js index 61521ff..9139786 100644 --- a/backend/src/routes/push.js +++ b/backend/src/routes/push.js @@ -112,7 +112,10 @@ router.post('/unsubscribe', authMiddleware, async (req, res) => { } catch (e) { res.status(500).json({ error: e.message }); } }); -// Send a test push to the requesting user's own device β for diagnosing FCM setup +// 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. router.post('/test', authMiddleware, async (req, res) => { try { const subs = await query(req.schema, @@ -130,25 +133,42 @@ 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 results = []; for (const sub of subs) { try { - await messaging.send({ + const message = { token: sub.fcm_token, - data: { - title: 'RosterChirp Test', - body: 'Push notifications are working! π', - url: '/', - groupId: '', - }, android: { priority: 'high' }, webpush: { headers: { Urgency: 'high' } }, - }); - results.push({ device: sub.device, status: 'sent' }); - console.log(`[Push] Test notification sent to user ${req.user.id} device=${sub.device}`); + }; + + 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.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: '', + }; + } + + await messaging.send(message); + results.push({ device: sub.device, mode, status: 'sent' }); + console.log(`[Push] Test (${mode}) sent to user ${req.user.id} device=${sub.device}`); } catch (err) { - results.push({ device: sub.device, status: 'failed', error: err.message, code: err.code }); - console.error(`[Push] Test notification failed for user ${req.user.id} device=${sub.device}:`, err.message); + results.push({ device: sub.device, mode, status: 'failed', error: err.message, code: err.code }); + console.error(`[Push] Test (${mode}) failed for user ${req.user.id} device=${sub.device}:`, err.message); } } res.json({ results }); diff --git a/frontend/public/sw.js b/frontend/public/sw.js index e0ba63a..1300512 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -54,6 +54,7 @@ self.addEventListener('fetch', (event) => { let badgeCount = 0; function showRosterChirpNotification(data) { + console.log('[SW] showRosterChirpNotification:', JSON.stringify(data)); badgeCount++; if (self.navigator?.setAppBadge) self.navigator.setAppBadge(badgeCount).catch(() => {}); @@ -64,17 +65,29 @@ function showRosterChirpNotification(data) { data: { url: data.url || '/' }, tag: data.groupId ? `rosterchirp-group-${data.groupId}` : 'rosterchirp-message', renotify: true, - vibrate: [200, 100, 200], // haptic pattern β also activates the OS sound channel + vibrate: [200, 100, 200], }); } // ββ FCM background messages βββββββββββββββββββββββββββββββββββββββββββββββββββ if (messaging) { messaging.onBackgroundMessage((payload) => { + console.log('[SW] onBackgroundMessage received, data:', JSON.stringify(payload.data)); return showRosterChirpNotification(payload.data || {}); }); +} else { + console.warn('[SW] Firebase messaging not initialised β push notifications disabled'); } +// ββ Raw push event (fallback diagnostic) βββββββββββββββββββββββββββββββββββββ +// Fires for every push event BEFORE the Firebase SDK handles it. +// Log it so chrome://inspect shows whether the SW is even waking up. +self.addEventListener('push', (event) => { + console.log('[SW] push event received, hasData:', !!event.data, 'text:', event.data?.text?.()?.slice(0, 120)); + // Note: Firebase compat SDK registers its own push listener and handles display. + // This listener is diagnostic only β do not call showNotification() here. +}); + // ββ Notification click ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ self.addEventListener('notificationclick', (event) => { event.notification.close(); diff --git a/frontend/src/components/ProfileModal.jsx b/frontend/src/components/ProfileModal.jsx index a3618f4..cf76e7e 100644 --- a/frontend/src/components/ProfileModal.jsx +++ b/frontend/src/components/ProfileModal.jsx @@ -180,24 +180,50 @@ export default function ProfileModal({ onClose }) { β’ App is backgrounded when the test fires
- +