(null)
+
+ useEffect(() => {
+ const info = getDeviceInfo()
+ setDevice(info)
+
+ // Listen for the native install prompt (Chromium browsers on Android)
+ const handleBeforeInstall = (e: Event) => {
+ e.preventDefault()
+ setNativePrompt(e as BeforeInstallPromptEvent)
+ }
+ window.addEventListener("beforeinstallprompt", handleBeforeInstall)
+
+ const isMobile = info.isIOS || info.isAndroid
+ if (isMobile && !isStandalone() && !isDismissed()) {
+ const timer = setTimeout(() => setShow(true), 1500)
+ return () => {
+ clearTimeout(timer)
+ window.removeEventListener("beforeinstallprompt", handleBeforeInstall)
+ }
+ }
+ return () => {
+ window.removeEventListener("beforeinstallprompt", handleBeforeInstall)
+ }
+ }, [])
+
+ const dismiss = useCallback(() => {
+ setShow(false)
+ memoryDismissed = true
+ try {
+ localStorage.setItem(PWA_DISMISS_KEY, Date.now().toString())
+ } catch {}
+ }, [])
+
+ // Escape key handler
+ useEffect(() => {
+ if (!show) return
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === "Escape") dismiss()
+ }
+ document.addEventListener("keydown", handler)
+ return () => document.removeEventListener("keydown", handler)
+ }, [show, dismiss])
+
+ // Focus the panel when shown for accessibility
+ useEffect(() => {
+ if (show && panelRef.current) {
+ panelRef.current.focus()
+ }
+ }, [show])
+
+ const handleInstall = useCallback(async () => {
+ if (nativePrompt) {
+ nativePrompt.prompt()
+ const { outcome } = await nativePrompt.userChoice
+ if (outcome === "accepted") {
+ setShow(false)
+ }
+ setNativePrompt(null)
+ }
+ dismiss()
+ }, [nativePrompt, dismiss])
+
+ /**
+ * Determine which instructions to show:
+ * - iOS + Safari → standard iOS steps
+ * - iOS + non-Safari → "open in Safari" hint
+ * - Android + Chrome (with native prompt) → trigger native install
+ * - Android + Chrome (no native prompt) → manual Chrome steps
+ * - Android + non-Chrome → "open in Chrome" hint
+ */
+ const renderSteps = () => {
+ if (device.isIOS) {
+ if (device.isSafari) return
+ return
+ }
+ // Android
+ if (device.isChrome) return
+ return
+ }
+
+ return (
+
+ {show && (
+
+ e.stopPropagation()}
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="pwa-install-title"
+ tabIndex={-1}
+ className={cn(
+ "w-full max-w-lg bg-[#1B1F24] rounded-t-[22px] p-6 pb-8 flex flex-col gap-5 outline-none",
+ dmSansClassName(),
+ )}
+ style={{
+ boxShadow:
+ "0 -4px 24px 0 rgba(0, 0, 0, 0.4), 0.5px 0.5px 0.5px 0 rgba(255, 255, 255, 0.08) inset",
+ }}
+ >
+ {/* Header */}
+
+
+
+
+
+
+
+ Supermemory
+
+
+ Your memories, wherever you are
+
+
+
+
+
+
+ {/* Feature pills */}
+
+ {FEATURES.map((f) => (
+
+
+
+ {f.label}
+
+
+ ))}
+
+
+ {/* Install steps */}
+
+
+ Install for a better experience
+
+ {renderSteps()}
+
+
+ {/* Action */}
+
+
+
+
+
+ )}
+
+ )
+}
+
+function StepRow({
+ step,
+ children,
+}: {
+ step: number
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {step}
+
+ {children}
+
+ )
+}
+
+function IOSSteps() {
+ return (
+
+
+ Tap the share button{" "}
+ {" "}
+ in Safari
+
+
+ Scroll down and tap{" "}
+ Add to Home Screen
+
+
+ Tap Add to install
+
+
+ )
+}
+
+function IOSNonSafariSteps() {
+ return (
+
+
+ Open this page in Safari
+
+
+ Tap the share button{" "}
+
+
+
+ Tap Add to Home Screen
+
+
+ )
+}
+
+function AndroidSteps() {
+ return (
+
+
+ Tap the menu button{" "}
+ {" "}
+ in Chrome
+
+
+ Tap Add to Home screen
+
+
+ Tap Install to confirm
+
+
+ )
+}
+
+function AndroidNonChromeSteps() {
+ return (
+
+
+ Open this page in Chrome
+
+
+ Tap the menu button{" "}
+
+
+
+ Tap Add to Home screen
+
+
+ )
+}
+
+function ShareIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+function MoreVertIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+/**
+ * Type for the `beforeinstallprompt` event (not yet in TS lib).
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent
+ */
+interface BeforeInstallPromptEvent extends Event {
+ prompt(): Promise
+ userChoice: Promise<{ outcome: "accepted" | "dismissed" }>
+}
+
+declare global {
+ interface WindowEventMap {
+ beforeinstallprompt: BeforeInstallPromptEvent
+ }
+}