diff --git a/backend/package.json b/backend/package.json index a5eb0c5..2ad5582 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.5", + "version": "0.12.6", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index b9ec2d3..868dd65 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.5}" +VERSION="${1:-0.12.6}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index ec8203e..0dfaca4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.5", + "version": "0.12.6", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 74e0e2e..66efcf8 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,35 +1,25 @@ // ── Firebase Messaging (background push for Android PWA) ────────────────────── -// Dynamically fetch Firebase config from backend to ensure consistency +// Config must be hardcoded here — the SW is woken by push events before any +// async fetch can resolve, so Firebase must be initialised synchronously. importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js'); importScripts('https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js'); -let FIREBASE_CONFIG = null; -let VAPID_KEY = null; +const FIREBASE_CONFIG = { + apiKey: "AIzaSyDx191unzXFT4WA1OvkdbrIY_c57kgruAU", + authDomain: "rosterchirp-push.firebaseapp.com", + projectId: "rosterchirp-push", + storageBucket: "rosterchirp-push.firebasestorage.app", + messagingSenderId: "126479377334", + appId: "1:126479377334:web:280abdd135cf7e0c50d717" +}; -// Fetch Firebase config and initialise messaging +// Initialise Firebase synchronously so the push listener is ready immediately let messaging = null; -let firebaseConfigPromise = fetch('/api/push/firebase-config') - .then(res => res.json()) - .then(config => { - FIREBASE_CONFIG = { - apiKey: config.apiKey, - authDomain: config.authDomain || `${config.projectId}.firebaseapp.com`, - projectId: config.projectId, - storageBucket: config.storageBucket || `${config.projectId}.firebasestorage.app`, - messagingSenderId: config.messagingSenderId, - appId: config.appId - }; - VAPID_KEY = config.vapidKey; - - if (FIREBASE_CONFIG.apiKey) { - firebase.initializeApp(FIREBASE_CONFIG); - messaging = firebase.messaging(); - console.log('[SW] Firebase initialized with dynamic config'); - } - }) - .catch(err => { - console.warn('[SW] Failed to fetch Firebase config:', err); - }); +if (FIREBASE_CONFIG.apiKey !== '__FIREBASE_API_KEY__') { + firebase.initializeApp(FIREBASE_CONFIG); + messaging = firebase.messaging(); + console.log('[SW] Firebase initialised'); +} // ── Cache ───────────────────────────────────────────────────────────────────── const CACHE_NAME = 'rosterchirp-v1'; @@ -81,49 +71,48 @@ function showRosterChirpNotification(data) { } // ── FCM background messages ─────────────────────────────────────────────────── -// Wait for Firebase config to be loaded before setting up message handler -firebaseConfigPromise.then(() => { - 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'); - } -}); +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 for Android) ───────────────────────────────────── -// Android Chrome sometimes doesn't properly trigger Firebase's onBackgroundMessage -// This fallback ensures notifications are displayed even if Firebase SDK fails +// Only runs when Firebase messaging is NOT yet initialised (e.g. push arrives +// before firebaseConfigPromise resolves). If Firebase IS ready it already has +// its own push listener registered and will call onBackgroundMessage — running +// this handler too would show the notification twice. self.addEventListener('push', (event) => { - console.log('[SW] push event received, hasData:', !!event.data, 'text:', event.data?.text?.()?.slice(0, 120)); - - // Try to handle the push event directly as a fallback - if (event.data) { + console.log('[SW] push event received, hasData:', !!event.data, 'messaging ready:', !!messaging); + + // Firebase compat SDK handles it via onBackgroundMessage — skip to avoid double notification. + if (messaging) return; + + if (!event.data) return; + + event.waitUntil((async () => { try { const data = event.data.json(); - console.log('[SW] Push data parsed:', JSON.stringify(data)); - - // If this is a Firebase message with data payload, show notification + console.log('[SW] Push fallback — data parsed:', JSON.stringify(data)); if (data.data || (data.title && data.body)) { - const notificationData = data.data || data; - return showRosterChirpNotification(notificationData); + await showRosterChirpNotification(data.data || data); } } catch (e) { - console.warn('[SW] Failed to parse push data:', e); - // Try to show a basic notification with the raw text + console.warn('[SW] Push fallback — failed to parse push data:', e); const text = event.data.text(); if (text) { - return self.registration.showNotification('RosterChirp', { - body: text.slice(0, 100), - icon: '/icons/icon-192.png', + await self.registration.showNotification('RosterChirp', { + body: text.slice(0, 100), + icon: '/icons/icon-192.png', badge: '/icons/icon-192-maskable.png', - tag: 'rosterchirp-fallback', + tag: 'rosterchirp-fallback', }); } } - } + })()); }); // ── Notification click ────────────────────────────────────────────────────────