From 1ff25d6eeb0317778bd1aff5430737c570b81fec Mon Sep 17 00:00:00 2001
From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com>
Date: Sun, 17 May 2026 03:12:59 +0000
Subject: [PATCH] feat: add PWA install instructions modal for mobile
---
apps/web/app/(app)/layout.tsx | 2 +
apps/web/components/pwa-install-prompt.tsx | 282 +++++++++++++++++++++
2 files changed, 284 insertions(+)
create mode 100644 apps/web/components/pwa-install-prompt.tsx
diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx
index 33c88e04..b2a27e07 100644
--- a/apps/web/app/(app)/layout.tsx
+++ b/apps/web/app/(app)/layout.tsx
@@ -2,12 +2,14 @@
import { EnsureWorkspace } from "@/components/ensure-workspace"
import { NextAppResearchCta } from "@/components/next-app-research-cta"
+import { PWAInstallPrompt } from "@/components/pwa-install-prompt"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
+
>
)
}
diff --git a/apps/web/components/pwa-install-prompt.tsx b/apps/web/components/pwa-install-prompt.tsx
new file mode 100644
index 00000000..a140ac87
--- /dev/null
+++ b/apps/web/components/pwa-install-prompt.tsx
@@ -0,0 +1,282 @@
+"use client"
+
+import { useState, useEffect, useCallback } from "react"
+import { cn } from "@lib/utils"
+import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
+import { XIcon, Brain, Sparkles, Globe } from "lucide-react"
+import { GradientLogo } from "@ui/assets/Logo"
+import { AnimatePresence, motion } from "motion/react"
+
+const PWA_DISMISS_KEY = "pwa-install-dismissed"
+const DISMISS_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
+
+function getDeviceInfo() {
+ if (typeof window === "undefined") return { isIOS: false, isAndroid: false }
+ const ua = navigator.userAgent
+ const isIOS =
+ /iPad|iPhone|iPod/.test(ua) ||
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
+ const isAndroid = /Android/.test(ua)
+ return { isIOS, isAndroid }
+}
+
+function isStandalone() {
+ if (typeof window === "undefined") return false
+ return (
+ window.matchMedia("(display-mode: standalone)").matches ||
+ (window.navigator as unknown as { standalone?: boolean }).standalone ===
+ true
+ )
+}
+
+function isDismissed() {
+ if (typeof window === "undefined") return true
+ try {
+ const dismissed = localStorage.getItem(PWA_DISMISS_KEY)
+ if (!dismissed) return false
+ const timestamp = Number.parseInt(dismissed, 10)
+ if (Date.now() - timestamp < DISMISS_DURATION_MS) return true
+ localStorage.removeItem(PWA_DISMISS_KEY)
+ return false
+ } catch {
+ return false
+ }
+}
+
+const FEATURES = [
+ { icon: Brain, label: "AI-powered memory" },
+ { icon: Sparkles, label: "Chat with Nova" },
+ { icon: Globe, label: "Access anywhere" },
+]
+
+export function PWAInstallPrompt() {
+ const [show, setShow] = useState(false)
+ const [device, setDevice] = useState<{ isIOS: boolean; isAndroid: boolean }>({
+ isIOS: false,
+ isAndroid: false,
+ })
+
+ useEffect(() => {
+ const info = getDeviceInfo()
+ setDevice(info)
+ const isMobile = info.isIOS || info.isAndroid
+ if (isMobile && !isStandalone() && !isDismissed()) {
+ const timer = setTimeout(() => setShow(true), 1500)
+ return () => clearTimeout(timer)
+ }
+ }, [])
+
+ const dismiss = useCallback(() => {
+ setShow(false)
+ try {
+ localStorage.setItem(PWA_DISMISS_KEY, Date.now().toString())
+ } catch {}
+ }, [])
+
+ return (
+
+ {show && (
+
+ e.stopPropagation()}
+ className={cn(
+ "w-full max-w-lg bg-[#1B1F24] rounded-t-[22px] p-6 pb-8 flex flex-col gap-5",
+ 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
+
+ {device.isIOS ?
:
}
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+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 AndroidSteps() {
+ return (
+
+
+ Tap the menu button{" "}
+ {" "}
+ in Chrome
+
+
+ Tap Add to Home screen
+
+
+ Tap Install to confirm
+
+
+ )
+}
+
+function ShareIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+function MoreVertIcon({ className }: { className?: string }) {
+ return (
+
+ )
+}