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;
}