const LS_KEY = 'shine-ui-webpush-subscription-v1'; const SW_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000; let swUpdateIntervalId = null; let controllerChangeHandled = false; function setupServiceWorkerAutoUpdate(registration, log) { if (!registration) return; if (!controllerChangeHandled && 'serviceWorker' in navigator) { controllerChangeHandled = true; navigator.serviceWorker.addEventListener('controllerchange', () => { log({ level: 'info', source: 'web-push', message: 'Service Worker обновился, перезагружаем UI', }); window.location.reload(); }); } registration.addEventListener('updatefound', () => { const installing = registration.installing; if (!installing) return; log({ level: 'info', source: 'web-push', message: 'Найдено обновление Service Worker', }); }); if (swUpdateIntervalId) { window.clearInterval(swUpdateIntervalId); swUpdateIntervalId = null; } const checkNow = () => { registration.update().catch(() => {}); }; checkNow(); swUpdateIntervalId = window.setInterval(checkNow, SW_UPDATE_CHECK_INTERVAL_MS); document.addEventListener('visibilitychange', () => { if (document.visibilityState !== 'visible') return; checkNow(); }); } function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } export async function initPwaPush({ authService, onLog = null }) { const log = (entry) => { if (typeof onLog === 'function') onLog(entry); }; if (!('serviceWorker' in navigator) || !('PushManager' in window)) { log({ level: 'warn', source: 'web-push', message: 'Web Push недоступен: нет serviceWorker или PushManager', }); return; } const vapidPublicKey = window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__ || ''; if (!vapidPublicKey) { log({ level: 'warn', source: 'web-push', message: 'Web Push отключен: не задан публичный VAPID ключ', }); return; } try { const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js', { updateViaCache: 'none', }); log({ level: 'info', source: 'web-push', message: 'Service Worker зарегистрирован', details: { scope: registration.scope }, }); setupServiceWorkerAutoUpdate(registration, log); const permission = await Notification.requestPermission(); if (permission !== 'granted') { log({ level: 'warn', source: 'web-push', message: `Разрешение на уведомления: ${permission}`, }); return; } log({ level: 'info', source: 'web-push', message: 'Разрешение на уведомления получено', }); let sub = await registration.pushManager.getSubscription(); let isNewSubscription = false; if (!sub) { sub = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), }); isNewSubscription = true; } log({ level: 'info', source: 'web-push', message: isNewSubscription ? 'Создана новая push-подписка' : 'Найдена существующая push-подписка', }); const serialized = JSON.stringify(sub); const prevSerialized = localStorage.getItem(LS_KEY); if (prevSerialized !== serialized) { localStorage.setItem(LS_KEY, serialized); log({ level: 'info', source: 'web-push', message: 'Push-подписка изменилась относительно прошлого запуска', }); } else { log({ level: 'info', source: 'web-push', message: 'Push-подписка не изменилась, но в MVP включена принудительная отправка на сервер', }); } const json = sub.toJSON(); const endpoint = json.endpoint || ''; const p256dhKey = json.keys?.p256dh || ''; const authKey = json.keys?.auth || ''; if (!endpoint || !p256dhKey || !authKey) { log({ level: 'warn', source: 'web-push', message: 'Подписка неполная: endpoint/p256dh/auth отсутствуют', }); return; } log({ level: 'info', source: 'web-push', message: 'Push-токен получен, отправка на сервер', details: { endpoint }, }); await authService.upsertPushToken({ endpoint, p256dhKey, authKey, platform: 'web', userAgent: navigator.userAgent || '', }); log({ level: 'info', source: 'web-push', message: 'Push-подписка успешно отправлена на сервер', }); } catch (error) { log({ level: 'error', source: 'web-push', message: 'Ошибка инициализации Web Push', details: error?.message || 'unknown', }); } }