nexus/ui/src/components/InstallPromptBanner.tsx

74 lines
2.3 KiB
TypeScript

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useInstallPrompt } from "../hooks/useInstallPrompt";
const DISMISS_KEY = "nexus.installPromptDismissed";
const DISMISS_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
function isDismissed(): boolean {
try {
const stored = localStorage.getItem(DISMISS_KEY);
if (!stored) return false;
const dismissedAt = parseInt(stored, 10);
return Date.now() - dismissedAt < DISMISS_COOLDOWN_MS;
} catch {
return false;
}
}
/**
* PWA install prompt banner.
*
* Shows when:
* - The browser has fired beforeinstallprompt (canInstall) OR the device is iOS
* - The app is not already installed (running in standalone mode)
* - The user has not dismissed within the last 7 days
*
* Positioned above MobileBottomNav on mobile and top-right on desktop.
*/
export function InstallPromptBanner() {
const { canInstall, promptInstall, isIOS } = useInstallPrompt();
const [dismissed, setDismissed] = useState(() => isDismissed());
if (dismissed) return null;
if (!canInstall && !isIOS) return null;
const handleInstall = async () => {
if (isIOS) return; // iOS: user follows Share menu instructions
await promptInstall();
};
const handleDismiss = () => {
try {
localStorage.setItem(DISMISS_KEY, String(Date.now()));
} catch {
// localStorage unavailable — dismiss for session only
}
setDismissed(true);
};
return (
<div
className="fixed bottom-16 left-4 right-4 z-50 md:bottom-auto md:top-4 md:left-auto md:right-4 md:max-w-sm bg-card border border-border rounded-lg p-4"
role="banner"
aria-label="Install Nexus app"
>
<p className="text-sm font-semibold mb-1">Add Nexus to your home screen</p>
<p className="text-xs text-muted-foreground mb-3">
{isIOS
? "Open the Share menu and tap 'Add to Home Screen'"
: "Get the full experience — launch instantly, works offline."}
</p>
<div className="flex items-center gap-2">
{!isIOS && (
<Button size="sm" onClick={handleInstall}>
Add to Home Screen
</Button>
)}
<Button variant="ghost" size="sm" onClick={handleDismiss}>
Not now
</Button>
</div>
</div>
);
}