feat: add PWA support with push notifications, service worker, and offline page
Implement progressive web app capabilities including VAPID-based push notification infrastructure, service worker with offline caching, PWA install prompt handling, and manifest configuration. Add push.py module for VAPID key generation and notification delivery, PushManager and PwaInstaller JavaScript classes, service worker with precache and push event handling, offline fallback page, and app icons at multiple resolutions.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
export class PushManager {
|
||||
constructor() {
|
||||
this.supported = "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
|
||||
if (!this.supported) {
|
||||
return;
|
||||
}
|
||||
this.triggers = Array.from(document.querySelectorAll("[data-push-enable]"));
|
||||
this.bindTriggers();
|
||||
this.refreshTriggerVisibility();
|
||||
this.register(true).catch((error) => console.error("Push silent register failed:", error));
|
||||
}
|
||||
|
||||
bindTriggers() {
|
||||
this.triggers.forEach((trigger) => {
|
||||
trigger.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
this.optIn();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refreshTriggerVisibility() {
|
||||
const granted = Notification.permission === "granted";
|
||||
this.triggers.forEach((trigger) => {
|
||||
trigger.hidden = granted;
|
||||
});
|
||||
}
|
||||
|
||||
async optIn() {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission === "granted") {
|
||||
await this.register(false);
|
||||
}
|
||||
}
|
||||
|
||||
async register(silent) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register("/service-worker.js");
|
||||
await registration.update();
|
||||
await navigator.serviceWorker.ready;
|
||||
|
||||
if (Notification.permission !== "granted") {
|
||||
this.refreshTriggerVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
const keyResponse = await fetch("/push.json");
|
||||
const keyData = await keyResponse.json();
|
||||
const applicationServerKey = Uint8Array.from(atob(keyData.publicKey), (c) => c.charCodeAt(0));
|
||||
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerKey,
|
||||
});
|
||||
|
||||
const response = await fetch("/push.json", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(subscription.toJSON()),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Bad status code from server.");
|
||||
}
|
||||
|
||||
this.refreshTriggerVisibility();
|
||||
} catch (error) {
|
||||
console.error("Error registering push notifications:", error);
|
||||
if (!silent) {
|
||||
alert("Enabling push notifications failed. Please check your browser settings and try again.\n\n" + error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
export class PwaInstaller {
|
||||
constructor() {
|
||||
this.deferredPrompt = null;
|
||||
this.triggers = Array.from(document.querySelectorAll("[data-pwa-install]"));
|
||||
if (!this.triggers.length) {
|
||||
return;
|
||||
}
|
||||
this.bindTriggers();
|
||||
window.addEventListener("beforeinstallprompt", (event) => this.onPromptAvailable(event));
|
||||
window.addEventListener("appinstalled", () => this.hideTriggers());
|
||||
}
|
||||
|
||||
bindTriggers() {
|
||||
this.triggers.forEach((trigger) => {
|
||||
trigger.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
this.install();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onPromptAvailable(event) {
|
||||
event.preventDefault();
|
||||
this.deferredPrompt = event;
|
||||
this.showTriggers();
|
||||
}
|
||||
|
||||
showTriggers() {
|
||||
this.triggers.forEach((trigger) => {
|
||||
trigger.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
hideTriggers() {
|
||||
this.triggers.forEach((trigger) => {
|
||||
trigger.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
async install() {
|
||||
if (!this.deferredPrompt) {
|
||||
return;
|
||||
}
|
||||
this.deferredPrompt.prompt();
|
||||
await this.deferredPrompt.userChoice;
|
||||
this.deferredPrompt = null;
|
||||
this.hideTriggers();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user