import { Injectable } from '@angular/core'; import { PushDevice, PushDeviceSubscription, } from '@task-stream/web/tracker/domain'; /** The service worker that shows the notifications (apps/web/public). */ const WORKER = '/push-sw.js'; /** Push through this browser's service worker and PushManager. */ @Injectable() export class BrowserPushDevice extends PushDevice { get supported(): boolean { return ( typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ); } async current(): Promise { if (!this.supported) return null; const registration = await navigator.serviceWorker.getRegistration(WORKER); const subscription = await registration?.pushManager.getSubscription(); return subscription ? toDevice(subscription) : null; } async subscribe(publicKey: string): Promise { if (!this.supported) throw new Error('Este navegador no admite avisos'); if ((await Notification.requestPermission()) !== 'granted') throw new Error( 'El navegador no tiene permiso para mostrar avisos: dáselo en los ajustes del sitio.', ); const registration = await navigator.serviceWorker.register(WORKER); await navigator.serviceWorker.ready; try { const subscription = (await registration.pushManager.getSubscription()) ?? (await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: fromBase64Url(publicKey), })); return toDevice(subscription); } catch { // The browser's own message is in English and says little. throw new Error( 'Este navegador no ha dejado activar los avisos. En iPhone, añade antes TaskStream a la pantalla de inicio.', ); } } async unsubscribe(): Promise { if (!this.supported) return null; const registration = await navigator.serviceWorker.getRegistration(WORKER); const subscription = await registration?.pushManager.getSubscription(); if (!subscription) return null; await subscription.unsubscribe(); return subscription.endpoint; } } function toDevice(subscription: PushSubscription): PushDeviceSubscription { const json = subscription.toJSON(); return { endpoint: subscription.endpoint, p256dh: json.keys?.['p256dh'] ?? '', auth: json.keys?.['auth'] ?? '', }; } /** VAPID keys travel as base64url; PushManager wants the bytes. */ function fromBase64Url(text: string): Uint8Array { const base64 = (text + '='.repeat((4 - (text.length % 4)) % 4)) .replace(/-/g, '+') .replace(/_/g, '/'); const raw = atob(base64); const bytes = new Uint8Array(new ArrayBuffer(raw.length)); for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i); return bytes; }