feat: add service worker and native PWA install prompt for Android
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

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 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-23 11:57:00 +01:00
parent e8d04c83d9
commit 5ac24009dd
4 changed files with 119 additions and 0 deletions

View File

@ -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."""

14
static/sw.js Normal file
View File

@ -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));
});

View File

@ -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');

View File

@ -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 @@
<!-- Android steps -->
<div class="pwa-steps" id="pwa-android">
<!-- Direct install button (shown only when beforeinstallprompt is available) -->
<div class="pwa-direct-install" id="pwaDirectInstall" style="display: none;">
<button class="pwa-direct-install-btn" onclick="triggerPwaInstall()">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Zainstaluj teraz
</button>
<p class="pwa-direct-install-hint">Kliknij i potwierdź — to wszystko!</p>
</div>
<p class="pwa-manual-fallback-label" id="pwaManualLabel" style="display: none; text-align: center; color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-md);">Jeśli przycisk nie działa, wykonaj poniższe kroki:</p>
<div class="pwa-step">
<div class="pwa-step-number">1</div>
<div class="pwa-step-content">
@ -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 %}