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

- +
+ + +
+
+ Test (via SW) β€” normal production path, service worker shows notification.
+ Test (via Browser) β€” bypasses service worker; Chrome displays directly. +
{pushResult && (
req('GET', '/push/firebase-config'), subscribePush: (fcmToken) => req('POST', '/push/subscribe', { fcmToken }), unsubscribePush: () => req('POST', '/push/unsubscribe'), - testPush: () => req('POST', '/push/test'), + testPush: (mode = 'data') => req('POST', `/push/test?mode=${mode}`), // Link preview getLinkPreview: (url) => req('GET', `/link-preview?url=${encodeURIComponent(url)}`),