Session 27: PWA autopilot — NetworkFirst cache policy, stale bucket cleanup, offline fallback, push helper, manifest polish, tier fix (1584 tests)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Push notification subscription (Session 27).
|
||||
*
|
||||
* Client-side helpers to request permission and (un)subscribe to Web Push
|
||||
* via the active service worker. The SW already handles incoming `push`
|
||||
* and `notificationclick` events (see web/src/sw.ts); these helpers manage
|
||||
* the browser→push-service subscription handshake.
|
||||
*
|
||||
* VAPID: subscription requires NEXT_PUBLIC_VAPID_PUBLIC_KEY. Until that
|
||||
* key is generated and set (a future session), subscribeToPush returns
|
||||
* null rather than throwing — callers degrade gracefully.
|
||||
*
|
||||
* The resulting PushSubscription is meant to be POSTed to the backend and
|
||||
* stored (Supabase) so the notification trigger system can target it.
|
||||
*/
|
||||
|
||||
/** Browser support check — both SW and PushManager must exist. */
|
||||
export function isPushSupported(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
'serviceWorker' in navigator &&
|
||||
'PushManager' in window &&
|
||||
'Notification' in window
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* VAPID public keys are base64url; the subscribe API wants a BufferSource.
|
||||
* We return the backing ArrayBuffer so the type is unambiguously
|
||||
* ArrayBuffer (not ArrayBufferLike) for `applicationServerKey`.
|
||||
*/
|
||||
function vapidKeyToBuffer(base64String: string): ArrayBuffer {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
const raw = window.atob(base64);
|
||||
const buffer = new ArrayBuffer(raw.length);
|
||||
const view = new Uint8Array(buffer);
|
||||
for (let i = 0; i < raw.length; i += 1) view[i] = raw.charCodeAt(i);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request permission and subscribe. Returns the PushSubscription on
|
||||
* success, or null when unsupported / denied / VAPID key missing.
|
||||
*/
|
||||
export async function subscribeToPush(): Promise<PushSubscription | null> {
|
||||
if (!isPushSupported()) return null;
|
||||
|
||||
const vapidKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
|
||||
if (!vapidKey) {
|
||||
console.warn('[push] NEXT_PUBLIC_VAPID_PUBLIC_KEY not set — cannot subscribe yet.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== 'granted') return null;
|
||||
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
|
||||
// Reuse an existing subscription if the browser already has one.
|
||||
const existing = await registration.pushManager.getSubscription();
|
||||
if (existing) return existing;
|
||||
|
||||
return registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: vapidKeyToBuffer(vapidKey),
|
||||
});
|
||||
}
|
||||
|
||||
/** Unsubscribe the active push subscription, if any. Idempotent. */
|
||||
export async function unsubscribeFromPush(): Promise<boolean> {
|
||||
if (!isPushSupported()) return true;
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
if (!subscription) return true;
|
||||
return subscription.unsubscribe();
|
||||
}
|
||||
|
||||
/** Current permission state without prompting — for UI affordances. */
|
||||
export function pushPermission(): NotificationPermission | 'unsupported' {
|
||||
if (!isPushSupported()) return 'unsupported';
|
||||
return Notification.permission;
|
||||
}
|
||||
Reference in New Issue
Block a user