From 093903162e34a49aad309000735f2ea10885f067 Mon Sep 17 00:00:00 2001 From: Mannu Date: Fri, 29 May 2026 01:00:52 +0530 Subject: [PATCH] =?UTF-8?q?improve:=20smarter=20install=20prompt=20?= =?UTF-8?q?=E2=80=94=20visit=20gate=20+=20snooze=20instead=20of=20permanen?= =?UTF-8?q?t=20dismiss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old behavior: dismiss once -> never shown again forever. New behavior: - Only shows after 3+ sessions (user has seen value first) - "Later" -> snoozes for 7 days, re-asks after that - "No thanks" -> snoozes for 30 days - Once actually installed (Android accepted) -> permanently hidden - iOS: two buttons (Later / No thanks) instead of bare X, so intent is clear - Android: tapping X now snoozes rather than permanently suppressing - Dark mode support added to both banners Co-Authored-By: Claude Sonnet 4.6 --- src/components/InstallPrompt.tsx | 144 +++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 44 deletions(-) diff --git a/src/components/InstallPrompt.tsx b/src/components/InstallPrompt.tsx index fc2217b..9de5f83 100644 --- a/src/components/InstallPrompt.tsx +++ b/src/components/InstallPrompt.tsx @@ -8,98 +8,158 @@ interface BeforeInstallPromptEvent extends Event { readonly userChoice: Promise<{ outcome: "accepted" | "dismissed" }>; } -function IOSInstallInstructions({ onDismiss }: { onDismiss: () => void }) { +// ─── Storage keys ──────────────────────────────────────────────────────────── +const KEY_SNOOZED_UNTIL = "tia_install_snoozed_until"; // timestamp ms — re-ask after this +const KEY_VISIT_COUNT = "tia_install_visit_count"; // int — increment on each page load +const KEY_INSTALLED = "tia_installed"; // "1" once the user installs + +// How many sessions before we first ask (0-indexed — show on the 3rd session) +const MIN_VISITS = 3; +// How long to wait before re-asking after a "Later" tap (7 days) +const SNOOZE_MS = 7 * 24 * 60 * 60 * 1000; +// After a hard "No thanks" (second dismiss) wait 30 days +const LONG_SNOOZE_MS = 30 * 24 * 60 * 60 * 1000; + +function isSnoozed(): boolean { + const until = localStorage.getItem(KEY_SNOOZED_UNTIL); + return !!until && Date.now() < Number(until); +} + +function snooze(hard = false) { + localStorage.setItem(KEY_SNOOZED_UNTIL, String(Date.now() + (hard ? LONG_SNOOZE_MS : SNOOZE_MS))); +} + +function incrementVisit(): number { + const current = Number(localStorage.getItem(KEY_VISIT_COUNT) || "0"); + const next = current + 1; + localStorage.setItem(KEY_VISIT_COUNT, String(next)); + return next; +} + +// ─── iOS instructions banner ───────────────────────────────────────────────── +function IOSInstallInstructions({ + onLater, onNo, +}: { onLater: () => void; onNo: () => void }) { return ( -
+
🌸 - Install Tia +
+

Add Tia to Home Screen

+

Works like a native app — loads instantly

+
-
-

- Add Tia to your home screen for the best experience. -

-
+
Tap - ⬆️ - then - "Add to Home Screen" + ⬆️ + then choose + "Add to Home Screen" +
+
+ +
); } -const DISMISSED_KEY = "tia_install_prompt_dismissed"; - +// ─── Main component ─────────────────────────────────────────────────────────── export function InstallPrompt() { const [deferred, setDeferred] = useState(null); - const [isIOS, setIsIOS] = useState(false); - const [dismissed, setDismissed] = useState(true); // start hidden to avoid flash + const [isIOS, setIsIOS] = useState(false); + const [show, setShow] = useState(false); // start hidden to avoid flash useEffect(() => { if (typeof window === "undefined") return; - const alreadyDismissed = localStorage.getItem(DISMISSED_KEY) === "1"; - if (alreadyDismissed) return; + // Already installed as PWA — never show + if (window.matchMedia("(display-mode: standalone)").matches) return; + if (localStorage.getItem(KEY_INSTALLED) === "1") return; - const standalone = window.matchMedia("(display-mode: standalone)").matches; - if (standalone) return; // already installed + // Track visit count; don't ask until the user has had a few sessions + const visits = incrementVisit(); + if (visits < MIN_VISITS) return; - // iOS Safari: no beforeinstallprompt, needs manual instructions - const ios = /iphone|ipad|ipod/i.test(navigator.userAgent); + // Currently snoozed from a previous "Later" tap + if (isSnoozed()) return; + + // iOS Safari — no native install event, we show manual instructions + const ios = /iphone|ipad|ipod/i.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); if (ios && isSafari) { setIsIOS(true); - setDismissed(false); + setShow(true); } - // Android / Chrome: capture the deferred install event + // Android / Chrome — capture the browser's deferred install prompt const handler = (e: Event) => { e.preventDefault(); setDeferred(e as BeforeInstallPromptEvent); - setDismissed(false); + setShow(true); }; window.addEventListener("beforeinstallprompt", handler); return () => window.removeEventListener("beforeinstallprompt", handler); }, []); - const handleDismiss = () => { - setDismissed(true); - localStorage.setItem(DISMISSED_KEY, "1"); + // "Remind me later" — snooze for 7 days + const handleLater = () => { + setShow(false); + snooze(false); }; + // "No thanks" — snooze for 30 days + const handleNo = () => { + setShow(false); + snooze(true); + }; + + // Android: user tapped Install const handleInstall = async () => { if (!deferred) return; await deferred.prompt(); const { outcome } = await deferred.userChoice; if (outcome === "accepted") { + localStorage.setItem(KEY_INSTALLED, "1"); setDeferred(null); - setDismissed(true); + setShow(false); + } else { + // They dismissed the native prompt — snooze our custom one too + handleLater(); } }; - if (dismissed) return null; + if (!show) return null; + // iOS: show manual instructions + if (isIOS) { + return ; + } + + // Android / Chrome: show install button if (deferred) { return ( -
+
🌸
-

Install Tia

-

Add to home screen for quick access

+

Install Tia

+

Add to home screen — loads instantly

- + @@ -107,9 +167,5 @@ export function InstallPrompt() { ); } - if (isIOS) { - return ; - } - return null; }