thermograph/frontend/static/push-client.js

94 lines
3.3 KiB
JavaScript
Raw Permalink Normal View History

// Web Push enablement for the current device. The service worker itself is
// registered globally by nav.js; this module owns the *subscription* handshake:
// ask permission, subscribe with the server's VAPID key, and register the
// resulting endpoint against the signed-in user (via account.js's cookie-aware
// apiFetch). Used by the /alerts page to render a "Notifications on this device"
// control.
//
// Push + service workers require a secure context (HTTPS, or http://localhost),
// so on a plain-HTTP LAN origin this cleanly reports "unsupported" instead of
// throwing.
import { apiFetch, uv } from "./account.js";
export function supported() {
return (
typeof window !== "undefined" &&
window.isSecureContext &&
"serviceWorker" in navigator &&
"PushManager" in window &&
"Notification" in window
);
}
// The applicationServerKey must be a Uint8Array of the base64url-decoded VAPID
// public key.
function urlB64ToUint8Array(base64) {
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const b64 = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
const raw = atob(b64);
const out = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
return out;
}
async function registration() {
// nav.js registered sw.js already; ready resolves once it's active.
return navigator.serviceWorker.ready;
}
// Is this device currently subscribed? (a PushSubscription exists locally)
export async function isEnabled() {
if (!supported()) return false;
try {
const reg = await registration();
return !!(await reg.pushManager.getSubscription());
} catch (e) {
return false;
}
}
export function permission() {
return supported() ? Notification.permission : "unsupported";
}
// Subscribe this device and register it server-side. Throws on failure so the
// caller can surface a message.
export async function enable() {
if (!supported()) throw new Error("This browser doesn't support push notifications.");
const perm = await Notification.requestPermission();
if (perm !== "granted") throw new Error("Notification permission was not granted.");
const reg = await registration();
let sub = await reg.pushManager.getSubscription();
if (!sub) {
const res = await apiFetch(uv("push/vapid-key"));
if (!res.ok) throw new Error("Couldn't fetch the server key.");
const { key } = await res.json();
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(key),
});
}
const res = await apiFetch(uv("push/subscribe"), { method: "POST", json: sub.toJSON() });
if (!res.ok) throw new Error("Couldn't register this device.");
}
// Unsubscribe locally and remove the endpoint server-side.
export async function disable() {
if (!supported()) return;
const reg = await registration();
const sub = await reg.pushManager.getSubscription();
if (!sub) return;
try {
await apiFetch(uv("push/subscribe"), { method: "DELETE", json: { endpoint: sub.endpoint } });
} catch (e) { /* best-effort server cleanup */ }
await sub.unsubscribe();
}
// Ask the server to push a test notification to every device on the account.
export async function sendTest() {
const res = await apiFetch(uv("push/test"), { method: "POST" });
if (!res.ok) throw new Error("Couldn't send a test notification.");
return res.json();
}