FCM test
This commit is contained in:
@@ -112,7 +112,10 @@ router.post('/unsubscribe', authMiddleware, async (req, res) => {
|
|||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} 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) => {
|
router.post('/test', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const subs = await query(req.schema,
|
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' });
|
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 = [];
|
const results = [];
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
try {
|
try {
|
||||||
await messaging.send({
|
const message = {
|
||||||
token: sub.fcm_token,
|
token: sub.fcm_token,
|
||||||
data: {
|
|
||||||
title: 'RosterChirp Test',
|
|
||||||
body: 'Push notifications are working! 🎉',
|
|
||||||
url: '/',
|
|
||||||
groupId: '',
|
|
||||||
},
|
|
||||||
android: { priority: 'high' },
|
android: { priority: 'high' },
|
||||||
webpush: { headers: { Urgency: '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) {
|
} catch (err) {
|
||||||
results.push({ device: sub.device, status: 'failed', error: err.message, code: err.code });
|
results.push({ device: sub.device, mode, status: 'failed', error: err.message, code: err.code });
|
||||||
console.error(`[Push] Test notification failed for user ${req.user.id} device=${sub.device}:`, err.message);
|
console.error(`[Push] Test (${mode}) failed for user ${req.user.id} device=${sub.device}:`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.json({ results });
|
res.json({ results });
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ self.addEventListener('fetch', (event) => {
|
|||||||
let badgeCount = 0;
|
let badgeCount = 0;
|
||||||
|
|
||||||
function showRosterChirpNotification(data) {
|
function showRosterChirpNotification(data) {
|
||||||
|
console.log('[SW] showRosterChirpNotification:', JSON.stringify(data));
|
||||||
badgeCount++;
|
badgeCount++;
|
||||||
if (self.navigator?.setAppBadge) self.navigator.setAppBadge(badgeCount).catch(() => {});
|
if (self.navigator?.setAppBadge) self.navigator.setAppBadge(badgeCount).catch(() => {});
|
||||||
|
|
||||||
@@ -64,17 +65,29 @@ function showRosterChirpNotification(data) {
|
|||||||
data: { url: data.url || '/' },
|
data: { url: data.url || '/' },
|
||||||
tag: data.groupId ? `rosterchirp-group-${data.groupId}` : 'rosterchirp-message',
|
tag: data.groupId ? `rosterchirp-group-${data.groupId}` : 'rosterchirp-message',
|
||||||
renotify: true,
|
renotify: true,
|
||||||
vibrate: [200, 100, 200], // haptic pattern — also activates the OS sound channel
|
vibrate: [200, 100, 200],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FCM background messages ───────────────────────────────────────────────────
|
// ── FCM background messages ───────────────────────────────────────────────────
|
||||||
if (messaging) {
|
if (messaging) {
|
||||||
messaging.onBackgroundMessage((payload) => {
|
messaging.onBackgroundMessage((payload) => {
|
||||||
|
console.log('[SW] onBackgroundMessage received, data:', JSON.stringify(payload.data));
|
||||||
return showRosterChirpNotification(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 ────────────────────────────────────────────────────────
|
// ── Notification click ────────────────────────────────────────────────────────
|
||||||
self.addEventListener('notificationclick', (event) => {
|
self.addEventListener('notificationclick', (event) => {
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
|
|||||||
@@ -180,24 +180,50 @@ export default function ProfileModal({ onClose }) {
|
|||||||
• App is backgrounded when the test fires
|
• App is backgrounded when the test fires
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex gap-2">
|
||||||
className="btn btn-primary"
|
<button
|
||||||
disabled={pushTesting}
|
className="btn btn-primary"
|
||||||
onClick={async () => {
|
style={{ flex: 1 }}
|
||||||
setPushTesting(true);
|
disabled={pushTesting}
|
||||||
setPushResult(null);
|
onClick={async () => {
|
||||||
try {
|
setPushTesting(true);
|
||||||
const { results } = await api.testPush();
|
setPushResult(null);
|
||||||
setPushResult({ ok: true, results });
|
try {
|
||||||
} catch (e) {
|
const { results } = await api.testPush('data');
|
||||||
setPushResult({ ok: false, error: e.message });
|
setPushResult({ ok: true, results, mode: 'data' });
|
||||||
} finally {
|
} catch (e) {
|
||||||
setPushTesting(false);
|
setPushResult({ ok: false, error: e.message });
|
||||||
}
|
} finally {
|
||||||
}}
|
setPushTesting(false);
|
||||||
>
|
}
|
||||||
{pushTesting ? 'Sending…' : 'Send Test Notification'}
|
}}
|
||||||
</button>
|
>
|
||||||
|
{pushTesting ? 'Sending…' : 'Test (via SW)'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
disabled={pushTesting}
|
||||||
|
onClick={async () => {
|
||||||
|
setPushTesting(true);
|
||||||
|
setPushResult(null);
|
||||||
|
try {
|
||||||
|
const { results } = await api.testPush('browser');
|
||||||
|
setPushResult({ ok: true, results, mode: 'browser' });
|
||||||
|
} catch (e) {
|
||||||
|
setPushResult({ ok: false, error: e.message });
|
||||||
|
} finally {
|
||||||
|
setPushTesting(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pushTesting ? 'Sending…' : 'Test (via Browser)'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', lineHeight: 1.4 }}>
|
||||||
|
<strong>Test (via SW)</strong> — normal production path, service worker shows notification.<br/>
|
||||||
|
<strong>Test (via Browser)</strong> — bypasses service worker; Chrome displays directly.
|
||||||
|
</div>
|
||||||
{pushResult && (
|
{pushResult && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export const api = {
|
|||||||
getFirebaseConfig: () => req('GET', '/push/firebase-config'),
|
getFirebaseConfig: () => req('GET', '/push/firebase-config'),
|
||||||
subscribePush: (fcmToken) => req('POST', '/push/subscribe', { fcmToken }),
|
subscribePush: (fcmToken) => req('POST', '/push/subscribe', { fcmToken }),
|
||||||
unsubscribePush: () => req('POST', '/push/unsubscribe'),
|
unsubscribePush: () => req('POST', '/push/unsubscribe'),
|
||||||
testPush: () => req('POST', '/push/test'),
|
testPush: (mode = 'data') => req('POST', `/push/test?mode=${mode}`),
|
||||||
|
|
||||||
// Link preview
|
// Link preview
|
||||||
getLinkPreview: (url) => req('GET', `/link-preview?url=${encodeURIComponent(url)}`),
|
getLinkPreview: (url) => req('GET', `/link-preview?url=${encodeURIComponent(url)}`),
|
||||||
|
|||||||
Reference in New Issue
Block a user