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.