import { ApplicationRef, computed, DestroyRef, inject, Injectable, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { SwUpdate, VersionReadyEvent } from '@angular/service-worker'; import { concat, filter, first, interval } from 'rxjs'; interface BeforeInstallPromptEvent extends Event { prompt(): Promise; userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; } @Injectable({ providedIn: 'root', }) export class PwaService { private appRef = inject(ApplicationRef); private swUpdate = inject(SwUpdate); private destroyRef = inject(DestroyRef); private deferredPrompt = signal(null); public canInstall = signal(false); public promptVisible = signal(false); public isIOS = signal(false); public isStandalone = signal(false); public showIOSPrompt = signal(false); public showInstallButton = computed( () => this.canInstall() || (this.isIOS() && !this.isStandalone()) ); public updateAvailable = signal(false); private readonly UPDATE_CHECK_INTERVAL = 30_000; private readonly RELOAD_DELAY = 3_000; private onUpdateReadyCallback: ((message: string) => void) | null = null; private reloadScheduled = false; constructor() { this.detectPlatform(); this.initializePwaPrompt(); this.initializeUpdateHandling(); } private initializePwaPrompt(): void { window.addEventListener('beforeinstallprompt', (event: Event) => { event.preventDefault(); const deferredEvent = event as BeforeInstallPromptEvent; this.deferredPrompt.set(deferredEvent); this.canInstall.set(true); this.promptVisible.set(true); }); window.addEventListener('appinstalled', () => { this.deferredPrompt.set(null); this.canInstall.set(false); this.promptVisible.set(false); }); } public async showInstallPrompt(): Promise { const prompt = this.deferredPrompt(); if (!prompt) { return; } await prompt.prompt(); const { outcome } = await prompt.userChoice; if (outcome === 'accepted') { this.deferredPrompt.set(null); this.canInstall.set(false); this.promptVisible.set(false); } } public dismissPrompt(): void { this.promptVisible.set(false); } public triggerInstall(): void { if (this.isIOS()) { this.showIOSPrompt.set(true); } else { this.showInstallPrompt(); } } private detectPlatform(): void { const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); this.isIOS.set(isIOSDevice); const isInStandaloneMode = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as Navigator & { standalone?: boolean }).standalone === true; this.isStandalone.set(isInStandaloneMode); if (isIOSDevice && !isInStandaloneMode && !this.wasIOSPromptDismissed()) { setTimeout(() => this.showIOSPrompt.set(true), 3000); } } private wasIOSPromptDismissed(): boolean { return localStorage.getItem('ios-pwa-prompt-dismissed') === 'true'; } public dismissIOSPrompt(permanent = false): void { this.showIOSPrompt.set(false); if (permanent) { localStorage.setItem('ios-pwa-prompt-dismissed', 'true'); } } public setOnUpdateReadyCallback(callback: (message: string) => void): void { this.onUpdateReadyCallback = callback; } private initializeUpdateHandling(): void { if (!this.swUpdate.isEnabled) { return; } this.swUpdate.versionUpdates .pipe( filter((event): event is VersionReadyEvent => event.type === 'VERSION_READY'), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { this.updateAvailable.set(true); this.notifyAndReload('Actualizando a la version mas reciente...'); }); this.swUpdate.unrecoverable.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this.notifyAndReload('Ocurrio un error con la aplicacion. Se recargara para actualizar.'); }); this.startPeriodicUpdateChecks(); } private startPeriodicUpdateChecks(): void { const appIsStable$ = this.appRef.isStable.pipe(first((isStable) => isStable)); concat(appIsStable$, interval(this.UPDATE_CHECK_INTERVAL)) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.checkForUpdate(); }); } public async checkForUpdate(): Promise { if (!this.swUpdate.isEnabled) { return false; } try { return await this.swUpdate.checkForUpdate(); } catch (error) { console.error('Error checking for update:', error); return false; } } private notifyAndReload(message: string): void { if (this.reloadScheduled) { return; } this.reloadScheduled = true; if (this.onUpdateReadyCallback) { this.onUpdateReadyCallback(message); } setTimeout(() => { window.location.reload(); }, this.RELOAD_DELAY); } }