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