// push-client.js — Web Push subscription flow dla Norda Biznes // // Rejestruje subskrypcję pod /push/subscribe, obsługuje dzwonek w navbarze, // wykrywa iOS PWA, obsługuje diagnostykę ?pushdiag=1, pending-url dla iOS. (function() { 'use strict'; function log(msg, data) { try { console.log('[push] ' + msg, data !== undefined ? data : ''); } catch (e) {} } function detectIOS() { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; } function isStandalone() { return (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || window.navigator.standalone === true; } function supportsPush() { return ('serviceWorker' in navigator) && ('PushManager' in window) && ('Notification' in window); } function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = atob(base64); const output = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; i++) output[i] = rawData.charCodeAt(i); return output; } function getBell() { return document.getElementById('pushBellBtn'); } function setBellState(state) { const bell = getBell(); if (!bell) return; bell.dataset.state = state; const slash = bell.querySelector('.push-disabled-slash'); if (state === 'enabled') { bell.title = 'Powiadomienia włączone — kliknij, żeby wyłączyć'; if (slash) slash.style.display = 'none'; bell.style.opacity = '1'; } else if (state === 'disabled') { bell.title = 'Włącz powiadomienia'; if (slash) slash.style.display = ''; bell.style.opacity = '0.55'; } else if (state === 'blocked') { bell.title = 'Powiadomienia zablokowane w przeglądarce. Zmień w ustawieniach strony.'; if (slash) slash.style.display = ''; bell.style.opacity = '0.4'; } else if (state === 'unsupported') { bell.title = 'Przeglądarka nie obsługuje powiadomień'; bell.style.opacity = '0.3'; } } async function getSubscription() { const reg = await navigator.serviceWorker.ready; return reg.pushManager.getSubscription(); } async function enablePush() { if (detectIOS() && !isStandalone()) { alert('Na iPhone powiadomienia działają tylko po dodaniu strony do ekranu początkowego:\n\n1) Dotknij Udostępnij (strzałka na dole)\n2) Wybierz „Do ekranu początkowego"\n3) Uruchom aplikację z ikonki na pulpicie\n4) Zaloguj się i kliknij ponownie dzwoneczek'); return; } if (!supportsPush()) { alert('Twoja przeglądarka nie obsługuje powiadomień.'); setBellState('unsupported'); return; } const perm = await Notification.requestPermission(); if (perm !== 'granted') { setBellState(perm === 'denied' ? 'blocked' : 'disabled'); return; } try { const keyResp = await fetch('/push/vapid-public-key', { credentials: 'include' }); if (!keyResp.ok) throw new Error('vapid-public-key HTTP ' + keyResp.status); const { key } = await keyResp.json(); const reg = await navigator.serviceWorker.ready; let sub = await reg.pushManager.getSubscription(); if (!sub) { sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(key), }); } const subObj = sub.toJSON(); const resp = await fetch('/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subObj.endpoint, keys: subObj.keys, }), credentials: 'include', }); if (!resp.ok) throw new Error('subscribe HTTP ' + resp.status); setBellState('enabled'); // Welcome push — test że wszystko działa await fetch('/push/test', { method: 'POST', credentials: 'include' }); } catch (e) { log('enable error', e); alert('Nie udało się włączyć powiadomień: ' + (e.message || e)); setBellState('disabled'); } } async function disablePush() { try { const sub = await getSubscription(); if (sub) { const endpoint = sub.endpoint; await sub.unsubscribe(); await fetch('/push/unsubscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint }), credentials: 'include', }); } setBellState('disabled'); } catch (e) { log('disable error', e); } } window.togglePush = async function(event) { if (event) event.preventDefault(); const bell = getBell(); if (!bell) return; const state = bell.dataset.state || 'disabled'; if (state === 'enabled') { await disablePush(); } else { await enablePush(); } }; async function initPushState() { const bell = getBell(); if (!bell) return; if (!supportsPush()) { // iOS Safari bez PWA — przeglądarka w ogóle bez PushManager. // Dzwonek nadal klikalny, zobaczy komunikat o PWA. if (detectIOS() && !isStandalone()) { setBellState('disabled'); return; } setBellState('unsupported'); return; } if (Notification.permission === 'denied') { setBellState('blocked'); return; } try { const sub = await getSubscription(); setBellState(sub && Notification.permission === 'granted' ? 'enabled' : 'disabled'); } catch (e) { setBellState('disabled'); } } async function checkPendingUrl() { // iOS PWA: jeśli otworzono PWA przez klik w powiadomienie, SW mógł zapisać URL if (!isStandalone()) return; try { const resp = await fetch('/push/pending-url', { credentials: 'include' }); if (!resp.ok) return; const { url } = await resp.json(); if (url && url !== window.location.pathname + window.location.search) { window.location.href = url; } } catch (e) { /* ignore */ } } document.addEventListener('DOMContentLoaded', function() { initPushState(); checkPendingUrl(); if (/[?&]pushdiag=1/.test(window.location.search)) { log('diagnostics', { supportsPush: supportsPush(), iOS: detectIOS(), standalone: isStandalone(), permission: (window.Notification && Notification.permission) || 'n/a', userAgent: navigator.userAgent, }); } }); })();