import { ref, computed } from 'vue'
import { getVapidKey, subscribePush, unsubscribePush } from '@/api'
/** URL-safe base64 → Uint8Array (the format PushManager expects for VAPID keys). */
export function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const raw = atob(base64)
const output = new Uint8Array(raw.length)
for (let i = 0; i < raw.length; i += 1) {
output[i] = raw.charCodeAt(i)
}
return output
}
// Feature checks are lazy functions, not module-load constants: test
// environments stub navigator/Notification after import time, and SSR-ish
// contexts may not have the globals at all.
function swSupported() {
return typeof navigator !== 'undefined' && 'serviceWorker' in navigator
}
function pmSupported() {
return typeof window !== 'undefined' && 'PushManager' in window
}
function notifSupported() {
return typeof Notification !== 'undefined'
}
/** Supported = SW + PushManager + Notification APIs all present. */
export const supported = computed(() => pmSupported() && notifSupported())
const permission = ref(notifSupported() ? Notification.permission : 'denied')
const subscribed = ref(false)
const busy = ref(false)
async function currentSubscription() {
if (!swSupported() || !pmSupported()) return null
const reg = await navigator.serviceWorker.getRegistration()
if (!reg) return null
return reg.pushManager.getSubscription()
}
async function syncState() {
if (!swSupported() || !pmSupported()) return
try {
const sub = await currentSubscription()
subscribed.value = !!sub
permission.value = notificationsSupported ? Notification.permission : 'denied'
} catch {
subscribed.value = false
}
}
/** Ask for permission, subscribe via the SW and register the endpoint server-side. */
async function enable() {
if (!supported.value || busy.value) return
busy.value = true
try {
permission.value = await Notification.requestPermission()
if (permission.value !== 'granted') return false
const { public_key: vapidKey } = await getVapidKey()
if (!vapidKey) return false
const reg = await navigator.serviceWorker.ready
let sub = await reg.pushManager.getSubscription()
if (!sub) {
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidKey),
})
}
const raw = sub.toJSON()
await subscribePush({
endpoint: raw.endpoint,
keys: raw.keys,
user_agent: navigator.userAgent,
})
subscribed.value = true
return true
} finally {
busy.value = false
}
}
/** Unsubscribe from the push service and unregister the endpoint server-side. */
async function disable() {
if (!swSupported() || !pmSupported() || busy.value) return
busy.value = true
try {
const sub = await currentSubscription()
if (sub) {
try {
await unsubscribePush(sub.endpoint)
} catch { /* server-side cleanup is best-effort */ }
await sub.unsubscribe()
}
subscribed.value = false
} finally {
busy.value = false
}
}
export function usePush() {
return { supported, permission, subscribed, busy, enable, disable, syncState }
}