Files
nis2-agile/public/js/pwa.js
T
DevEnv nis2-agileandClaude Opus 4.8 36513fa722 [FEAT] PWA installabile Android/iOS conforme AGID (v1.15.0)
- manifest.webmanifest + service worker (offline shell, /api network-only, asset stale-while-revalidate)
- set icone (192/512 + maskable + apple-touch + favicon), generate zero-deps (pure Node+zlib)
- tag PWA + viewport viewport-fit=cover (zoom mantenuto, WCAG 1.4.4) in 77 pagine HTML
- AGID/WCAG 2.1 AA: touch target >=44px, orientamento/zoom liberi, safe-area standalone
- generatori riproducibili: scripts/gen-pwa-icons.mjs + scripts/inject-pwa-head.mjs
- bump version 1.14.1 -> 1.15.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:09:54 +02:00

131 lines
5.1 KiB
JavaScript

/*
* NIS2 Agile — PWA bootstrap
* Registra il service worker e gestisce l'UI di installazione (Android + iOS).
* Vanilla JS, nessuna dipendenza. Localizzato IT/EN (chiave i18n: 'nis2_lang').
*/
(function () {
'use strict';
// --- i18n minimale, allineato a i18n.js (localStorage 'nis2_lang') ---
function lang() {
try { return (localStorage.getItem('nis2_lang') || 'it').slice(0, 2); }
catch (e) { return 'it'; }
}
var STR = {
it: {
install: 'Installa app',
banner: 'Installa NIS2 Agile sul dispositivo per un accesso più rapido.',
ios: 'Per installare: tocca Condividi e poi "Aggiungi a Home".',
close: 'Chiudi'
},
en: {
install: 'Install app',
banner: 'Install NIS2 Agile on your device for faster access.',
ios: 'To install: tap Share, then "Add to Home Screen".',
close: 'Close'
}
};
function t(k) { var l = lang(); return (STR[l] && STR[l][k]) || STR.it[k]; }
function isStandalone() {
return (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|| window.navigator.standalone === true;
}
// --- 1. Registrazione service worker ---
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.catch(function (err) { console.warn('[pwa] SW registration failed', err); });
});
}
// Gia' installata (standalone): niente banner di installazione.
if (isStandalone()) return;
// --- UI banner sobrio, indipendente dal CSS dell'app ---
function makeBanner(message, withInstallBtn) {
var bar = document.createElement('div');
bar.setAttribute('role', 'region');
bar.setAttribute('aria-label', t('install'));
bar.style.cssText = 'position:fixed;left:12px;right:12px;bottom:12px;z-index:9999;'
+ 'background:#0066CC;color:#fff;border-radius:10px;padding:12px 14px;'
+ 'box-shadow:0 6px 24px rgba(0,0,0,.25);display:flex;align-items:center;gap:10px;'
+ 'font-family:system-ui,Arial,sans-serif;font-size:.95rem;max-width:560px;margin:0 auto;';
var txt = document.createElement('span');
txt.style.cssText = 'flex:1;line-height:1.35;';
txt.textContent = message;
bar.appendChild(txt);
var btnInstall = null;
if (withInstallBtn) {
btnInstall = document.createElement('button');
btnInstall.type = 'button';
btnInstall.textContent = t('install');
btnInstall.style.cssText = 'background:#fff;color:#0066CC;border:0;border-radius:8px;'
+ 'padding:10px 16px;font-weight:600;min-height:44px;cursor:pointer;white-space:nowrap;';
bar.appendChild(btnInstall);
}
var btnClose = document.createElement('button');
btnClose.type = 'button';
btnClose.setAttribute('aria-label', t('close'));
btnClose.textContent = '✕';
btnClose.style.cssText = 'background:transparent;color:#fff;border:0;font-size:1.1rem;'
+ 'line-height:1;min-width:44px;min-height:44px;cursor:pointer;';
bar.appendChild(btnClose);
document.body.appendChild(bar);
return { bar: bar, btnInstall: btnInstall, btnClose: btnClose };
}
// --- 2. Android / Chrome: beforeinstallprompt ---
var deferred = null;
var DISMISS_KEY = 'pwa-install-dismissed';
function dismissed() { try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch (e) { return false; } }
function setDismissed() { try { localStorage.setItem(DISMISS_KEY, '1'); } catch (e) {} }
window.addEventListener('beforeinstallprompt', function (e) {
e.preventDefault();
deferred = e;
if (dismissed()) return;
try {
var ui = makeBanner(t('banner'), true);
ui.btnInstall.addEventListener('click', function () {
ui.bar.remove();
if (!deferred) return;
deferred.prompt();
if (deferred.userChoice && deferred.userChoice.finally) {
deferred.userChoice.finally(function () { deferred = null; });
} else { deferred = null; }
});
ui.btnClose.addEventListener('click', function () { ui.bar.remove(); setDismissed(); });
} catch (err) { console.warn('[pwa] banner error', err); }
});
window.addEventListener('appinstalled', function () {
deferred = null;
setDismissed();
});
// --- 3. iOS Safari: nessun beforeinstallprompt -> hint manuale (una volta) ---
try {
var ua = navigator.userAgent || '';
var isIOS = /iphone|ipad|ipod/i.test(ua)
|| (/Macintosh/.test(ua) && 'ontouchend' in document); // iPad iPadOS desktop-mode
var isSafari = /safari/i.test(ua) && !/crios|fxios|edgios|android/i.test(ua);
var IOS_KEY = 'pwa-ios-hint-dismissed';
var iosDismissed = (function () { try { return localStorage.getItem(IOS_KEY) === '1'; } catch (e) { return false; } })();
if (isIOS && isSafari && !iosDismissed) {
window.addEventListener('load', function () {
var ui = makeBanner(t('ios'), false);
ui.btnClose.addEventListener('click', function () {
ui.bar.remove();
try { localStorage.setItem(IOS_KEY, '1'); } catch (e) {}
});
});
}
} catch (err) { console.warn('[pwa] ios hint error', err); }
})();