web: the offer to install is made once, and the answer is kept
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
// whenever it likes, and the event has to be held until a click spends it — so this
|
||||
// is a module-level slot plus a subscriber list, and no store, hook or context.
|
||||
//
|
||||
// The one piece of state the visitor owns is the refusal: remember it, and the offer
|
||||
// never comes back. A dismissal is an answer, not a snooze.
|
||||
// The state the visitor owns is the answer: a refusal, or an install. Remember either
|
||||
// one, and the offer never comes back. A no is an answer, not a snooze.
|
||||
|
||||
type InstallEvent = Event & {
|
||||
prompt: () => Promise<void>;
|
||||
@@ -11,6 +11,9 @@ type InstallEvent = Event & {
|
||||
};
|
||||
|
||||
const DISMISSED = 'rc.install.dismissed.v1';
|
||||
// The other answer: installed, and never asked again — including from a plain browser
|
||||
// tab, which has no `standalone` to tell it so.
|
||||
const INSTALLED = 'rc.install.installed.v1';
|
||||
|
||||
// The window event a control elsewhere in the app (the Library's own button, say)
|
||||
// fires to pull the offer forward instead of waiting out the delay.
|
||||
@@ -33,11 +36,15 @@ export function subscribe(listener: () => void): () => void {
|
||||
}
|
||||
|
||||
export function canInstall(): boolean {
|
||||
return deferred !== null && !dismissed();
|
||||
return deferred !== null && !dismissed() && !isInstalled();
|
||||
}
|
||||
|
||||
// Spends the browser's prompt and reports the answer. Null means there was nothing
|
||||
// to spend: the event never arrived, or this is a second call.
|
||||
//
|
||||
// Either answer is remembered, because either answer is the visitor's: yes is the app
|
||||
// being installed, and the browser's own sheet being waved away is a no — the event is
|
||||
// spent now, and it never fires again for this install.
|
||||
export async function promptInstall(): Promise<'accepted' | 'dismissed' | null> {
|
||||
const event = deferred;
|
||||
if (!event) return null;
|
||||
@@ -47,6 +54,8 @@ export async function promptInstall(): Promise<'accepted' | 'dismissed' | null>
|
||||
notify();
|
||||
await event.prompt();
|
||||
const { outcome } = await event.userChoice;
|
||||
if (outcome === 'accepted') markInstalled();
|
||||
else dismissInstall();
|
||||
return outcome;
|
||||
}
|
||||
|
||||
@@ -76,6 +85,18 @@ export function dismissInstall(): void {
|
||||
notify();
|
||||
}
|
||||
|
||||
// True in the installed window itself, and also in any browser tab that watched the
|
||||
// install happen — that tab keeps running the site, and must not ask for the app it
|
||||
// just installed.
|
||||
export function isInstalled(): boolean {
|
||||
return isStandalone() || localStorage.getItem(INSTALLED) === '1';
|
||||
}
|
||||
|
||||
export function markInstalled(): void {
|
||||
localStorage.setItem(INSTALLED, '1');
|
||||
notify();
|
||||
}
|
||||
|
||||
// Asks for the offer now.
|
||||
export function offerInstall(): void {
|
||||
window.dispatchEvent(new Event(OFFER_EVENT));
|
||||
@@ -93,5 +114,5 @@ window.addEventListener('beforeinstallprompt', (event) => {
|
||||
// and is the only signal that the offer is done.
|
||||
window.addEventListener('appinstalled', () => {
|
||||
deferred = null;
|
||||
notify();
|
||||
markInstalled();
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
canInstall,
|
||||
dismissInstall,
|
||||
dismissed,
|
||||
isInstalled,
|
||||
isIosSafari,
|
||||
isStandalone,
|
||||
promptInstall,
|
||||
subscribe,
|
||||
} from '../pwa/install';
|
||||
@@ -30,7 +30,7 @@ export function InstallPrompt() {
|
||||
useEffect(() => {
|
||||
// An installed app has nothing to offer, and a refusal is final — the visitor
|
||||
// said no to this dialog, not to this session.
|
||||
if (isStandalone() || dismissed()) return;
|
||||
if (isInstalled() || dismissed()) return;
|
||||
|
||||
const manual = isIosSafari();
|
||||
let waited = false;
|
||||
@@ -86,8 +86,8 @@ export function InstallPrompt() {
|
||||
type="button"
|
||||
className="btn primary"
|
||||
data-key="install-yes"
|
||||
// Not a dismissal: taking the offer and waving the browser's own sheet
|
||||
// away are different answers, and the app's own button can still ask.
|
||||
// `promptInstall` records the browser's answer itself: accepted means the
|
||||
// app is installed, waved away means the offer was spent and refused.
|
||||
onClick={() => {
|
||||
void promptInstall();
|
||||
setFace(null);
|
||||
|
||||
Reference in New Issue
Block a user