PageSourceSearch

https://flnk.it/js/push.js

js flnk.it collected 2026-09-24 09:08:09 UTC 12,212 bytes, 254 lines download raw bytes

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.