v0.12.26 iOS notification bug fix

This commit is contained in:
2026-03-28 14:51:00 -04:00
parent f40bb123d2
commit 252c0e09cb
5 changed files with 43 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "rosterchirp-backend", "name": "rosterchirp-backend",
"version": "0.12.35", "version": "0.12.36",
"description": "RosterChirp backend server", "description": "RosterChirp backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.12.35}" VERSION="${1:-0.12.36}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="rosterchirp" IMAGE_NAME="rosterchirp"

View File

@@ -1,6 +1,6 @@
{ {
"name": "rosterchirp-frontend", "name": "rosterchirp-frontend",
"version": "0.12.35", "version": "0.12.36",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -63,15 +63,19 @@ const isMobileDevice = isIOS || isAndroid;
function TestNotificationsModal({ onClose }) { function TestNotificationsModal({ onClose }) {
const toast = useToast(); const toast = useToast();
const { user } = useAuth();
const isAdmin = user?.role === 'admin';
const [debugData, setDebugData] = useState(null); const [debugData, setDebugData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false); const [testing, setTesting] = useState(false);
const [permission, setPermission] = useState(
const permission = (typeof Notification !== 'undefined') ? Notification.permission : 'unsupported'; (typeof Notification !== 'undefined') ? Notification.permission : 'unsupported'
);
const [cachedToken, setCachedToken] = useState(localStorage.getItem('rc_fcm_token')); const [cachedToken, setCachedToken] = useState(localStorage.getItem('rc_fcm_token'));
const [lastError, setLastError] = useState(localStorage.getItem('rc_fcm_error')); const [lastError, setLastError] = useState(localStorage.getItem('rc_fcm_error'));
const load = async () => { const load = async () => {
if (!isAdmin) return; // debug endpoint is admin-only
setLoading(true); setLoading(true);
try { try {
const data = await api.pushDebug(); const data = await api.pushDebug();
@@ -83,7 +87,22 @@ function TestNotificationsModal({ onClose }) {
} }
}; };
useEffect(() => { load(); }, []); useEffect(() => { load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const handleGrantPermission = async () => {
if (typeof Notification === 'undefined') {
toast('Notifications not supported on this device/browser', 'error');
return;
}
const result = await Notification.requestPermission();
setPermission(result);
if (result === 'granted') {
window.dispatchEvent(new CustomEvent('rosterchirp:push-init'));
toast('Permission granted — registering…', 'success');
} else {
toast('Permission denied', 'error');
}
};
const doTest = async (mode) => { const doTest = async (mode) => {
setTesting(true); setTesting(true);
@@ -112,6 +131,7 @@ function TestNotificationsModal({ onClose }) {
const reregister = () => { const reregister = () => {
localStorage.removeItem('rc_fcm_token'); localStorage.removeItem('rc_fcm_token');
localStorage.removeItem('rc_fcm_error'); localStorage.removeItem('rc_fcm_error');
localStorage.removeItem('rc_webpush_endpoint'); // clear iOS webpush cache too
setCachedToken(null); setCachedToken(null);
setLastError(null); setLastError(null);
window.dispatchEvent(new CustomEvent('rosterchirp:push-init')); window.dispatchEvent(new CustomEvent('rosterchirp:push-init'));
@@ -147,6 +167,11 @@ function TestNotificationsModal({ onClose }) {
{!isIOS && debugData && <DebugRow label="Firebase Admin" value={debugData.firebaseAdminReady ? 'Ready' : 'Not ready'} ok={debugData.firebaseAdminReady} bad={!debugData.firebaseAdminReady} />} {!isIOS && debugData && <DebugRow label="Firebase Admin" value={debugData.firebaseAdminReady ? 'Ready' : 'Not ready'} ok={debugData.firebaseAdminReady} bad={!debugData.firebaseAdminReady} />}
{lastError && <DebugRow label="Last reg. error" value={lastError} bad={true} />} {lastError && <DebugRow label="Last reg. error" value={lastError} bad={true} />}
</div> </div>
{permission === 'default' && (
<button className="btn btn-sm btn-primary" onClick={handleGrantPermission} style={{ marginBottom: 8 }}>
Grant Permission
</button>
)}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button className="btn btn-sm btn-primary" onClick={reregister}>Re-register</button> <button className="btn btn-sm btn-primary" onClick={reregister}>Re-register</button>
{!isIOS && <button className="btn btn-sm btn-secondary" onClick={clearToken}>Clear token</button>} {!isIOS && <button className="btn btn-sm btn-secondary" onClick={clearToken}>Clear token</button>}
@@ -325,7 +350,8 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f
{showMenu && ( {showMenu && (
<div ref={menuRef} className="footer-menu"> <div ref={menuRef} className="footer-menu">
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 4 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 700, fontSize: 14, color: 'var(--text-primary)', paddingLeft: 4 }}>User Menu</span>
<button onClick={() => setShowMenu(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 4px', color: 'var(--text-tertiary)', lineHeight: 1 }} aria-label="Close menu"> <button onClick={() => setShowMenu(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 4px', color: 'var(--text-tertiary)', lineHeight: 1 }} aria-label="Close menu">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button> </button>

View File

@@ -262,7 +262,15 @@ export default function Chat() {
const handleVisibility = () => { const handleVisibility = () => {
if (document.visibilityState === 'visible') registerPush(); if (document.visibilityState === 'visible') registerPush();
}; };
const handlePushInit = () => registerPush(); // When the user explicitly requests push (via the Notifications toggle or
// re-register button), ask for permission if it hasn't been granted yet.
const handlePushInit = async () => {
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
const result = await Notification.requestPermission();
if (result !== 'granted') return;
}
registerPush();
};
document.addEventListener('visibilitychange', handleVisibility); document.addEventListener('visibilitychange', handleVisibility);
window.addEventListener('rosterchirp:push-init', handlePushInit); window.addEventListener('rosterchirp:push-init', handlePushInit);
return () => { return () => {