From 5ac24009dd0474f085d6d00325e4881c22140d2c Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Mon, 23 Feb 2026 11:57:00 +0100 Subject: [PATCH] feat: add service worker and native PWA install prompt for Android Adds minimal service worker for PWA installability. On Android Chrome, the smart banner and install page now trigger the native install dialog directly instead of showing manual instructions. iOS still shows step-by-step guide (Apple provides no install API). Co-Authored-By: Claude Opus 4.6 --- blueprints/public/routes.py | 9 +++++++ static/sw.js | 14 ++++++++++ templates/base.html | 43 ++++++++++++++++++++++++++++++ templates/pwa_install.html | 53 +++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 static/sw.js 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 @@
+ + + + +
1
@@ -526,4 +570,13 @@ panel.classList.toggle('active', panel.id === 'pwa-' + platform); }); } + + // Show direct install button if beforeinstallprompt available (Android) + // deferredInstallPrompt and triggerPwaInstall are defined in base.html + window.addEventListener('beforeinstallprompt', function() { + var directBtn = document.getElementById('pwaDirectInstall'); + var manualLabel = document.getElementById('pwaManualLabel'); + if (directBtn) directBtn.style.display = 'block'; + if (manualLabel) manualLabel.style.display = 'block'; + }); {% endblock %}