PageSourceSearch

https://probiwaem.ru/static/notification_client.js?v=0.31.27

js probiwaem.ru collected 2026-10-02 02:07:46 UTC 8,114 bytes, 202 lines download raw bytes

1/* notification_client.js — клиентский фасад для Browser Push (v0.24.2).
2 *
3 * Зачем: даёт UI-коду (account.html, watchlist add-flow) единый API без
4 * прямой работы с Service Worker / PushManager.
5 *
6 * Главный API (window.pbNotifications):
7 *   register()   → запрашивает permission + регистрирует SW + subscribe + POST на бэк.
8 *   unregister() → отписывает + DELETE на бэке.
9 *   status()     → {permission, subscribed, sw_ready, supported}.
10 *
11 * SW регистрируется ЛЕНИВО — только при первом вызове register(). До этого
12 * браузер не загружает sw_push.js → нулевой риск конфликта с /sw.js (PWA-кэш,
13 * pwa_register.js). Скоуп /static/ выбран чтобы не перекрывать root-SW.
14 *
15 * Зависимости от бэка (Agent H):
16 *   GET    /api/auth/vapid-public-key            → {key}
17 *   POST   /api/my/notifications/push/subscribe  ← PushSubscription.toJSON()
18 *   DELETE /api/my/notifications/push/subscribe  ← {endpoint}
19 */
20(function () {
21    'use strict';
22
23    var SW_PATH = '/static/sw_push.js';
24    var SW_SCOPE = '/static/';
25
26    // ── helpers ──────────────────────────────────────────────────────────────
27
28    function isSupported() {
29        return typeof navigator !== 'undefined'
30            && 'serviceWorker' in navigator
31            && typeof window !== 'undefined'
32            && 'PushManager' in window
33            && 'Notification' in window;
34    }
35
36    function urlBase64ToUint8Array(base64String) {
37        // VAPID public key (URL-safe base64) → Uint8Array для applicationServerKey.
38        var padding = '='.repeat((4 - base64String.length % 4) % 4);
39        var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
40        var rawData = window.atob(base64);
41        var arr = new Uint8Array(rawData.length);
42        for (var i = 0; i < rawData.length; ++i) arr[i] = rawData.charCodeAt(i);
43        return arr;
44    }
45
46    async function getVapidKey() {
47        try {
48            var r = await fetch('/api/auth/vapid-public-key', {
49                credentials: 'include',
50                headers: { 'Accept': 'application/json' },
51            });
52            if (!r.ok) return null;
53            var d = await r.json();
54            return (d && d.key) || null;
55        } catch (_) {
56            return null;
57        }
58    }
59
60    async function getRegistration() {
61        // Сначала пробуем по точному scope; fallback — getRegistrations() и
62        // ищем регистрацию для нашего sw_push.js (Safari иногда не отдаёт
63        // по getRegistration(scope)).
64        try {
65            var reg = await navigator.serviceWorker.getRegistration(SW_SCOPE);
66            if (reg) return reg;
67        } catch (_) {}
68        try {
69            var regs = await navigator.serviceWorker.getRegistrations();
70            for (var i = 0; i < regs.length; i++) {
71                var r = regs[i];
72                var sw = r.active || r.installing || r.waiting;
73                if (sw && sw.scriptURL && sw.scriptURL.indexOf('sw_push.js') !== -1) {
74                    return r;
75                }
76            }
77        } catch (_) {}
78        return null;
79    }
80
81    // ── public API ───────────────────────────────────────────────────────────
82
83    async function register() {
84        if (!isSupported()) return { ok: false, reason: 'unsupported' };
85        if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
86            return { ok: false, reason: 'insecure_context' };
87        }
88
89        var perm;
90        try {
91            perm = await Notification.requestPermission();
92        } catch (_) {
93            return { ok: false, reason: 'permission_error' };
94        }
95        if (perm !== 'granted') return { ok: false, reason: 'denied' };
96
97        var reg;
98        try {
99            reg = await navigator.serviceWorker.register(SW_PATH, { scope: SW_SCOPE });
100            await navigator.serviceWorker.ready;
101        } catch (e) {
102            return { ok: false, reason: 'sw_register_failed', error: e && e.message };
103        }
104
105        var vapidKey = await getVapidKey();
106        if (!vapidKey) return { ok: false, reason: 'no_server_key' };
107
108        var sub;
109        try {
110            sub = await reg.pushManager.getSubscription();
111            if (!sub) {
112                sub = await reg.pushManager.subscribe({
113                    userVisibleOnly: true,
114                    applicationServerKey: urlBase64ToUint8Array(vapidKey),
115                });
116            }
117        } catch (e) {
118            return { ok: false, reason: 'subscribe_failed', error: e && e.message };
119        }
120
121        try {
122            var r = await fetch('/api/my/notifications/push/subscribe', {
123                method: 'POST',
124                credentials: 'include',
125                headers: { 'Content-Type': 'application/json' },
126                body: JSON.stringify(sub.toJSON()),
127            });
128            if (!r.ok) return { ok: false, reason: 'server_error', status: r.status };
129        } catch (_) {
130            return { ok: false, reason: 'network_error' };
131        }
132
133        // Подтверждение — локальная notification (не через push-channel).
134        try {
135            await reg.showNotification('Уведомления подключены', {
136                body: 'Будем писать когда упадёт цена или появятся свежие жалобы.',
137                icon: '/static/favicon-192.png',
138                badge: '/static/favicon-96.png',
139                tag: 'pb-onboarding-confirm',
140            });
141        } catch (_) { /* не критично */ }
142
143        return { ok: true };
144    }
145
146    async function unregister() {
147        if (!isSupported()) return { ok: false, reason: 'unsupported' };
148
149        var reg = await getRegistration();
150        if (!reg) return { ok: true, reason: 'not_registered' };
151
152        var sub = null;
153        try { sub = await reg.pushManager.getSubscription(); } catch (_) {}
154        if (!sub) return { ok: true, reason: 'no_subscription' };
155
156        // Сначала сообщаем серверу (он удалит row в push_subscriptions).
157        try {
158            await fetch('/api/my/notifications/push/subscribe', {
159                method: 'DELETE',
160                credentials: 'include',
161                headers: { 'Content-Type': 'application/json' },
162                body: JSON.stringify({ endpoint: sub.endpoint }),
163            });
164        } catch (_) { /* всё равно отписываем локально */ }
165
166        try {
167            await sub.unsubscribe();
168        } catch (_) {
169            return { ok: false, reason: 'unsubscribe_failed' };
170        }
171        return { ok: true };
172    }
173
174    async function status() {
175        if (!isSupported()) {
176            return { permission: 'unsupported', subscribed: false, sw_ready: false, supported: false };
177        }
178        var perm = 'default';
179        try { perm = Notification.permission; } catch (_) {}
180
181        var reg = await getRegistration();
182        var sub = null;
183        if (reg) {
184            try { sub = await reg.pushManager.getSubscription(); } catch (_) {}
185        }
186        return {
187            permission: perm,
188            subscribed: !!sub,
189            sw_ready: !!reg,
190            supported: true,
191        };
192    }
193
194    window.pbNotifications = {
195        register: register,
196        unregister: unregister,
197        status: status,
198        // helpers — экспортируем для удобства/тестов.
199        _isSupported: isSupported,
200        _urlBase64ToUint8Array: urlBase64ToUint8Array,
201    };
202})();

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.