From 719998c17e83ed2811590cd5b383e2f489886f2b Mon Sep 17 00:00:00 2001 From: jeriox Date: Sat, 10 Jan 2026 23:30:34 +0100 Subject: [PATCH 1/5] prompt users for pwa installation and notifications --- ephios/locale/de/LC_MESSAGES/django.po | 128 ++++++++++++++++++------- ephios/static/ephios/js/main.js | 58 +++++++++++ ephios/templates/base.html | 80 ++++++++++++++++ 3 files changed, 229 insertions(+), 37 deletions(-) diff --git a/ephios/locale/de/LC_MESSAGES/django.po b/ephios/locale/de/LC_MESSAGES/django.po index af1bf4355..a19423050 100644 --- a/ephios/locale/de/LC_MESSAGES/django.po +++ b/ephios/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-11 23:54+0200\n" +"POT-Creation-Date: 2026-01-10 23:20+0100\n" "PO-Revision-Date: 2025-09-04 22:35+0200\n" "Last-Translator: Felix Rindt \n" "Language-Team: German rememberDismissed("pwaPrompt")); + + const pwaAppleOffcanvas = document.getElementById('pwaAppleOffcanvas'); + const pwaAppleBsOffcanvas = new bootstrap.Offcanvas('#pwaAppleOffcanvas') + pwaAppleOffcanvas.addEventListener('hide.bs.offcanvas', _ => rememberDismissed("pwaPrompt")); + + const notificationOffcanvas = document.getElementById("notificationOffcanvas"); + const notificationBsOffcanvas = new bootstrap.Offcanvas('#notificationOffcanvas'); + notificationOffcanvas.addEventListener("hide.bs.offcanvas", _ => rememberDismissed("notificationPrompt")) + + let deferredInstallPrompt; + + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferredInstallPrompt = e; + if (!getCookie("pwaPrompt") && !window.location.pathname.startsWith("/accounts")) { + pwaAndroidBsOffcanvas.show(); + } + }); + + window.addEventListener('appinstalled', () => { + pwaAndroidBsOffcanvas.hide(); + if (typeof isPushEnabled !== undefined && !isPushEnabled) { + notificationBsOffcanvas.show(); + document.getElementById("webpush-subscribe-button") + .addEventListener("click", _ => notificationBsOffcanvas.hide()) + } + deferredInstallPrompt = null; + }); + + const buttonInstall = document.getElementById("pwaInstall"); + buttonInstall.addEventListener('click', async () => { + deferredInstallPrompt.prompt(); + await deferredInstallPrompt.userChoice; + pwaAndroidBsOffcanvas.hide(); + deferredInstallPrompt = null; + }); + + if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !getCookie("pwaPrompt") && !window.location.pathname.startsWith("/accounts")) { + pwaAppleBsOffcanvas.show(); + } + + setTimeout(() => { + if ((window.matchMedia('(display-mode: standalone)').matches || navigator.standalone) && typeof isPushEnabled !== undefined && !isPushEnabled && !getCookie("notificationPrompt")) { + document.getElementById("webpush-subscribe-button") + .addEventListener("click", _ => notificationBsOffcanvas.hide()) + notificationBsOffcanvas.show(); + } + }, 2000); + }) +function rememberDismissed(key) { + const date = new Date(); + date.setDate(date.getDate() + 30); + document.cookie = key + "=hidden;expires=" + date.toUTCString(); +} + function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { diff --git a/ephios/templates/base.html b/ephios/templates/base.html index 912fca584..a1de65bd5 100644 --- a/ephios/templates/base.html +++ b/ephios/templates/base.html @@ -1,3 +1,4 @@ +{% load webpush_notifications %} {% load signal_extras %} {% load settings_extras %} {% load statici18n %} @@ -66,6 +67,7 @@ {% block javascript %}{% endblock %} {% endcompress %} + {% webpush_header %} {% collect_insert_html_signal "head" %} {% block html_head %} {% endblock %} @@ -187,6 +189,84 @@ +
+
+
{% translate "Install ephios app" %}
+ +
+
+

{% translate "You can install ephios on your device and use it like any other app!" %} {% translate "Simply click the button below to continue." %}

+ +
+
+
+
+
{% translate "Install ephios app" %}
+ +
+
+

{% translate "You can install ephios on your device and use it like any other app!" %} {%translate "Simply follow the steps below." %}

+
+
+
+
+
+ + + +
+
+
{% translate "Open in your main browser" %}
+
+
+
+
+ + + + + +
+
+
{% translate "Press More if no Share icon" %}
+
+
+
+
+ + + +
+
+
{% translate "Press Share in Navigation bar" %}
+
+
+
+
+ + + +
+
+
{% translate "Scroll down to "Add to Home Screen"" %}
+
+
+
+
+
+
+
{% translate "Activate notifications" %}
+ +
+
+

{% translate "ephios can send you push notifications so that you stay up to date. Do you want to activate them?" %}

+ + +
+
From e05e2d5c567e5b6f8d3c8c0c3c2efcb92edc2c58 Mon Sep 17 00:00:00 2001 From: jeriox Date: Sun, 11 Jan 2026 17:07:53 +0100 Subject: [PATCH 2/5] address review comments --- ephios/core/context.py | 1 + .../core/settings/settings_notifications.html | 5 ----- ephios/static/ephios/js/main.js | 20 ++++++++++++++++--- ephios/templates/base.html | 7 +++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/ephios/core/context.py b/ephios/core/context.py index 7182ed67f..090bb255e 100644 --- a/ephios/core/context.py +++ b/ephios/core/context.py @@ -42,6 +42,7 @@ def ephios_base_context(request): "ephios_version": settings.EPHIOS_VERSION, "PWA_APP_ICONS": get_pwa_app_icons(), "PWA_APP_SPLASH_SCREEN": [], + "VAPID_PUBLIC_KEY": settings.WEBPUSH_SETTINGS.get("VAPID_PUBLIC_KEY", ""), "DEBUG": settings.DEBUG, "organization_name": global_preferences_registry.manager()["general__organization_name"], "platform_name": dynamic_settings.PLATFORM_NAME, diff --git a/ephios/core/templates/core/settings/settings_notifications.html b/ephios/core/templates/core/settings/settings_notifications.html index de4ff3195..2b93a0706 100644 --- a/ephios/core/templates/core/settings/settings_notifications.html +++ b/ephios/core/templates/core/settings/settings_notifications.html @@ -2,11 +2,6 @@ {% load crispy_forms_filters %} {% load utils %} {% load i18n %} -{% load webpush_notifications %} - -{% block html_head %} - {% webpush_header %} -{% endblock %} {% block settings_content %}

{% translate "Notifications" %}

diff --git a/ephios/static/ephios/js/main.js b/ephios/static/ephios/js/main.js index f2b5a22a0..d3c93d90c 100644 --- a/ephios/static/ephios/js/main.js +++ b/ephios/static/ephios/js/main.js @@ -155,30 +155,39 @@ $(document).ready(function () { }); } + // Get reference to android (installable) prompt offcanvas const pwaAndroidOffcanvas = document.getElementById('pwaAndroidOffcanvas'); const pwaAndroidBsOffcanvas = new bootstrap.Offcanvas('#pwaAndroidOffcanvas') pwaAndroidOffcanvas.addEventListener('hide.bs.offcanvas', _ => rememberDismissed("pwaPrompt")); + // Get reference to apple (non-installable) prompt offcanvas const pwaAppleOffcanvas = document.getElementById('pwaAppleOffcanvas'); const pwaAppleBsOffcanvas = new bootstrap.Offcanvas('#pwaAppleOffcanvas') pwaAppleOffcanvas.addEventListener('hide.bs.offcanvas', _ => rememberDismissed("pwaPrompt")); + // Get reference to notification subscription prompt offcanvas const notificationOffcanvas = document.getElementById("notificationOffcanvas"); const notificationBsOffcanvas = new bootstrap.Offcanvas('#notificationOffcanvas'); notificationOffcanvas.addEventListener("hide.bs.offcanvas", _ => rememberDismissed("notificationPrompt")) let deferredInstallPrompt; + // The browser fires the beforeinstallprompt event when the PWA can be installed. + // We need to store the prompt to be able to show the install button later on. + // We show the prompt to the user if they did not decline in the last month and are logged in window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredInstallPrompt = e; - if (!getCookie("pwaPrompt") && !window.location.pathname.startsWith("/accounts")) { + if (!getCookie("pwaPrompt") && document.body.dataset.userIsAuthenticated === "True") { pwaAndroidBsOffcanvas.show(); } }); + // The browser fires the appinstalled event after the PWA has been installed. + // We use this opportunity to ask the user if they want to receive notifications window.addEventListener('appinstalled', () => { pwaAndroidBsOffcanvas.hide(); + // isPushEnabled is set by webpush.js from django-webpush if (typeof isPushEnabled !== undefined && !isPushEnabled) { notificationBsOffcanvas.show(); document.getElementById("webpush-subscribe-button") @@ -187,6 +196,8 @@ $(document).ready(function () { deferredInstallPrompt = null; }); + // When the user clicks on the install button in our PWA install prompt, we can used the saved prompt + // from the browser event to actually trigger the installation const buttonInstall = document.getElementById("pwaInstall"); buttonInstall.addEventListener('click', async () => { deferredInstallPrompt.prompt(); @@ -195,17 +206,20 @@ $(document).ready(function () { deferredInstallPrompt = null; }); - if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !getCookie("pwaPrompt") && !window.location.pathname.startsWith("/accounts")) { + // iOS does not support programmatic installation, so we show an offcanvas with instructions instead + if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !getCookie("pwaPrompt") && document.body.dataset.userIsAuthenticated === "True") { pwaAppleBsOffcanvas.show(); } setTimeout(() => { + // Only show this prompt inside of the PWA (with display-mode: standalone) for logged in user that did not decline in the last month + // isPushEnabled is set by webpush.js from django-webpush if ((window.matchMedia('(display-mode: standalone)').matches || navigator.standalone) && typeof isPushEnabled !== undefined && !isPushEnabled && !getCookie("notificationPrompt")) { document.getElementById("webpush-subscribe-button") .addEventListener("click", _ => notificationBsOffcanvas.hide()) notificationBsOffcanvas.show(); } - }, 2000); + }, 2000); // isPushEnabled is only set to true after a request to the backend, so it takes some time }) diff --git a/ephios/templates/base.html b/ephios/templates/base.html index a1de65bd5..5de10d404 100644 --- a/ephios/templates/base.html +++ b/ephios/templates/base.html @@ -15,6 +15,9 @@ + + + @@ -57,6 +60,7 @@ {% else %} {% endif %} + @@ -67,13 +71,12 @@ {% block javascript %}{% endblock %} {% endcompress %} - {% webpush_header %} {% collect_insert_html_signal "head" %} {% block html_head %} {% endblock %} - +