"use client" import { useCallback, useEffect, useState } from "react" import { Plus, Share, X } from "lucide-react" // ========================================================== // PwaInstallPrompt // ========================================================== // Bottom-sheet shown on mobile when the Monitor is opened in // a browser (not launched as an installed PWA). Two variants: // iOS Safari → manual 3-step instructions // Android → generic "browser menu → Add to Home Screen" // // No `beforeinstallprompt` handling: capturing the event to // drive a custom Install button interacts badly with Chrome's // own "Add to Home Screen" menu — Chromium degrades the manual // path to a plain shortcut when a page has intercepted the // event but hasn't yet called `prompt()`. Installation goes // through the browser's own menu entry and produces a real PWA. // // Never shown on desktop, or when already running standalone. // Dismissal options: // "Not now" → temporary, hidden for 30 days // "Don't show again" → permanent (no expiry) // Backdrop / X → session-only dismiss (reappears on // the next page load) // ========================================================== const DISMISSED_FOREVER_KEY = "proxmenux-install-dismissed" const DISMISSED_UNTIL_KEY = "proxmenux-install-dismissed-until" const NOT_NOW_DAYS = 30 function isMobileDevice(): boolean { if (typeof window === "undefined") return false // Prefer feature detection (coarse pointer + touch) over UA sniffing, // and fall back to UA for the corner case where a mobile browser // reports fine pointer under a desktop-mode toggle. const coarse = window.matchMedia("(pointer: coarse)").matches const ua = navigator.userAgent const uaMobile = /Android|iPhone|iPad|iPod|Mobile|Opera Mini|BlackBerry|IEMobile/i.test(ua) return coarse || uaMobile } function isStandalone(): boolean { if (typeof window === "undefined") return false const displayModeStandalone = window.matchMedia("(display-mode: standalone)").matches const iosStandalone = (window.navigator as Navigator & { standalone?: boolean }).standalone === true return displayModeStandalone || iosStandalone } function isIOS(): boolean { if (typeof window === "undefined") return false const ua = navigator.userAgent // iPadOS 13+ reports as MacIntel — detect that too when maxTouchPoints > 1. const iPadMasqueradingAsMac = ua.includes("Macintosh") && (navigator as Navigator & { maxTouchPoints?: number }).maxTouchPoints! > 1 return /iPhone|iPad|iPod/i.test(ua) || iPadMasqueradingAsMac } export function PwaInstallPrompt() { const [open, setOpen] = useState(false) const [platform, setPlatform] = useState<"ios" | "android" | null>(null) useEffect(() => { if (typeof window === "undefined") return if (!isMobileDevice() || isStandalone()) return try { if (localStorage.getItem(DISMISSED_FOREVER_KEY) === "1") return const untilRaw = localStorage.getItem(DISMISSED_UNTIL_KEY) if (untilRaw) { const until = Number.parseInt(untilRaw, 10) // Corrupt / non-numeric values fall through and the prompt shows, // which is the safe default. if (Number.isFinite(until) && until > Date.now()) return } } catch { // localStorage unavailable (private mode etc.) — treat as not dismissed. } setPlatform(isIOS() ? "ios" : "android") setOpen(true) }, []) const handleNotNow = useCallback(() => { try { const until = Date.now() + NOT_NOW_DAYS * 24 * 60 * 60 * 1000 localStorage.setItem(DISMISSED_UNTIL_KEY, String(until)) } catch { // Best-effort; if localStorage fails the user will see the prompt // again next visit, which is the safe default. } setOpen(false) }, []) const handleNeverAgain = useCallback(() => { try { localStorage.setItem(DISMISSED_FOREVER_KEY, "1") } catch { // Best-effort; if localStorage fails the user will see the prompt // again next visit, which is the safe default. } setOpen(false) }, []) const handleClose = useCallback(() => { // Session-only dismiss: closing via X or backdrop does NOT persist, // so the prompt reappears on the next page load. Users who want to // silence it for longer must use "Not now" (30 d) or "Don't show again". setOpen(false) }, []) if (!open || !platform) return null return (
{platform === "ios" ? "Add the Monitor to your home screen for quick access." : "Add the Monitor as an app to launch it like a native application."}