@@ -1,10 +1,19 @@
import { useEffect , useState } from 'react' ;
import { useEffect , useState } from 'react' ;
interface BeforeInstallPromptEvent extends Event {
prompt : ( ) = > Promise < void > ;
userChoice : Promise < { outcome : 'accepted' | 'dismissed' } > ;
}
declare global {
declare global {
interface Navigator { standalone? : boolean }
interface Navigator { standalone? : boolean }
interface WindowEventMap {
beforeinstallprompt : BeforeInstallPromptEvent ;
}
}
}
const LAST_SHOWN_KEY = 'pwaInstallPrompt_lastShown' ;
const LAST_SHOWN_KEY = 'pwaInstallPrompt_lastShown' ;
const ANDROID_DISMISSED_KEY = 'pwaInstallPrompt_androidDismissed' ;
function isIOS ( ) : boolean {
function isIOS ( ) : boolean {
if ( typeof navigator === 'undefined' ) return false ;
if ( typeof navigator === 'undefined' ) return false ;
@@ -31,59 +40,144 @@ function isStandalone(): boolean {
export function PWAInstallPrompt ( { hidden = false } : { hidden? : boolean } ) {
export function PWAInstallPrompt ( { hidden = false } : { hidden? : boolean } ) {
if ( hidden ) return null ;
if ( hidden ) return null ;
const [ show , setShow ] = useState ( false ) ;
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 ) ;
const [ initialized , setInitialized ] = useState ( false ) ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
// Only run on client
// Only run on client
if ( typeof window === 'undefined' ) return ;
if ( typeof window === 'undefined' ) return ;
const eligible = isIOS ( ) && isSafari ( ) && ! isStandalone ( ) ;
// Check if already in standalone mode
if ( ! eligible ) {
if ( isStandalone ( ) ) {
setInitialized ( true ) ;
setInitialized ( true ) ;
return ;
return ;
}
}
let lastShown = 0 ;
// Handle iOS
try {
const isIOSDevice = isIOS ( ) && isSafari ( ) ;
lastShown = Number ( localStorage . getItem ( LAST_SHOWN_KEY ) || 0 ) ;
if ( isIOSDevice ) {
} catch { }
let lastShown = 0 ;
const oneWeek = 7 * 24 * 60 * 60 * 1000 ;
try {
const shouldShow = ! lastShown || Date . now ( ) - lastShown > oneWeek ;
lastShown = Number ( localStorage . getItem ( LAST_SHOWN_KEY ) || 0 ) ;
} catch { }
const oneWeek = 7 * 24 * 60 * 60 * 1000 ;
const shouldShow = ! lastShown || Date . now ( ) - lastShown > oneWeek ;
setShow ( shouldShow ) ;
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 ) ;
setInitialized ( true ) ;
return ( ) = > {
window . removeEventListener ( 'beforeinstallprompt' , handleBeforeInstall ) ;
} ;
} , [ ] ) ;
} , [ ] ) ;
const dismiss = ( ) = > {
const dismiss = ( ) = > {
try { localStorage . setItem ( LAST_SHOWN_KEY , String ( Date . now ( ) ) ) ; } catch { }
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 ) ;
setShow ( false ) ;
} ;
} ;
if ( ! initialized || ! show ) return null ;
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 (
return (
< div
< div
role = "dialog"
role = "dialog"
aria-label = " PWA Installation Anleitung"
aria-label = { promptType === 'android' ? 'PWA Installation' : ' PWA Installation Anleitung' }
className = "fixed bottom-0 left-0 right-0 z-50 px-4 pb-4"
className = "fixed bottom-0 left-0 right-0 z-50 px-4 pb-4"
style = { { paddingBottom : ` max(env(safe-area-inset-bottom), 1rem) ` } }
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" >
< 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 >
< 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 >
< div className = "flex items-start gap-3" >
< div classNam e ="text-2xl" > 📱 < / div >
{ promptTyp e === 'android' ? (
< div className = "flex-1" >
// Android: Direct install button
< h3 className = "text-lg sm:text-xl font-bold mb-2" > App installieren < / h3 >
< div className = "flex items-center gap-4" >
< p className = "text-white/90 mb-2" > So installierst du Stargirlnails als App auf deinem iPhone / iPad : < / p >
< div className = "text-3xl" > 📱 < / div >
< ol className = "list-decimal pl-5 space-y-1 text-white/95 " >
< div className = "flex-1 " >
< li > Ö ffne diese Seite in Safari ( nicht Chrome oder andere Browser ) . < / li >
< h3 className = "text-lg sm:text-xl font-bold mb-2" > App installieren < / h3 >
< li > Tippe auf das Teilen - Symbol ( □ ↑ ) unten in der Mitte . < / li >
< p className = "text-white/90 mb-3" >
< li > Scrolle nach unten und wäh le „ Zum Home - B ildschirm".</li>
Installiere Stargirlnails als App für schnel len Zugriff direkt vom Startb ildschirm!
<li>Tippe auf „Hinzufügen" . < / li >
< / p >
< / ol >
< div className = "flex gap-2" >
< p className = "mt-3 text-sm text-white/90" > ✨ Schneller Zugriff , keine App - Store - Installation nötig , automatische Updates . < / p >
< 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 >
< / 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 >
< / div >
< / div >
) ;
) ;