From b922c5640909b26f40048378ce6176fff7a8cbf1 Mon Sep 17 00:00:00 2001 From: benchav Date: Sun, 24 May 2026 10:55:33 -0600 Subject: [PATCH 01/11] feat: add PWA installation prompt banner with cooldown logic via new hook --- src/components/pwa/PWAInstallBanner.tsx | 158 ++++++++++++++++++++++++ src/hooks/usePWAInstall.ts | 122 ++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 src/components/pwa/PWAInstallBanner.tsx create mode 100644 src/hooks/usePWAInstall.ts diff --git a/src/components/pwa/PWAInstallBanner.tsx b/src/components/pwa/PWAInstallBanner.tsx new file mode 100644 index 0000000..835ecb7 --- /dev/null +++ b/src/components/pwa/PWAInstallBanner.tsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from 'react'; +import { usePWAInstall } from '../../hooks/usePWAInstall'; + +/** + * PWAInstallBanner + * + * Banner premium de instalación de la PWA. + * Se muestra como un banner flotante en la parte inferior de la pantalla, + * con animación de entrada/salida, glassmorphism, y lógica inteligente + * para no ser invasivo (cooldown de 7 días tras descartar). + * + * Compatible con desktop (Chrome, Edge) y móvil (Android Chrome, Samsung Internet). + * En iOS Safari no se soporta `beforeinstallprompt` — el banner no se mostrará. + */ +export function PWAInstallBanner() { + const { canInstall, isInstalled, promptInstall, dismiss } = usePWAInstall(); + const [visible, setVisible] = useState(false); + const [exiting, setExiting] = useState(false); + + // Mostrar con un delay de 2 segundos para no interrumpir la carga inicial + useEffect(() => { + if (!canInstall || isInstalled) { + setVisible(false); + return; + } + + const timer = setTimeout(() => setVisible(true), 2000); + return () => clearTimeout(timer); + }, [canInstall, isInstalled]); + + const handleDismiss = () => { + setExiting(true); + setTimeout(() => { + setVisible(false); + setExiting(false); + dismiss(); + }, 300); + }; + + const handleInstall = async () => { + await promptInstall(); + setExiting(true); + setTimeout(() => { + setVisible(false); + setExiting(false); + }, 300); + }; + + if (!visible) return null; + + return ( + <> + {/* Backdrop sutil — solo en móvil para dar enfoque */} +