fix: harden PWA install prompt
This commit is contained in:
@@ -133,7 +133,7 @@ export function AdminShell() {
|
||||
<span className="text-sm font-extrabold">Admin</span>
|
||||
</div>
|
||||
<div className="ml-auto w-auto">
|
||||
<InstallAppPrompt />
|
||||
<InstallAppPrompt compact />
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
|
||||
@@ -166,7 +166,7 @@ export function AppShell() {
|
||||
<span className="text-sm font-extrabold">AgendaMax</span>
|
||||
</div>
|
||||
<div className="ml-auto w-auto">
|
||||
<InstallAppPrompt />
|
||||
<InstallAppPrompt compact />
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
|
||||
@@ -3,16 +3,26 @@ import { Download, PlusSquare, Share } from "lucide-react";
|
||||
import { Modal } from "./Modal";
|
||||
import { useInstallPrompt } from "../lib/useInstallPrompt";
|
||||
|
||||
export function InstallAppPrompt() {
|
||||
export function InstallAppPrompt({ compact = false }: { compact?: boolean }) {
|
||||
const { state, install } = useInstallPrompt();
|
||||
const [iosOpen, setIosOpen] = useState(false);
|
||||
const buttonClassName = compact
|
||||
? "btn btn-secondary h-9 w-9 shrink-0 justify-center p-0"
|
||||
: "btn btn-secondary w-full justify-start";
|
||||
|
||||
if (state === "unsupported" || state === "installed") return null;
|
||||
if (state === "ios-instructions") {
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary w-full justify-start" onClick={() => setIosOpen(true)}>
|
||||
<Download className="h-4 w-4" /> Instalar AgendaMax
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClassName}
|
||||
aria-label="Instalar AgendaMax"
|
||||
title={compact ? "Instalar AgendaMax" : undefined}
|
||||
onClick={() => setIosOpen(true)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
<span className={compact ? "sr-only" : undefined}>Instalar AgendaMax</span>
|
||||
</button>
|
||||
<Modal open={iosOpen} onClose={() => setIosOpen(false)} title="Instalar AgendaMax" subtitle="Safari lo añade a tu pantalla de inicio.">
|
||||
<div className="safe-area-bottom">
|
||||
@@ -25,5 +35,16 @@ export function InstallAppPrompt() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <button type="button" className="btn btn-secondary w-full justify-start" onClick={() => void install()}><Download className="h-4 w-4" /> Instalar AgendaMax</button>;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClassName}
|
||||
aria-label="Instalar AgendaMax"
|
||||
title={compact ? "Instalar AgendaMax" : undefined}
|
||||
onClick={() => void install()}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
<span className={compact ? "sr-only" : undefined}>Instalar AgendaMax</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export function Modal({ open, onClose, title, subtitle, children, size = "md", f
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Cerrar"
|
||||
className="rounded-lg p-1.5 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export type InstallPromptState = "unsupported" | "available" | "ios-instructions" | "installed";
|
||||
|
||||
@@ -26,6 +26,7 @@ export function useInstallPrompt() {
|
||||
});
|
||||
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const installInFlight = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isStandalone()) {
|
||||
@@ -40,6 +41,7 @@ export function useInstallPrompt() {
|
||||
};
|
||||
const onInstalled = () => {
|
||||
setDeferred(null);
|
||||
setDismissed(false);
|
||||
setState("installed");
|
||||
};
|
||||
|
||||
@@ -52,13 +54,21 @@ export function useInstallPrompt() {
|
||||
}, []);
|
||||
|
||||
const install = async () => {
|
||||
if (!deferred) return;
|
||||
if (!deferred || installInFlight.current) return;
|
||||
const event = deferred;
|
||||
installInFlight.current = true;
|
||||
setDeferred(null);
|
||||
await event.prompt();
|
||||
const choice = await event.userChoice;
|
||||
if (choice.outcome === "accepted") setState("installed");
|
||||
else setDismissed(true);
|
||||
try {
|
||||
await event.prompt();
|
||||
const choice = await event.userChoice;
|
||||
if (choice.outcome === "accepted") setState("installed");
|
||||
else setDismissed(true);
|
||||
} catch {
|
||||
setDeferred(event);
|
||||
setState("available");
|
||||
} finally {
|
||||
installInFlight.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
return { state: dismissed ? "unsupported" : state, install, dismiss: () => setDismissed(true) };
|
||||
|
||||
Reference in New Issue
Block a user