// Web Push subscription registration. Browser-only; Tauri WebView2 / WebKit // do not install service workers. The Tauri build relies on native OS // notifications via osNotify.ts instead. // // Flow: // 1. Register /sw.js if not already. // 2. Subscribe with the VAPID public key (Vite injects it via env). // 3. Persist {endpoint, keys} JSON-encoded into push_tokens.token for the // current device. Server-side fan-out reads this row to send pushes. import { isTauriRuntime } from './globalShortcut'; import { supabase } from './supabase'; const VAPID_PUBLIC_KEY: string | undefined = (import.meta as unknown as { env?: { VITE_VAPID_PUBLIC_KEY?: string } }).env ?.VITE_VAPID_PUBLIC_KEY; export async function registerWebPush(deviceId: string): Promise { // Tauri uses native notifications — no service worker. if (isTauriRuntime()) return; if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return; if (!('PushManager' in window)) return; if (!VAPID_PUBLIC_KEY) { console.warn('VITE_VAPID_PUBLIC_KEY not set — push disabled'); return; } try { const reg = await navigator.serviceWorker.register('/sw.js'); await navigator.serviceWorker.ready; let sub = await reg.pushManager.getSubscription(); if (!sub) { const permission = await Notification.requestPermission(); if (permission !== 'granted') return; sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) as BufferSource, }); } const token = JSON.stringify({ endpoint: sub.endpoint, keys: sub.toJSON().keys ?? {}, }); const { error } = await supabase .from('push_tokens') .upsert( { device_id: deviceId, platform: 'web' as never, token }, { onConflict: 'device_id' }, ); if (error) { console.warn('push_tokens upsert failed', error); } } catch (err: unknown) { console.error('registerWebPush failed', err); } } export async function unregisterWebPush(deviceId: string): Promise { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return; try { const reg = await navigator.serviceWorker.getRegistration(); if (reg) { const sub = await reg.pushManager.getSubscription(); if (sub) await sub.unsubscribe(); } } catch { /* ignore */ } try { await supabase.from('push_tokens').delete().eq('device_id', deviceId); } catch (err: unknown) { console.warn('push_tokens delete failed', err); } } // VAPID public key arrives as URL-safe base64; PushManager expects a raw byte // array. Standard conversion. function urlBase64ToUint8Array(base64: string): Uint8Array { const padding = '='.repeat((4 - (base64.length % 4)) % 4); const padded = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(padded); const out = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); return out; }