"use client" import { useState, useEffect, useCallback, type ReactNode } from "react" import Image from "next/image" import { cn } from "@lib/utils" import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts" import { XIcon, Check } from "lucide-react" import { Drawer, DrawerContent, DrawerTitle } from "@ui/components/drawer" import { Button } from "@ui/components/button" const PWA_DISMISS_KEY = "pwa-install-dismissed" const DISMISS_DURATION_MS = 7 * 24 * 60 * 60 * 1000 const INSET = "shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]" const CARD_INSET = "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]" type DeviceInfo = { isIOS: boolean isAndroid: boolean isSafari: boolean isChrome: boolean } let memoryDismissed = false function getDeviceInfo(): DeviceInfo { if (typeof window === "undefined") return { isIOS: false, isAndroid: false, isSafari: false, isChrome: false } const ua = navigator.userAgent const isIOS = /iPad|iPhone|iPod/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1) const isAndroid = /Android/.test(ua) const isSafari = /Safari/.test(ua) && !/CriOS|FxiOS|Chrome|Chromium|Edg|OPR|Opera/.test(ua) const isChrome = /Chrome/.test(ua) && !/Edg|OPR|Opera/.test(ua) return { isIOS, isAndroid, isSafari, isChrome } } 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 if (memoryDismissed) 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 BENEFITS = [ "Instant launch", "Full-screen mode", "Native-app feel", "Stays signed in", ] type Step = { title: string description: ReactNode } function buildSteps(device: DeviceInfo): Step[] { if (device.isIOS) { if (device.isSafari) { return [ { title: "Open the share menu", description: ( <> Tap the share icon{" "} {" "} at the bottom of Safari. > ), }, { title: "Add to Home Screen", description: "Scroll down in the share sheet and tap Add to Home Screen.", }, { title: "Confirm", description: "Tap Add in the top right to install.", }, ] } return [ { title: "Open this page in Safari", description: "The install flow only works inside Safari on iPhone and iPad.", }, { title: "Open the share menu", description: ( <> Tap the share icon{" "} {" "} at the bottom. > ), }, { title: "Add to Home Screen", description: "Choose Add to Home Screen and confirm.", }, ] } if (device.isChrome) { return [ { title: "Open the Chrome menu", description: ( <> Tap the menu icon{" "} {" "} in the top right. > ), }, { title: "Add to Home screen", description: "Tap Add to Home screen from the menu.", }, { title: "Confirm", description: "Tap Install to finish.", }, ] } return [ { title: "Open this page in Chrome", description: "Install works best from Chrome on Android.", }, { title: "Open the Chrome menu", description: ( <> Tap the menu icon{" "} {" "} in the top right. > ), }, { title: "Add to Home screen", description: "Choose Add to Home screen and confirm.", }, ] } export function PWAInstallPrompt() { const [show, setShow] = useState(false) const [device, setDevice] = useState({ isIOS: false, isAndroid: false, isSafari: false, isChrome: false, }) const [nativePrompt, setNativePrompt] = useState(null) useEffect(() => { const info = getDeviceInfo() setDevice(info) const handleBeforeInstall = (e: Event) => { e.preventDefault() setNativePrompt(e as BeforeInstallPromptEvent) } window.addEventListener("beforeinstallprompt", handleBeforeInstall) const isMobile = info.isIOS || info.isAndroid let timer: ReturnType | undefined if (isMobile && !isStandalone() && !isDismissed()) { timer = setTimeout(() => setShow(true), 1500) } return () => { if (timer) clearTimeout(timer) window.removeEventListener("beforeinstallprompt", handleBeforeInstall) } }, []) const dismiss = useCallback(() => { setShow(false) memoryDismissed = true try { localStorage.setItem(PWA_DISMISS_KEY, Date.now().toString()) } catch {} }, []) const handleInstall = useCallback(async () => { if (nativePrompt) { nativePrompt.prompt() await nativePrompt.userChoice setNativePrompt(null) } dismiss() }, [nativePrompt, dismiss]) const openShareMenu = useCallback(async () => { try { if (typeof navigator !== "undefined" && "share" in navigator) { await navigator.share({ title: "Supermemory", url: window.location.href, }) } } catch {} dismiss() }, [dismiss]) const canNativeInstall = !!nativePrompt && device.isAndroid && device.isChrome const canShare = typeof navigator !== "undefined" && "share" in navigator const steps = buildSteps(device) return ( !open && dismiss()}> div:first-child]:bg-[#3A4252] [&>div:first-child]:h-1 [&>div:first-child]:w-9 [&>div:first-child]:mt-2.5 [&>div:first-child]:mb-1", dmSansClassName(), )} > Install Supermemory Install Supermemory Your memories, one tap away. {BENEFITS.map((text) => ( {text} ))} {steps.map((step, i) => ( {i + 1} {i < steps.length - 1 && ( )} {step.title} {step.description} ))} {canNativeInstall ? "Install now" : canShare ? "Add to Home Screen" : "Got it"} ) } function ShareIcon({ className }: { className?: string }) { return ( ) } function MoreVertIcon({ className }: { className?: string }) { return ( ) } interface BeforeInstallPromptEvent extends Event { prompt(): Promise userChoice: Promise<{ outcome: "accepted" | "dismissed" }> } declare global { interface WindowEventMap { beforeinstallprompt: BeforeInstallPromptEvent } }
Your memories, one tap away.
{step.title}
{step.description}