1(function () { 2 // iOS Safari (non-PWA) doesn't support Web Push and the body-wide MutationObservers 3 // below interact poorly with Blazor enhanced navigation on iOS, leaving authenticated 4 // pages partially loaded. Short-circuit with no-op stubs so callers don't break. 5 var _ua = navigator.userAgent || ''; 6 var _isIOS = /iPad|iPhone|iPod/.test(_ua) || 7 (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); 8 var _isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || 9 window.navigator.standalone === true; 10 if (_isIOS && !_isStandalone) { 11 var unsupported = function () { return Promise.resolve({ ok: false, error: 'unsupported' }); }; 12 window.fliPush = { 13 init: function () { return Promise.resolve(null); }, 14 subscribe: unsupported, 15 unsubscribe: function () { return Promise.resolve({ ok: true }); }, 16 status: function () { return Promise.resolve('unsupported'); } 17 }; 18 window.fliPushHintInit = function () { }; 19 window.fliPushHintDismiss = function () { 20 var el = document.getElementById('fli-push-hint'); 21 if (el) el.remove(); 22 }; 23 window.fliPushHintAllow = function () { window.fliPushHintDismiss(); }; 24 window.fliPushUiAction = function () { }; 25 window.fliPushUiTest = function () { }; 26 window.fliPushUiRender = function () { 27 var box = document.getElementById('fli-push-ui'); 28 if (box) box.innerHTML = '<div class="alert alert-warning small py-2 mb-0"><i class="bi bi-exclamation-triangle me-1"></i>Web Push isn\'t supported in iOS Safari. Add this site to your Home Screen to enable notifications.</div>'; 29 }; 30 return; 31 } 32 33 function urlBase64ToUint8Array(base64String) { 34 const padding = '='.repeat((4 - base64String.length % 4) % 4); 35 const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); 36 const raw = atob(base64); 37 const out = new Uint8Array(raw.length); 38 for (let i = 0; i < raw.length; ++i) out[i] = raw.charCodeAt(i); 39 return out; 40 } 41 42 function supported() { 43 return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window; 44 } 45 46 async function init() { 47 if (!supported()) return null; 48 try { 49 return await navigator.serviceWorker.register('/service-worker.js'); 50 } catch (e) { 51 console.warn('SW register failed', e); 52 return null; 53 } 54 } 55 56 async function getVapidKey() { 57 const r = await fetch('/api/push/vapid-public-key'); 58 if (!r.ok) return null; 59 const d = await r.json(); 60 return d.key; 61 } 62 63 async function subscribe() { 64 if (!supported()) return { ok: false, error: 'unsupported' }; 65 const perm = await Notification.requestPermission(); 66 if (perm !== 'granted') return { ok: false, error: 'denied' }; 67 68 const reg = await navigator.serviceWorker.register('/service-worker.js'); 69 await navigator.serviceWorker.ready; 70 71 const key = await getVapidKey(); 72 if (!key) return { ok: false, error: 'no-vapid-key' }; 73 74 let sub = await reg.pushManager.getSubscription(); 75 if (!sub) { 76 sub = await reg.pushManager.subscribe({ 77 userVisibleOnly: true, 78 applicationServerKey: urlBase64ToUint8Array(key) 79 }); 80 } 81 82 const raw = sub.toJSON(); 83 // Include the registered Drop device id (if any) so the server can exclude 84 // this device from notifications it triggered itself (e.g. drops sent here). 85 let deviceId = null; 86 try { deviceId = localStorage.getItem('fastlinkit_device_id'); } catch (e) { } 87 const body = { 88 endpoint: sub.endpoint, 89 p256dh: raw.keys && raw.keys.p256dh, 90 auth: raw.keys && raw.keys.auth, 91 deviceId: deviceId || null 92 }; 93 const r = await fetch('/api/push/subscribe', { 94 method: 'POST', 95 headers: { 'Content-Type': 'application/json' }, 96 body: JSON.stringify(body) 97 }); 98 return { ok: r.ok }; 99 } 100 101 async function unsubscribe() { 102 if (!supported()) return { ok: false, error: 'unsupported' }; 103 const reg = await navigator.serviceWorker.getRegistration(); 104 if (!reg) return { ok: true }; 105 const sub = await reg.pushManager.getSubscription(); 106 if (!sub) return { ok: true }; 107 const endpoint = sub.endpoint; 108 await sub.unsubscribe(); 109 await fetch('/api/push/unsubscribe', { 110 method: 'DELETE', 111 headers: { 'Content-Type': 'application/json' }, 112 body: JSON.stringify({ endpoint }) 113 }); 114 return { ok: true }; 115 } 116 117 async function status() { 118 if (!supported()) return 'unsupported'; 119 const perm = Notification.permission; 120 if (perm !== 'granted') return perm; 121 const reg = await navigator.serviceWorker.getRegistration(); 122 if (!reg) return 'granted-no-sw'; 123 const sub = await reg.pushManager.getSubscription(); 124 return sub ? 'subscribed' : 'granted'; 125 } 126 127 window.fliPush = { init, subscribe, unsubscribe, status }; 128 129 // Banner helpers (used by the "Enable browser notifications" hint in MainLayout) 130 window.fliPushHintDismiss = function () { 131 try { localStorage.setItem('fli-push-hint-dismissed', '1'); } catch (e) { } 132 var el = document.getElementById('fli-push-hint'); 133 if (el) el.remove(); 134 }; 135 window.fliPushHintAllow = async function (btn) { 136 if (btn) btn.disabled = true; 137 try { await subscribe(); } finally { window.fliPushHintDismiss(); } 138 }; 139 // Manage-page UI (status card with Subscribe / Unsubscribe / Send test buttons) 140 window.fliPushUiAction = async function (btn, action) { 141 if (btn) btn.disabled = true; 142 try { 143 if (action === 'sub') await subscribe(); 144 else await unsubscribe(); 145 } finally { window.fliPushUiRender(); } 146 }; 147 window.fliPushUiTest = async function (btn) { 148 if (btn) btn.disabled = true; 149 var out = document.getElementById('fli-push-test-result'); 150 if (out) out.textContent = 'sending...'; 151 try { 152 var r = await fetch('/api/push/test', { method: 'POST' }); 153 var body = null; 154 try { body = await r.json(); } catch (e) { } 155 156 if (!out) return; 157 if (!r.ok || !body) { 158 out.textContent = 'failed (HTTP ' + r.status + ')'; 159 return; 160 } 161 if (!body.vapid) { 162 out.textContent = 'server is missing VAPID keys â push cannot be sent'; 163 } else if (!body.enabled) { 164 out.textContent = 'browser push is switched off â click Subscribe or turn it on in Preferences'; 165 } else if (body.subscriptions === 0) { 166 out.textContent = 'no devices are subscribed â click Subscribe first'; 167 } else if (body.sent > 0) { 168 out.textContent = 'sent to ' + body.sent + ' / ' + body.subscriptions 169 + ' device' + (body.subscriptions === 1 ? '' : 's') 170 + ' â check your system tray'; 171 } else if (body.expired > 0) { 172 out.textContent = 'all ' + body.expired + ' subscriptions were expired and have been removed â click Subscribe again'; 173 } else { 174 out.textContent = 'push declined: ' + (body.error || 'unknown error'); 175 } 176 } catch (e) { if (out) out.textContent = 'error: ' + e.message; } 177 finally { if (btn) btn.disabled = false; } 178 }; 179 window.fliPushUiRender = function () { 180 var box = document.getElementById('fli-push-ui'); 181 if (!box) return; 182 status().then(function (st) { 183 var html = ''; 184 if (st === 'unsupported') { 185 html = '<div class="alert alert-warning small py-2 mb-0"><i class="bi bi-exclamation-triangle me-1"></i>Your browser does not support push notifications.</div>'; 186 } else if (st === 'denied') { 187 html = '<div class="alert alert-danger small py-2 mb-0"><i class="bi bi-shield-x me-1"></i><strong>Blocked.</strong> Click the lock icon in the address bar â Notifications â Allow, then reload.</div>'; 188 } else if (st === 'subscribed') { 189 html = '<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Active on this device</span>' + 190 ' <button type="button" id="fli-push-btn-unsub" class="btn btn-outline-secondary btn-sm ms-2"><i class="bi bi-bell-slash me-1"></i>Unsubscribe this device</button>' + 191 ' <button type="button" id="fli-push-btn-test"
191class="btn btn-outline-primary btn-sm ms-2"><i class="bi bi-send me-1"></i>Send test</button>' + 192 ' <span id="fli-push-test-result" class="small text-muted ms-2"></span>'; 193 } else { 194 html = '<button type="button" id="fli-push-btn-sub" class="btn btn-outline-primary btn-sm"><i class="bi bi-bell me-1"></i>Subscribe this device</button>' + 195 '<div class="form-text mt-1"><i class="bi bi-info-circle me-1"></i>Your browser will ask for permission.</div>'; 196 } 197 box.innerHTML = html; 198 function bind(id, handler) { 199 var el = document.getElementById(id); 200 if (!el) return; 201 var fired = false; 202 // pointerup is harder to intercept than click 203 el.addEventListener('pointerup', function (ev) { 204 if (ev.button !== 0) return; 205 ev.preventDefault(); ev.stopPropagation(); 206 fired = true; 207 handler(); 208 }); 209 el.addEventListener('click', function (ev) { 210 ev.preventDefault(); ev.stopPropagation(); 211 if (!fired) handler(); 212 fired = false; 213 }); 214 } 215 bind('fli-push-btn-unsub', function () { window.fliPushUiAction(document.getElementById('fli-push-btn-unsub'), 'unsub'); }); 216 bind('fli-push-btn-test', function () { window.fliPushUiTest(document.getElementById('fli-push-btn-test')); }); 217 bind('fli-push-btn-sub', function () { window.fliPushUiAction(document.getElementById('fli-push-btn-sub'), 'sub'); }); 218 }); 219 }; 220 // Auto-render whenever the Manage-page container appears in the DOM 221 try { 222 var uiMo = new MutationObserver(function () { 223 if (document.getElementById('fli-push-ui')) window.fliPushUiRender(); 224 }); 225 if (document.body) uiMo.observe(document.body, { childList: true, subtree: true }); 226 else document.addEventListener('DOMContentLoaded', function () { uiMo.observe(document.body, { childList: true, subtree: true }); }); 227 } catch (e) { } 228 229 window.fliPushHintInit = function () { 230 try { if (localStorage.getItem('fli-push-hint-dismissed') === '1') return; } catch (e) { } 231 function check() { 232 if (!window.fliPush) { setTimeout(check, 400); return; } 233 window.fliPush.status().then(function (st) { 234 var el = document.getElementById('fli-push-hint'); 235 if (!el) return; 236 // Use Bootstrap d-none / d-flex classes because d-flex applies !important. 237 if (st === 'default') { 238 el.classList.remove('d-none'); 239 el.classList.add('d-flex'); 240 } else { 241 el.classList.remove('d-flex'); 242 el.classList.add('d-none'); 243 } 244 }); 245 } 246 check(); 247 try { 248 var mo = new MutationObserver(function () { 249 if (document.getElementById('fli-push-hint')) check(); 250 }); 251 mo.observe(document.body, { childList: true, subtree: true }); 252 } catch (e) { } 253 }; 254})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.