diff --git a/blueprints/public/routes.py b/blueprints/public/routes.py index 6121da5..f0048fb 100644 --- a/blueprints/public/routes.py +++ b/blueprints/public/routes.py @@ -1564,6 +1564,15 @@ def pwa_install(): return render_template('pwa_install.html') +@bp.route('/sw.js') +def service_worker(): + """Service worker served from root scope for PWA installability.""" + return current_app.send_static_file('sw.js'), 200, { + 'Content-Type': 'application/javascript', + 'Service-Worker-Allowed': '/' + } + + @bp.route('/robots.txt') def robots_txt(): """Robots.txt for search engine crawlers.""" diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000..088d2b4 --- /dev/null +++ b/static/sw.js @@ -0,0 +1,14 @@ +// Norda Biznes Partner — minimal service worker for PWA installability +// No offline caching — just enough for Chrome to offer install prompt + +self.addEventListener('install', function() { + self.skipWaiting(); +}); + +self.addEventListener('activate', function(event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener('fetch', function(event) { + event.respondWith(fetch(event.request)); +}); diff --git a/templates/base.html b/templates/base.html index a3fc479..589bf61 100755 --- a/templates/base.html +++ b/templates/base.html @@ -2254,6 +2254,49 @@ setInterval(updateNotificationBadgeFromAPI, 60000); {% endif %} + // Register Service Worker for PWA installability + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(function() {}); + } + + // PWA install prompt — capture beforeinstallprompt for Android + var deferredInstallPrompt = null; + + window.addEventListener('beforeinstallprompt', function(e) { + e.preventDefault(); + deferredInstallPrompt = e; + + // On Android we can install directly — change banner button behavior + var actionBtn = document.querySelector('.pwa-smart-banner-action'); + if (actionBtn) { + actionBtn.href = '#'; + actionBtn.addEventListener('click', function(ev) { + ev.preventDefault(); + triggerPwaInstall(); + }); + } + }); + + window.addEventListener('appinstalled', function() { + deferredInstallPrompt = null; + dismissPwaBanner(); + }); + + function triggerPwaInstall() { + if (deferredInstallPrompt) { + deferredInstallPrompt.prompt(); + deferredInstallPrompt.userChoice.then(function(result) { + if (result.outcome === 'accepted') { + dismissPwaBanner(); + } + deferredInstallPrompt = null; + }); + } else { + // Fallback — go to instructions page (iOS or prompt unavailable) + window.location.href = '{{ url_for("public.pwa_install") }}'; + } + } + // PWA Smart Banner logic (function() { var banner = document.getElementById('pwaSmartBanner'); diff --git a/templates/pwa_install.html b/templates/pwa_install.html index b436b51..1926368 100644 --- a/templates/pwa_install.html +++ b/templates/pwa_install.html @@ -318,6 +318,39 @@ flex-shrink: 0; } + /* Direct install button for Android */ + .pwa-direct-install { + text-align: center; + margin-bottom: var(--spacing-xl); + padding: var(--spacing-xl); + background: var(--surface); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-md); + border: 2px solid var(--primary); + } + + .pwa-direct-install-btn { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + background: var(--primary); + color: white; + border: none; + border-radius: 28px; + padding: 16px 32px; + font-size: var(--font-size-lg); + font-weight: 700; + font-family: var(--font-family); + cursor: pointer; + animation: pulseGlow 2s ease-in-out infinite; + } + + .pwa-direct-install-hint { + color: var(--text-secondary); + font-size: var(--font-size-sm); + margin-top: var(--spacing-sm); + } + /* Mobile-only: show steps, hide desktop msg */ @media (max-width: 768px) { .pwa-desktop-msg { display: none !important; } @@ -435,6 +468,17 @@