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

334
fcm-app/public/app.js Normal file
View File

@@ -0,0 +1,334 @@
// Load Firebase SDK immediately
const script1 = document.createElement('script');
script1.src = 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js';
script1.onload = () => {
const script2 = document.createElement('script');
script2.src = 'https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js';
script2.onload = () => {
// Initialize Firebase immediately
initializeFirebase();
console.log('Firebase SDK and initialization complete');
// Now that Firebase is ready, set up the app
setupApp();
};
document.head.appendChild(script2);
};
document.head.appendChild(script1);
// Global variables
let currentUser = null;
let fcmToken = null;
let messaging = null;
let swRegistration = null;
let initPromise = null;
let foregroundHandlerSetup = false;
const VAPID_KEY = 'BE6hPKkbf-h0lUQ1tYo249pBOdZFFcWQn9suwg3NDwSE8C_hv8hk1dUY9zxHBQEChO_IAqyFZplF_SUb5c4Ofrw';
// Simple user authentication
const users = {
'pwau1': { password: 'test123', name: 'Desktop User' },
'pwau2': { password: 'test123', name: 'Mobile User 1' },
'pwau3': { password: 'test123', name: 'Mobile User 2' }
};
// Initialize Firebase — returns a promise that resolves when messaging is ready
function initializeFirebase() {
if (initPromise) return initPromise;
const firebaseConfig = {
apiKey: "AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA",
authDomain: "fcmtest-push.firebaseapp.com",
projectId: "fcmtest-push",
storageBucket: "fcmtest-push.firebasestorage.app",
messagingSenderId: "439263996034",
appId: "1:439263996034:web:9b3d52af2c402e65fdec9b"
};
if (firebase.apps.length === 0) {
firebase.initializeApp(firebaseConfig);
console.log('Firebase app initialized');
}
initPromise = navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered:', registration);
swRegistration = registration;
messaging = firebase.messaging();
console.log('Firebase messaging initialized successfully');
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
initPromise = null;
throw error;
});
return initPromise;
}
// Show user info panel and hide login form
function showUserInfo() {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('userInfo').style.display = 'block';
document.getElementById('currentUser').textContent = users[currentUser]?.name || currentUser;
}
// Setup app after Firebase is ready
function setupApp() {
// Set up event listeners
document.getElementById('loginForm').addEventListener('submit', login);
document.getElementById('sendNotificationBtn').addEventListener('click', sendNotification);
document.getElementById('logoutBtn').addEventListener('click', logout);
// Restore session and re-register FCM token if notifications were already granted
const savedUser = localStorage.getItem('currentUser');
if (savedUser) {
currentUser = savedUser;
showUserInfo();
if (Notification.permission === 'granted') {
initializeFirebase()
.then(() => messaging.getToken({ vapidKey: VAPID_KEY, serviceWorkerRegistration: swRegistration }))
.then(token => { if (token) return registerToken(currentUser, token); })
.catch(err => console.error('Token refresh on session restore failed:', err));
}
}
}
// Request notification permission and get FCM token
async function requestNotificationPermission() {
try {
console.log('Requesting notification permission...');
const permission = await Notification.requestPermission();
console.log('Permission result:', permission);
if (permission === 'granted') {
console.log('Notification permission granted.');
showStatus('Getting FCM token...', 'info');
try {
const token = await messaging.getToken({ vapidKey: VAPID_KEY, serviceWorkerRegistration: swRegistration });
console.log('FCM Token generated:', token);
if (!token) {
throw new Error('getToken() returned empty — check VAPID key and service worker');
}
fcmToken = token;
// Send token to server
await registerToken(currentUser, token);
showStatus('Notifications enabled successfully!', 'success');
} catch (tokenError) {
console.error('Error getting FCM token:', tokenError);
showStatus('Failed to get FCM token: ' + tokenError.message, 'error');
}
} else {
console.log('Notification permission denied.');
showStatus('Notification permission denied.', 'error');
}
} catch (error) {
console.error('Error requesting notification permission:', error);
showStatus('Failed to enable notifications: ' + error.message, 'error');
}
}
// Register FCM token with server
async function registerToken(username, token) {
try {
console.log('Attempting to register token:', { username, token: token.substring(0, 20) + '...' });
const response = await fetch('/register-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, token })
});
console.log('Registration response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Server returned ${response.status}: ${errorText}`);
}
const result = await response.json();
console.log('Token registered successfully:', result);
showStatus(`Token registered for ${username}`, 'success');
} catch (error) {
console.error('Error registering token:', error);
showStatus('Failed to register token with server: ' + error.message, 'error');
}
}
// Handle foreground messages (guard against duplicate registration)
function handleForegroundMessages() {
if (foregroundHandlerSetup) return;
foregroundHandlerSetup = true;
messaging.onMessage(function(payload) {
console.log('Received foreground message: ', payload);
// Show notification in foreground
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/icon-192.png',
badge: '/icon-192.png'
};
new Notification(notificationTitle, notificationOptions);
showStatus(`New notification: ${payload.notification.body}`, 'info');
});
}
// Login function
async function login(event) {
if (event) event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!users[username] || users[username].password !== password) {
showStatus('Invalid username or password', 'error');
return;
}
currentUser = username;
localStorage.setItem('currentUser', username);
showUserInfo();
showStatus(`Logged in as ${users[username].name}`, 'success');
// Initialize Firebase and request notifications
if (typeof firebase !== 'undefined') {
await initializeFirebase();
await requestNotificationPermission();
handleForegroundMessages();
} else {
showStatus('Firebase not loaded. Please check your connection.', 'error');
}
}
// Logout function
function logout() {
currentUser = null;
fcmToken = null;
localStorage.removeItem('currentUser');
document.getElementById('loginForm').style.display = 'block';
document.getElementById('userInfo').style.display = 'none';
document.getElementById('username').value = '';
document.getElementById('password').value = '';
showStatus('Logged out successfully.', 'info');
}
// Send notification function
async function sendNotification() {
if (!currentUser) {
showStatus('Please login first.', 'error');
return;
}
try {
// First check registered users
const usersResponse = await fetch('/users');
const users = await usersResponse.json();
console.log('Registered users:', users);
const response = await fetch('/send-notification', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fromUser: currentUser,
title: 'Test Notification',
body: `Notification sent from ${currentUser} at ${new Date().toLocaleTimeString()}`
})
});
if (!response.ok) {
throw new Error('Failed to send notification');
}
const result = await response.json();
console.log('Send result:', result);
if (result.recipients === 0) {
showStatus('No other users have registered tokens. Open the app on other devices and enable notifications.', 'error');
} else {
showStatus(`Notification sent to ${result.recipients} user(s)!`, 'success');
}
} catch (error) {
console.error('Error sending notification:', error);
showStatus('Failed to send notification.', 'error');
}
}
// Show status message
function showStatus(message, type) {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = `status ${type}`;
statusEl.style.display = 'block';
setTimeout(() => {
statusEl.style.display = 'none';
}, 5000);
}
// Register service worker and handle PWA installation
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
// Handle PWA installation
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('beforeinstallprompt fired');
e.preventDefault();
deferredPrompt = e;
// Show install button or banner
showInstallButton();
});
function showInstallButton() {
const installBtn = document.createElement('button');
installBtn.textContent = 'Install App';
installBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #2196F3;
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
z-index: 1000;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
installBtn.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response to the install prompt: ${outcome}`);
deferredPrompt = null;
installBtn.remove();
}
});
document.body.appendChild(installBtn);
}
})
.catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});
});
}

View File

@@ -0,0 +1,48 @@
importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js');
firebase.initializeApp({
apiKey: "AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA",
authDomain: "fcmtest-push.firebaseapp.com",
projectId: "fcmtest-push",
storageBucket: "fcmtest-push.firebasestorage.app",
messagingSenderId: "439263996034",
appId: "1:439263996034:web:9b3d52af2c402e65fdec9b"
});
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function(payload) {
console.log('Received background message:', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/icon-192.png',
badge: '/icon-192.png',
tag: 'fcm-test',
data: payload.data
};
self.registration.showNotification(notificationTitle, notificationOptions);
});
self.addEventListener('notificationclick', function(event) {
console.log('Notification clicked:', event);
event.notification.close();
if (event.action === 'close') return;
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
});

BIN
fcm-app/public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
fcm-app/public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

111
fcm-app/public/index.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FCM Test PWA</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2196F3">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 400px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.login-form {
display: block;
}
.user-info {
display: none;
}
input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 12px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px 0;
}
button:hover {
background-color: #1976D2;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
text-align: center;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.user-display {
background-color: #e3f2fd;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
text-align: center;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>FCM Test PWA</h1>
<div id="status" class="status" style="display: none;"></div>
<div id="loginForm" class="login-form">
<h2>Login</h2>
<input type="text" id="username" placeholder="Username (pwau1, pwau2, or pwau3)" required>
<input type="password" id="password" placeholder="Password" required>
<button onclick="login()">Login</button>
</div>
<div id="userInfo" class="user-info">
<div class="user-display">
Logged in as: <span id="currentUser"></span>
</div>
<button id="sendNotificationBtn" onclick="sendNotification()">Send Notification</button>
<button id="logoutBtn" onclick="logout()">Logout</button>
</div>
</div>
<script src="/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
{
"name": "FCM Test PWA",
"short_name": "FCM Test",
"description": "PWA for testing Firebase Cloud Messaging",
"start_url": "/",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"background_color": "#ffffff",
"theme_color": "#2196F3",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"purpose": "any maskable",
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"purpose": "any maskable",
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"categories": ["utilities", "productivity"],
"lang": "en-US"
}

82
fcm-app/public/sw.js Normal file
View File

@@ -0,0 +1,82 @@
const CACHE_NAME = 'fcm-test-pwa-v1';
const urlsToCache = [
'/',
'/index.html',
'/app.js',
'/manifest.json'
];
// Install event
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
// Fetch event
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
// Background sync for FCM
importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js');
// Initialize Firebase in service worker
firebase.initializeApp({
apiKey: "AIzaSyAw1v4COZ68Po8CuwVKrQq0ygf7zFd2QCA",
authDomain: "fcmtest-push.firebaseapp.com",
projectId: "fcmtest-push",
storageBucket: "fcmtest-push.firebasestorage.app",
messagingSenderId: "439263996034",
appId: "1:439263996034:web:9b3d52af2c402e65fdec9b"
});
const messaging = firebase.messaging();
// Handle notification clicks
self.addEventListener('notificationclick', function(event) {
console.log('Notification clicked:', event);
event.notification.close();
if (event.action === 'close') return;
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(clientList) {
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
});
// Handle background messages
messaging.onBackgroundMessage(function(payload) {
console.log('Received background message ', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/icon-192.png',
badge: '/icon-192.png',
tag: 'fcm-test'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});