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 ( + + ) +}