188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
|
||
interface BeforeInstallPromptEvent extends Event {
|
||
prompt: () => Promise<void>;
|
||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||
}
|
||
|
||
declare global {
|
||
interface Navigator { standalone?: boolean }
|
||
interface WindowEventMap {
|
||
beforeinstallprompt: BeforeInstallPromptEvent;
|
||
}
|
||
}
|
||
|
||
const LAST_SHOWN_KEY = 'pwaInstallPrompt_lastShown';
|
||
const ANDROID_DISMISSED_KEY = 'pwaInstallPrompt_androidDismissed';
|
||
|
||
function isIOS(): boolean {
|
||
if (typeof navigator === 'undefined') return false;
|
||
const ua = navigator.userAgent || '';
|
||
const iOS = /iPhone|iPad|iPod/i.test(ua);
|
||
const iPadOS13Plus = /Macintosh/.test(ua) && 'ontouchend' in document;
|
||
return iOS || iPadOS13Plus;
|
||
}
|
||
|
||
function isSafari(): boolean {
|
||
const ua = navigator.userAgent || '';
|
||
const isSafari = /Safari/i.test(ua) && !/CriOS|FxiOS|EdgiOS/i.test(ua);
|
||
return isSafari;
|
||
}
|
||
|
||
function isStandalone(): boolean {
|
||
const navStandalone = (navigator as any)?.standalone === true;
|
||
const mm = typeof window !== 'undefined' && window.matchMedia
|
||
? window.matchMedia('(display-mode: standalone)').matches
|
||
: false;
|
||
return Boolean(navStandalone || mm);
|
||
}
|
||
|
||
export function PWAInstallPrompt({ hidden = false }: { hidden?: boolean }) {
|
||
if (hidden) return null;
|
||
const [show, setShow] = useState(false);
|
||
const [promptType, setPromptType] = useState<'ios' | 'android' | null>(null);
|
||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||
const [initialized, setInitialized] = useState(false);
|
||
|
||
useEffect(() => {
|
||
// Only run on client
|
||
if (typeof window === 'undefined') return;
|
||
|
||
// Check if already in standalone mode
|
||
if (isStandalone()) {
|
||
setInitialized(true);
|
||
return;
|
||
}
|
||
|
||
// Handle iOS
|
||
const isIOSDevice = isIOS() && isSafari();
|
||
if (isIOSDevice) {
|
||
let lastShown = 0;
|
||
try {
|
||
lastShown = Number(localStorage.getItem(LAST_SHOWN_KEY) || 0);
|
||
} catch {}
|
||
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
||
const shouldShow = !lastShown || Date.now() - lastShown > oneWeek;
|
||
|
||
if (shouldShow) {
|
||
setPromptType('ios');
|
||
setShow(true);
|
||
}
|
||
setInitialized(true);
|
||
return;
|
||
}
|
||
|
||
// Handle Android (beforeinstallprompt)
|
||
const handleBeforeInstall = (e: BeforeInstallPromptEvent) => {
|
||
e.preventDefault();
|
||
|
||
// Check if user has dismissed Android prompt before
|
||
let dismissed = false;
|
||
try {
|
||
dismissed = localStorage.getItem(ANDROID_DISMISSED_KEY) === 'true';
|
||
} catch {}
|
||
|
||
if (!dismissed) {
|
||
setDeferredPrompt(e);
|
||
setPromptType('android');
|
||
setShow(true);
|
||
}
|
||
};
|
||
|
||
window.addEventListener('beforeinstallprompt', handleBeforeInstall);
|
||
setInitialized(true);
|
||
|
||
return () => {
|
||
window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
|
||
};
|
||
}, []);
|
||
|
||
const dismiss = () => {
|
||
if (promptType === 'ios') {
|
||
try { localStorage.setItem(LAST_SHOWN_KEY, String(Date.now())); } catch {}
|
||
} else if (promptType === 'android') {
|
||
try { localStorage.setItem(ANDROID_DISMISSED_KEY, 'true'); } catch {}
|
||
}
|
||
setShow(false);
|
||
};
|
||
|
||
const handleAndroidInstall = async () => {
|
||
if (!deferredPrompt) return;
|
||
|
||
try {
|
||
await deferredPrompt.prompt();
|
||
const choiceResult = await deferredPrompt.userChoice;
|
||
|
||
if (choiceResult.outcome === 'accepted') {
|
||
console.log('PWA installation accepted');
|
||
}
|
||
|
||
setDeferredPrompt(null);
|
||
setShow(false);
|
||
} catch (error) {
|
||
console.error('Error during PWA installation:', error);
|
||
}
|
||
};
|
||
|
||
if (!initialized || !show || !promptType) return null;
|
||
|
||
return (
|
||
<div
|
||
role="dialog"
|
||
aria-label={promptType === 'android' ? 'PWA Installation' : 'PWA Installation Anleitung'}
|
||
className="fixed bottom-0 left-0 right-0 z-50 px-4 pb-4"
|
||
style={{ paddingBottom: `max(env(safe-area-inset-bottom), 1rem)` }}
|
||
>
|
||
<div className="relative max-w-3xl mx-auto rounded-xl shadow-lg bg-gradient-to-r from-pink-500 to-purple-600 text-white p-4 sm:p-6">
|
||
<button aria-label="Hinweis schließen" onClick={dismiss} className="absolute top-2 right-2 text-white/90 hover:text-white text-2xl leading-none">×</button>
|
||
|
||
{promptType === 'android' ? (
|
||
// Android: Direct install button
|
||
<div className="flex items-center gap-4">
|
||
<div className="text-3xl">📱</div>
|
||
<div className="flex-1">
|
||
<h3 className="text-lg sm:text-xl font-bold mb-2">App installieren</h3>
|
||
<p className="text-white/90 mb-3">
|
||
Installiere Stargirlnails als App für schnellen Zugriff direkt vom Startbildschirm!
|
||
</p>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={handleAndroidInstall}
|
||
className="bg-white text-pink-600 px-4 py-2 rounded-lg font-semibold hover:bg-pink-50 transition-colors"
|
||
>
|
||
Jetzt installieren
|
||
</button>
|
||
<button
|
||
onClick={dismiss}
|
||
className="bg-white/20 text-white px-4 py-2 rounded-lg font-semibold hover:bg-white/30 transition-colors"
|
||
>
|
||
Später
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
// iOS: Manual instructions
|
||
<div className="flex items-start gap-3">
|
||
<div className="text-2xl">📱</div>
|
||
<div className="flex-1">
|
||
<h3 className="text-lg sm:text-xl font-bold mb-2">App installieren</h3>
|
||
<p className="text-white/90 mb-2">So installierst du Stargirlnails als App auf deinem iPhone/iPad:</p>
|
||
<ol className="list-decimal pl-5 space-y-1 text-white/95">
|
||
<li>Öffne diese Seite in Safari (nicht Chrome oder andere Browser).</li>
|
||
<li>Tippe auf das Teilen-Symbol (□↑) unten in der Mitte.</li>
|
||
<li>Scrolle nach unten und wähle „Zum Home-Bildschirm".</li>
|
||
<li>Tippe auf „Hinzufügen".</li>
|
||
</ol>
|
||
<p className="mt-3 text-sm text-white/90">✨ Schneller Zugriff, keine App-Store-Installation nötig, automatische Updates.</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default PWAInstallPrompt;
|
||
|