/** * Messaging Service (FCM) * * Servicio para Firebase Cloud Messaging (Push Notifications). * Permite solicitar permisos, obtener tokens, escuchar mensajes y manejar * navegación (deep linking) cuando el usuario toca una notificación. */ import { Injector, NgZone } from '@angular/core'; import { Observable } from 'rxjs'; import { EnablePushResult, NotificationAction, NotificationClickEvent, NotificationPayload, NotificationPermission, ValtechFirebaseConfig } from './types'; import * as i0 from "@angular/core"; /** * Callback opcional de registro de device para `MessagingService.enable()`. * * El registro del device vive en `AuthService` (necesita el JWT + el endpoint * del backend). `MessagingService` NO puede depender de `AuthService` sin * crear un ciclo (AuthService → MessagingService). Por eso el caller le pasa * el paso de registro como callback. Recibe el token FCM y devuelve si el * device quedó registrado. */ export type RegisterDeviceFn = (token: string) => Promise; /** * Opciones de `MessagingService.enable()`. */ export interface EnablePushOptions { /** * Paso opcional de registro de device en el backend. Si se omite, `enable()` * resuelve con `status: 'enabled'` apenas obtiene el token (sin registrar). */ registerDevice?: RegisterDeviceFn; } /** * Estado interno del servicio de messaging */ interface MessagingState { token: string | null; permission: NotificationPermission; isSupported: boolean; } /** * Servicio para Firebase Cloud Messaging (FCM). * * Permite recibir notificaciones push en la aplicación web. * Requiere VAPID key configurada en ValtechFirebaseConfig. * * @example * ```typescript * @Component({...}) * export class NotificationComponent { * private messaging = inject(MessagingService); * * token = signal(null); * * async enableNotifications() { * // Solicitar permiso y obtener token * const token = await this.messaging.requestPermission(); * * if (token) { * this.token.set(token); * // Enviar token a tu backend para almacenarlo * await this.backend.registerDeviceToken(token); * } * } * * // Escuchar mensajes en foreground * messages$ = this.messaging.onMessage(); * } * ``` */ export declare class MessagingService { private injector; private config; private platformId; private ngZone; private messageSubject; private notificationClickSubject; private stateSubject; private unsubscribeOnMessage?; /** Flag para persistir mensajes FCM en localStorage (debugging) */ private readonly debugPersistence; /** Key para localStorage de mensajes FCM (debugging) */ private readonly DEBUG_STORAGE_KEY; /** * Key de localStorage donde se persiste el token FCM. * * El token vive en un `BehaviorSubject` en memoria y se pierde en cada * recarga / suspensión de la PWA (iOS reinicia PWAs con frecuencia). Persistir * el token permite hidratar el estado en cold start y que la UI no parpadee. * Es un *optimistic hint* — la verdad la confirma el siguiente `getToken()`. */ private readonly TOKEN_STORAGE_KEY; /** * Timeout (ms) para `navigator.serviceWorker.ready` dentro de `getToken()`. * * En un cold load el SW puede no estar activado aún y `serviceWorker.ready` * no resolver nunca. Pasado este tiempo, `getToken()` rechaza limpio en lugar * de colgarse indefinidamente. */ private readonly SW_READY_TIMEOUT_MS; /** * Timeout (ms) del watchdog de `enable()` antes de auto-recargar la página. * * El flujo de activación a veces se cuelga ANTES de `getToken()` — caso * típico: `Notification.requestPermission()` no muestra el popup del SO en un * cold load. El timeout de `getToken()` (SW_READY_TIMEOUT_MS) no cubre eso; * por eso `enable()` envuelve el flujo completo en este watchdog. * * Un flujo exitoso real llega a token+device en ~4s; 15s es holgado y no * atrapa un éxito lento. */ private readonly ENABLE_WATCHDOG_MS; /** * Key de sessionStorage — garantiza un solo auto-reload por sesión. Si el * flujo se cuelga una 2ª vez tras el reload, NO se recarga de nuevo (anti-loop). */ private readonly AUTORELOAD_FLAG; constructor(injector: Injector, config: ValtechFirebaseConfig, platformId: Object, ngZone: NgZone); /** * Obtiene la instancia de Messaging via Firebase SDK directo (NO AngularFire DI). * * **Por qué no usamos `injector.get(Messaging)`:** * AngularFire `provideMessaging` registra un APP_INITIALIZER que internamente * resuelve `await isSupported()`. Si `injector.get(Messaging)` se llama antes * que ese APP_INITIALIZER termine (ej. otros APP_INITIALIZER del cliente * construyen AuthService → MessagingService → toca Messaging temprano), * AngularFire lanza el error "APP_INITIALIZER ... has not resolved". Angular * DI **cachea errores de factory** — una vez que falló, todos los * `injector.get` siguientes retornan el mismo error cacheado. Retry loops * no funcionan. * * **Workaround:** bypass AngularFire entirely y usar Firebase SDK directo * (`firebase/messaging.getMessaging(getApp())`). AngularFire es un wrapper * sobre el mismo SDK — la instancia que obtenemos es funcionalmente idéntica. * `getApp()` retorna la default Firebase app inicializada por * `provideFirebaseApp(() => initializeApp(config.firebase))`. Si por alguna * razón no está inicializada aún, hacemos initializeApp idempotente. */ private fcmSupportPromise?; private getMessagingInstance; /** * Inicializa el servicio de messaging */ private initializeMessaging; /** * Configura listener para mensajes del Service Worker. * Recibe eventos cuando el usuario hace click en una notificación background. */ private setupServiceWorkerListener; /** * Verifica si FCM está soportado en el navegador actual */ private checkSupport; /** * Solicita permiso de notificaciones y obtiene el token FCM. * * @returns Token FCM si se otorgó permiso, null si se denegó * * @example * ```typescript * const token = await messaging.requestPermission(); * if (token) { * console.log('Token FCM:', token); * // Enviar a backend * } else { * console.log('Permiso denegado o no soportado'); * } * ``` */ requestPermission(): Promise; /** * Flujo completo de activación de push, robusto, cross-app. * * Orquesta: pedir permiso → SW ready → obtener token FCM → (opcional) * registrar el device en backend. Es el punto de orquestación único que cada * app del factory consume — la lógica de robustez vive aquí, no en cada página. * * **Watchdog de auto-reload.** El flujo a veces se cuelga ANTES de `getToken()` * (ej. `Notification.requestPermission()` que no muestra el popup en un cold * load) — el timeout interno de `getToken()` no cubre ese caso. Por eso * `enable()` envuelve el flujo entero en un watchdog: si no alcanza un estado * terminal en `ENABLE_WATCHDOG_MS` (15s): * - 1ª vez en la sesión → marca un flag en `sessionStorage` y hace * `window.location.reload()` (un fresh load suele tener el SW activo). * Resuelve con `{ status: 'timeout', reloaded: true }`. * - ya se auto-recargó antes → NO recarga (anti-loop): limpia el flag y * resuelve con `{ status: 'timeout', reloaded: false }` para que la app * muestre un error. * * NO hace throw — siempre resuelve con un `EnablePushResult` descriptivo que * la página consumidora inspecciona para decidir qué toast mostrar. * * @param options.registerDevice Callback opcional que registra el device en * el backend (vive en `AuthService` — se pasa como callback para evitar el * ciclo de DI AuthService ↔ MessagingService). * * @example * ```typescript * const result = await messaging.enable({ * registerDevice: (token) => auth.registerDevice(token).then(r => r.registered), * }); * if (result.status === 'enabled') { * // result.token disponible — push activo * } else if (result.status === 'timeout' && !result.reloaded) { * // mostrar error: el flujo se colgó y ya se consumió el auto-reload * } * ``` */ enable(options?: EnablePushOptions): Promise; /** * Flujo real de activación, sin el watchdog (lo envuelve `enable()`). * Resuelve siempre con un `EnablePushResult` — no hace throw. */ private runEnableFlow; /** True si ya se auto-recargó una vez en esta sesión. */ private hasAutoReloaded; /** Marca que se consumió el auto-reload de esta sesión. */ private markAutoReloaded; /** Limpia el flag anti-loop — tras un éxito o un fallo definitivo. */ private clearAutoReloadFlag; /** * Obtiene el token FCM actual (sin solicitar permiso). * * @returns Token FCM si está disponible, null si no * * @example * ```typescript * const token = await messaging.getToken(); * ``` */ getToken(): Promise; /** * Resuelve el `ServiceWorkerRegistration` del SW de FCM. * * El SW `/firebase-messaging-sw.js` ya se registra una vez en el bootstrap de * la app (`config.ts`). Reutilizamos ese registro en lugar de re-registrarlo * en cada `getToken()` — re-registrar repetidamente dispara revalidaciones del * SW en iOS PWA. Solo registramos como fallback si todavía no existe. */ private resolveServiceWorkerRegistration; /** * Espera a que el Service Worker quede activo (`navigator.serviceWorker.ready`) * pero con un timeout duro. * * `navigator.serviceWorker.ready` resuelve cuando hay un SW *activo*. En un * cold load (primera visita, SW recién registrado, iOS PWA recién abierta) el * SW puede quedar en estado `installing`/`waiting` y `ready` no resolver nunca * → `getToken()` se cuelga. El `Promise.race` contra un `setTimeout` garantiza * que esta espera termina: o gana `ready` (caso normal) o gana el timeout y * lanzamos un error claro para que `getToken()` rechace en vez de colgarse. * * El timer se limpia en ambas ramas para no dejar timers colgando. */ private waitForServiceWorkerReady; /** * Persiste el token FCM en localStorage (o lo limpia si es null/empty). */ private persistToken; /** * Hidrata el estado del token desde localStorage en el arranque del servicio. */ private hydrateTokenFromStorage; /** * Elimina el token FCM actual (unsubscribe de notificaciones). * * @example * ```typescript * await messaging.deleteToken(); * console.log('Token eliminado, no recibirá más notificaciones'); * ``` */ deleteToken(): Promise; /** * Observable de mensajes recibidos en foreground. * * IMPORTANTE: Los mensajes en background son manejados por el Service Worker. * * @returns Observable que emite cuando llega un mensaje en foreground * * @example * ```typescript * messaging.onMessage().subscribe(payload => { * console.log('Mensaje recibido:', payload); * // Mostrar notificación custom o actualizar UI * }); * ``` */ onMessage(): Observable; /** * Configura el listener de mensajes en foreground */ private setupMessageListener; /** * Persiste un mensaje FCM en localStorage para debugging. * Solo se usa cuando debugMessagePersistence está habilitado. * Mantiene los últimos 50 mensajes. */ private persistMessageForDebug; /** * Obtiene el estado actual del permiso de notificaciones. * * @returns 'granted' | 'denied' | 'default' * * @example * ```typescript * const permission = messaging.getPermissionState(); * if (permission === 'granted') { * // Ya tiene permiso * } else if (permission === 'default') { * // Puede solicitar permiso * } else { * // Denegado, debe habilitar manualmente * } * ``` */ getPermissionState(): NotificationPermission; /** * Verifica si FCM está soportado en el navegador actual. * Usa lógica inteligente para evitar falsos negativos por timing del Injector. * * @returns true si FCM está soportado * * @example * ```typescript * if (await messaging.isSupported()) { * // Puede usar notificaciones push * } else { * // Navegador no soporta o no tiene Service Worker * } * ``` */ isSupported(): Promise; /** * iOS sin estar instalado como PWA standalone — push web no disponible. * Aplica a Safari y a Edge/Chrome/Firefox en iOS (todos WebKit por mandato Apple). */ private isIOSWithoutStandalone; /** * Obtiene el token actual sin hacer request. * * @returns Token almacenado o null */ get currentToken(): string | null; /** * Observable del estado completo del servicio de messaging. */ get state$(): Observable; /** * Verifica si el usuario ya otorgó permiso de notificaciones. */ get hasPermission(): boolean; /** * Hidrata el estado del token desde un valor externo (ej: localStorage). * Útil cuando el token ya existe pero el MessagingService no lo tiene en memoria. * * @param token Token FCM a setear * * @example * ```typescript * const storedToken = localStorage.getItem('fcm_token'); * if (storedToken) { * messaging.setTokenFromStorage(storedToken); * } * ``` */ setTokenFromStorage(token: string): void; /** * Observable de clicks en notificaciones. * * Emite cuando el usuario hace click en una notificación (foreground o background). * Usa este observable para navegar a la página correspondiente. * * @returns Observable que emite NotificationClickEvent * * @example * ```typescript * @Component({...}) * export class AppComponent { * private messaging = inject(MessagingService); * private router = inject(Router); * * constructor() { * this.messaging.onNotificationClick().subscribe(event => { * if (event.action.route) { * this.router.navigate([event.action.route], { * queryParams: event.action.queryParams * }); * } * }); * } * } * ``` */ onNotificationClick(): Observable; /** * Extrae la acción de navegación de los datos de una notificación. * * Busca campos específicos en el payload de datos: * - `route`: Ruta interna de la app (ej: '/orders/123') * - `url`: URL externa (ej: 'https://example.com') * - `action_type`: Tipo de acción personalizada * - Campos con prefijo `action_`: Datos adicionales * * @param data - Datos del payload de la notificación * @returns Acción de navegación extraída * * @example * ```typescript * // Payload desde el backend: * // { route: '/orders/123', action_type: 'view_order', action_orderId: '123' } * * const action = messaging.extractActionFromData(notification.data); * // { route: '/orders/123', actionType: 'view_order', actionData: { orderId: '123' } } * ``` */ extractActionFromData(data?: Record): NotificationAction; /** * Emite manualmente un evento de click en notificación. * * Útil para manejar clicks en notificaciones foreground donde * la app decide mostrar un banner custom. * * @param notification - Payload de la notificación * * @example * ```typescript * messaging.onMessage().subscribe(notification => { * // Mostrar banner custom * this.showBanner(notification, () => { * // Usuario hizo click en el banner * messaging.handleNotificationClick(notification); * }); * }); * ``` */ handleNotificationClick(notification: NotificationPayload): void; /** * Verifica si una notificación tiene acción de navegación. * * @param data - Datos del payload * @returns true si tiene route o url */ hasNavigationAction(data?: Record): boolean; /** * Parsea un query string en un objeto. */ private parseQueryString; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵprov: i0.ɵɵInjectableDeclaration; } export {};