84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|