|
|
|
|
@@ -63,15 +63,19 @@ const isMobileDevice = isIOS || isAndroid;
|
|
|
|
|
|
|
|
|
|
function TestNotificationsModal({ onClose }) {
|
|
|
|
|
const toast = useToast();
|
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
const isAdmin = user?.role === 'admin';
|
|
|
|
|
const [debugData, setDebugData] = useState(null);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [testing, setTesting] = useState(false);
|
|
|
|
|
|
|
|
|
|
const permission = (typeof Notification !== 'undefined') ? Notification.permission : 'unsupported';
|
|
|
|
|
const [permission, setPermission] = useState(
|
|
|
|
|
(typeof Notification !== 'undefined') ? Notification.permission : 'unsupported'
|
|
|
|
|
);
|
|
|
|
|
const [cachedToken, setCachedToken] = useState(localStorage.getItem('rc_fcm_token'));
|
|
|
|
|
const [lastError, setLastError] = useState(localStorage.getItem('rc_fcm_error'));
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
|
|
|
|
if (!isAdmin) return; // debug endpoint is admin-only
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
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) => {
|
|
|
|
|
setTesting(true);
|
|
|
|
|
@@ -112,6 +131,7 @@ function TestNotificationsModal({ onClose }) {
|
|
|
|
|
const reregister = () => {
|
|
|
|
|
localStorage.removeItem('rc_fcm_token');
|
|
|
|
|
localStorage.removeItem('rc_fcm_error');
|
|
|
|
|
localStorage.removeItem('rc_webpush_endpoint'); // clear iOS webpush cache too
|
|
|
|
|
setCachedToken(null);
|
|
|
|
|
setLastError(null);
|
|
|
|
|
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} />}
|
|
|
|
|
{lastError && <DebugRow label="Last reg. error" value={lastError} bad={true} />}
|
|
|
|
|
</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' }}>
|
|
|
|
|
<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>}
|
|
|
|
|
@@ -325,7 +350,8 @@ export default function UserFooter({ onProfile, onHelp, onAbout, mobileCompact=f
|
|
|
|
|
|
|
|
|
|
{showMenu && (
|
|
|
|
|
<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">
|
|
|
|
|
<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>
|
|
|
|
|
|