diff --git a/app/layout.tsx b/app/layout.tsx index 48057f0..27c760b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import { Inter } from 'next/font/google' import { LaunchGate } from '@/components/LaunchGate' import { ServiceWorkerRegistrar } from '@/components/ServiceWorkerRegistrar' import { PwaMetaTags } from '@/components/PwaMetaTags' +import { InstallBanner } from '@/components/InstallBanner' import './globals.css' const inter = Inter({ @@ -58,6 +59,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac + {children} {/* Domain is encoded in the proxy script URL - no data-domain attribute needed */} {process.env.NODE_ENV === 'production' && ( diff --git a/components/InstallBanner.tsx b/components/InstallBanner.tsx new file mode 100644 index 0000000..b5b1f3e --- /dev/null +++ b/components/InstallBanner.tsx @@ -0,0 +1,66 @@ +'use client' + +import { useState, useRef, useEffect } from 'react' + +interface BeforeInstallPromptEvent extends Event { + prompt: () => Promise + userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }> +} + +export function InstallBanner() { + const [canInstall, setCanInstall] = useState(false) + const [dismissed, setDismissed] = useState(false) + const installEventRef = useRef(null) + + useEffect(() => { + const handleBeforeInstall = (e: Event) => { + e.preventDefault() + installEventRef.current = e as BeforeInstallPromptEvent + setCanInstall(true) + } + const handleInstalled = () => { + setCanInstall(false) + } + window.addEventListener('beforeinstallprompt', handleBeforeInstall) + window.addEventListener('appinstalled', handleInstalled) + return () => { + window.removeEventListener('beforeinstallprompt', handleBeforeInstall) + window.removeEventListener('appinstalled', handleInstalled) + installEventRef.current = null + setCanInstall(false) + } + }, []) + + const handleInstall = async () => { + const e = installEventRef.current + if (!e) return + await e.prompt() + const { outcome } = await e.userChoice + if (outcome === 'accepted') setCanInstall(false) + } + + if (!canInstall || dismissed) return null + + return ( +
+ Install Multicorn Learn +
+ + +
+
+ ) +} diff --git a/components/PwaMetaTags.tsx b/components/PwaMetaTags.tsx index 5a7ebc7..2eb81fe 100644 --- a/components/PwaMetaTags.tsx +++ b/components/PwaMetaTags.tsx @@ -4,13 +4,13 @@ import { useEffect } from 'react' /** * Injects iOS PWA meta tags. Next.js Metadata API does not output - * apple-mobile-web-app-capable in a way that enables Safari splash screens, + * mobile-web-app-capable in a way that enables Safari splash screens, * so we inject these on the client. */ export function PwaMetaTags() { useEffect(() => { const tags: [string, Record][] = [ - ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], + ['meta', { name: 'mobile-web-app-capable', content: 'yes' }], ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'default' }], ['meta', { name: 'apple-mobile-web-app-title', content: 'Learn' }], ] diff --git a/public/learn/apple-touch-icon.png b/public/learn/apple-touch-icon.png index 92a8ec3..8c4d8e1 100644 Binary files a/public/learn/apple-touch-icon.png and b/public/learn/apple-touch-icon.png differ diff --git a/public/learn/favicon-192x192.png b/public/learn/favicon-192x192.png index 8f64ef9..f3ee545 100644 Binary files a/public/learn/favicon-192x192.png and b/public/learn/favicon-192x192.png differ diff --git a/public/learn/favicon-512x512.png b/public/learn/favicon-512x512.png index ebfdbf4..535099a 100644 Binary files a/public/learn/favicon-512x512.png and b/public/learn/favicon-512x512.png differ