FCM test
This commit is contained in:
@@ -112,4 +112,47 @@ 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
|
||||||
|
router.post('/test', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const subs = await query(req.schema,
|
||||||
|
'SELECT * FROM push_subscriptions WHERE user_id = $1 AND fcm_token IS NOT NULL',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
if (subs.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: 'No push subscription found for your account. Grant notification permission and reload the app first.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const messaging = getMessaging();
|
||||||
|
if (!messaging) {
|
||||||
|
return res.status(503).json({ error: 'Firebase Admin not initialised on server — check FIREBASE_SERVICE_ACCOUNT in .env' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const sub of subs) {
|
||||||
|
try {
|
||||||
|
await messaging.send({
|
||||||
|
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}`);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.json({ results });
|
||||||
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = { router, sendPushToUser };
|
module.exports = { router, sendPushToUser };
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export default function ProfileModal({ onClose }) {
|
|||||||
const [newPw, setNewPw] = useState('');
|
const [newPw, setNewPw] = useState('');
|
||||||
const [confirmPw, setConfirmPw] = useState('');
|
const [confirmPw, setConfirmPw] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tab, setTab] = useState('profile'); // 'profile' | 'password'
|
const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications'
|
||||||
|
const [pushTesting, setPushTesting] = useState(false);
|
||||||
|
const [pushResult, setPushResult] = useState(null);
|
||||||
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
||||||
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
||||||
|
|
||||||
@@ -101,6 +103,7 @@ export default function ProfileModal({ onClose }) {
|
|||||||
<div className="flex gap-2" style={{ marginBottom: 20 }}>
|
<div className="flex gap-2" style={{ marginBottom: 20 }}>
|
||||||
<button className={`btn btn-sm ${tab === 'profile' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setTab('profile')}>Profile</button>
|
<button className={`btn btn-sm ${tab === 'profile' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setTab('profile')}>Profile</button>
|
||||||
<button className={`btn btn-sm ${tab === 'password' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setTab('password')}>Change Password</button>
|
<button className={`btn btn-sm ${tab === 'password' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setTab('password')}>Change Password</button>
|
||||||
|
<button className={`btn btn-sm ${tab === 'notifications' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => { setTab('notifications'); setPushResult(null); }}>Notifications</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === 'profile' && (
|
{tab === 'profile' && (
|
||||||
@@ -167,6 +170,56 @@ export default function ProfileModal({ onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{tab === 'notifications' && (
|
||||||
|
<div className="flex-col gap-3">
|
||||||
|
<div style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
|
||||||
|
<p style={{ margin: '0 0 8px' }}>Tap <strong>Send Test Notification</strong> to trigger a push to this device. The notification will arrive shortly if everything is configured correctly.</p>
|
||||||
|
<p style={{ margin: 0 }}>If it doesn't arrive, check:<br/>
|
||||||
|
• Notification permission granted (browser prompt)<br/>
|
||||||
|
• Android Settings → Apps → RosterChirp → Notifications → Enabled<br/>
|
||||||
|
• App is backgrounded when the test fires
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={pushTesting}
|
||||||
|
onClick={async () => {
|
||||||
|
setPushTesting(true);
|
||||||
|
setPushResult(null);
|
||||||
|
try {
|
||||||
|
const { results } = await api.testPush();
|
||||||
|
setPushResult({ ok: true, results });
|
||||||
|
} catch (e) {
|
||||||
|
setPushResult({ ok: false, error: e.message });
|
||||||
|
} finally {
|
||||||
|
setPushTesting(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pushTesting ? 'Sending…' : 'Send Test Notification'}
|
||||||
|
</button>
|
||||||
|
{pushResult && (
|
||||||
|
<div style={{
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: pushResult.ok ? 'var(--surface-variant)' : '#fdecea',
|
||||||
|
color: pushResult.ok ? 'var(--text-primary)' : '#c62828',
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
{pushResult.ok ? (
|
||||||
|
pushResult.results.map((r, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<strong>{r.device}</strong>: {r.status === 'sent' ? '✓ Sent — check your device for the notification' : `✗ Failed — ${r.error}`}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div>✗ {pushResult.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{tab === 'password' && (
|
{tab === 'password' && (
|
||||||
<div className="flex-col gap-3">
|
<div className="flex-col gap-3">
|
||||||
<div className="flex-col gap-1">
|
<div className="flex-col gap-1">
|
||||||
|
|||||||
@@ -126,19 +126,29 @@ export default function Chat() {
|
|||||||
if (granted !== 'granted') return;
|
if (granted !== 'granted') return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[Push] Requesting FCM token...');
|
||||||
const fcmToken = await getToken(firebaseMessaging, {
|
const fcmToken = await getToken(firebaseMessaging, {
|
||||||
vapidKey,
|
vapidKey,
|
||||||
serviceWorkerRegistration: reg,
|
serviceWorkerRegistration: reg,
|
||||||
});
|
});
|
||||||
if (!fcmToken) return;
|
if (!fcmToken) {
|
||||||
|
console.warn('[Push] getToken() returned null — notification permission may not be granted at OS level, or VAPID key is wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[Push] FCM token obtained:', fcmToken.slice(0, 30) + '...');
|
||||||
|
|
||||||
const token = localStorage.getItem('tc_token') || sessionStorage.getItem('tc_token');
|
const token = localStorage.getItem('tc_token') || sessionStorage.getItem('tc_token');
|
||||||
await fetch('/api/push/subscribe', {
|
const subRes = await fetch('/api/push/subscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||||||
body: JSON.stringify({ fcmToken }),
|
body: JSON.stringify({ fcmToken }),
|
||||||
});
|
});
|
||||||
console.log('[Push] FCM subscription registered');
|
if (!subRes.ok) {
|
||||||
|
const err = await subRes.json().catch(() => ({}));
|
||||||
|
console.warn('[Push] Subscribe failed:', err.error || subRes.status);
|
||||||
|
} else {
|
||||||
|
console.log('[Push] FCM subscription registered successfully');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Push] FCM subscription failed:', e.message);
|
console.warn('[Push] FCM subscription failed:', e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +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'),
|
||||||
|
|
||||||
// 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