Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Odróżnia push od portalowych powiadomień w navbarze. Po pierwszym udanym włączeniu pokazuje komunikat że subskrypcja działa tylko na tym urządzeniu i zachęca do kliknięcia również na innych telefonach i komputerach. Tooltipy zaktualizowane pod kątem "na tym urządzeniu". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
218 lines
8.7 KiB
JavaScript
218 lines
8.7 KiB
JavaScript
// 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 waves = bell.querySelectorAll('.push-wave');
|
|
if (state === 'enabled') {
|
|
bell.title = 'Powiadomienia włączone na tym urządzeniu — kliknij, żeby wyłączyć';
|
|
bell.style.opacity = '1';
|
|
waves.forEach(function(w) { w.style.opacity = ''; });
|
|
} else if (state === 'disabled') {
|
|
bell.title = 'Kliknij, aby włączyć powiadomienia na tym urządzeniu';
|
|
bell.style.opacity = '0.55';
|
|
waves.forEach(function(w, i) { w.style.opacity = i === 0 ? '0.5' : '0.35'; });
|
|
} else if (state === 'blocked') {
|
|
bell.title = 'Powiadomienia zablokowane w przeglądarce. Zmień w ustawieniach strony.';
|
|
bell.style.opacity = '0.4';
|
|
waves.forEach(function(w) { w.style.opacity = '0.2'; });
|
|
} else if (state === 'unsupported') {
|
|
bell.title = 'Ta przeglądarka nie obsługuje powiadomień';
|
|
bell.style.opacity = '0.3';
|
|
}
|
|
}
|
|
|
|
function showToast(text, durationMs) {
|
|
try {
|
|
const old = document.getElementById('pushInfoToast');
|
|
if (old) old.remove();
|
|
const toast = document.createElement('div');
|
|
toast.id = 'pushInfoToast';
|
|
toast.textContent = text;
|
|
toast.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);' +
|
|
'background:#233e6d;color:#fff;padding:14px 20px;border-radius:10px;' +
|
|
'box-shadow:0 6px 24px rgba(0,0,0,0.25);z-index:9999;max-width:420px;' +
|
|
'font-size:14px;line-height:1.45;text-align:center;font-family:inherit;';
|
|
document.body.appendChild(toast);
|
|
setTimeout(function() {
|
|
toast.style.transition = 'opacity 0.4s';
|
|
toast.style.opacity = '0';
|
|
setTimeout(function() { toast.remove(); }, 450);
|
|
}, durationMs || 7000);
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
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' });
|
|
// Edukacyjny komunikat: subskrypcja jest per urządzenie/przeglądarka
|
|
showToast('✓ Powiadomienia włączone na tym urządzeniu. Na innych telefonach i komputerach kliknij ten sam przycisk, żeby włączyć je także tam.', 9000);
|
|
} 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,
|
|
});
|
|
}
|
|
});
|
|
})();
|