Spaces:
Running
Running
| /** | |
| * InstallPrompt.tsx — X3: PWA install banner | |
| * Appare dopo 45s di uso cumulativo. Android: "Installa" button. | |
| * iOS Safari: istruzioni "Condividi → Aggiungi a schermata Home". | |
| * Dismissable permanente via localStorage. | |
| */ | |
| import { useState, useEffect, useRef } from "react"; | |
| import { Z_INDEX } from "@/lib/zindex"; | |
| import { getCapabilities } from "@/lib/capabilities"; | |
| const DISMISSED_KEY = "pwa_install_dismissed_v1"; | |
| const USAGE_KEY = "pwa_usage_seconds"; | |
| export default function InstallPrompt() { | |
| const [show, setShow] = useState(false); | |
| const [isIos, setIsIos] = useState(false); | |
| const deferredRef = useRef<any>(null); | |
| useEffect(() => { | |
| try { | |
| const caps = getCapabilities(); | |
| if (caps.isStandalone) return; | |
| if (localStorage.getItem(DISMISSED_KEY)) return; | |
| setIsIos(!!(caps.isIos && caps.isSafari)); | |
| const handler = (e: Event) => { | |
| e.preventDefault(); | |
| deferredRef.current = e; | |
| }; | |
| window.addEventListener("beforeinstallprompt", handler); | |
| const prev = parseInt(localStorage.getItem(USAGE_KEY) ?? "0", 10); | |
| const start = Date.now(); | |
| const tick = setInterval(() => { | |
| const elapsed = Math.floor((Date.now() - start) / 1000) + prev; | |
| localStorage.setItem(USAGE_KEY, String(elapsed)); | |
| if (elapsed >= 45) { | |
| setShow(true); | |
| clearInterval(tick); | |
| } | |
| }, 5000); | |
| return () => { | |
| window.removeEventListener("beforeinstallprompt", handler); | |
| clearInterval(tick); | |
| }; | |
| } catch { /* non-blocking */ } | |
| return undefined; | |
| }, []); | |
| if (!show) return null; | |
| const dismiss = () => { | |
| setShow(false); | |
| try { localStorage.setItem(DISMISSED_KEY, "1"); } catch { /* */ } | |
| }; | |
| const install = async () => { | |
| if (deferredRef.current) { | |
| try { | |
| await deferredRef.current.prompt(); | |
| const { outcome } = await deferredRef.current.userChoice; | |
| if (outcome === "accepted") dismiss(); | |
| } catch { /* user dismissed */ } | |
| } | |
| }; | |
| return ( | |
| <div style={{ | |
| position: "fixed", bottom: 72, left: 12, right: 12, | |
| background: "rgba(8,8,22,0.96)", | |
| border: "1px solid rgba(99,102,241,0.30)", | |
| borderRadius: 14, padding: "14px 16px", | |
| zIndex: Z_INDEX.CRITICAL_TOAST, boxShadow: "0 8px 32px rgba(0,0,0,0.6)", | |
| display: "flex", flexDirection: "column", gap: 10, | |
| }}> | |
| <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}> | |
| <div> | |
| <div style={{ fontSize: "0.85rem", fontWeight: 700, color: "#e2e8f0" }}> | |
| Installa l'app | |
| </div> | |
| <div style={{ fontSize: "0.72rem", color: "rgba(148,163,184,0.7)", marginTop: 3 }}> | |
| {isIos | |
| ? 'Tocca Condividi → "Aggiungi a schermata Home"' | |
| : "Accesso rapido, funziona offline"} | |
| </div> | |
| </div> | |
| <button onClick={dismiss} style={{ | |
| all: "unset", cursor: "pointer", | |
| color: "rgba(148,163,184,0.4)", fontSize: "1.2rem", padding: "0 4px", | |
| lineHeight: 1, | |
| }}>×</button> | |
| </div> | |
| {!isIos && ( | |
| <button onClick={install} style={{ | |
| all: "unset", cursor: "pointer", textAlign: "center", | |
| background: "rgba(99,102,241,0.2)", border: "1px solid rgba(99,102,241,0.4)", | |
| color: "#a5b4fc", borderRadius: 8, padding: "8px 0", | |
| fontSize: "0.78rem", fontWeight: 600, | |
| }}> | |
| Installa | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| } | |