Newer
Older
navi-1 / webclient / src / composables / usePush.js
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 }
}