import React, { useEffect, useState } from 'react'; import { Text, View } from 'react-native'; import { Redirect, Stack, usePathname, useRouter } from 'expo-router'; import { useShareIntent } from 'expo-share-intent'; import { StatusBar } from 'expo-status-bar'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { AppProvider, useApp } from '../context/AppContext'; import { CoachMarksProvider } from '../context/CoachMarksContext'; import { CoachMarksOverlay } from '../components/CoachMarksOverlay'; import { useColors } from '../constants/Colors'; import { initDatabase, AppMetaDb } from '../services/database'; import * as SecureStore from 'expo-secure-store'; import * as SplashScreen from 'expo-splash-screen'; import * as ExpoLinking from 'expo-linking'; import { AuthService } from '../services/authService'; import { AnimatedSplashScreen } from '../components/AnimatedSplashScreen'; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync().catch(() => { }); const SECURE_INSTALL_MARKER = 'greenlens_install_v1'; const isShareIntentUrl = (url: string | null | undefined) => Boolean(url?.includes('://dataUrl=')); const toStartupErrorMessage = (error: unknown): string => { if (!error) return 'Unknown startup error'; if (error instanceof Error) return error.message; return String(error); }; const StartupFallback = ({ details }: { details?: string | null }) => ( GreenLens could not start. Please send this startup error to support. {details ? ( {details} ) : null} ); class RootErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean; errorMessage: string | null }> { state = { hasError: false, errorMessage: null }; static getDerivedStateFromError(error: unknown) { return { hasError: true, errorMessage: toStartupErrorMessage(error) }; } componentDidCatch(error: unknown) { console.error('[RootErrorBoundary]', error); } render() { if (this.state.hasError) { return ; } return this.props.children; } } const ensureInstallConsistency = async (): Promise => { try { const sqliteMarker = AppMetaDb.get('install_marker_v2'); const secureMarker = await SecureStore.getItemAsync(SECURE_INSTALL_MARKER).catch(() => null); if (sqliteMarker === '1' && secureMarker === '1') { return false; // Alles gut, keine Neuinstallation } if (sqliteMarker === '1' || secureMarker === '1') { // Teilweise vorhanden -> heilen, nicht löschen AppMetaDb.set('install_marker_v2', '1'); await SecureStore.setItemAsync(SECURE_INSTALL_MARKER, '1'); return false; } // Fresh Install: Alles zurücksetzen await AuthService.logout(); await AsyncStorage.removeItem('greenlens_show_tour'); AppMetaDb.set('install_marker_v2', '1'); await SecureStore.setItemAsync(SECURE_INSTALL_MARKER, '1'); return true; } catch (error) { console.error('Failed to initialize install marker', error); return false; } }; function RootLayoutInner() { const { isDarkMode, colorPalette, signOut, session, billingSummary, isActivatingEntitlement, isInitializing, isLoadingPlants, isLoadingBilling, } = useApp(); const colors = useColors(isDarkMode, colorPalette); const pathname = usePathname(); const router = useRouter(); const [shareIntentEnabled, setShareIntentEnabled] = useState(false); const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent({ disabled: !shareIntentEnabled }); const [installCheckDone, setInstallCheckDone] = useState(false); const [splashAnimationComplete, setSplashAnimationComplete] = useState(false); useEffect(() => { let mounted = true; ExpoLinking.getInitialURL() .then((url) => { if (mounted && isShareIntentUrl(url)) { setShareIntentEnabled(true); } }) .catch(() => {}); const subscription = ExpoLinking.addEventListener('url', ({ url }) => { if (isShareIntentUrl(url)) { setShareIntentEnabled(true); } }); return () => { mounted = false; subscription.remove(); }; }, []); useEffect(() => { (async () => { const didResetSessionForFreshInstall = await ensureInstallConsistency(); if (didResetSessionForFreshInstall) { await signOut(); } setInstallCheckDone(true); })(); }, [signOut]); const isAppReady = installCheckDone && !isInitializing && !isLoadingPlants; useEffect(() => { if (!hasShareIntent || !isAppReady) return; const sharedImage = shareIntent.files?.find((file) => file.mimeType?.startsWith('image/')); if (!sharedImage) { resetShareIntent(); return; } const uri = sharedImage.path; if (!uri) { resetShareIntent(); return; } resetShareIntent(); router.push({ pathname: '/scanner', params: { sharedImageUri: uri }, }); }, [hasShareIntent, shareIntent, resetShareIntent, router, isAppReady]); const hasActiveEntitlement = isActivatingEntitlement || (billingSummary?.entitlement?.plan === 'pro' && billingSummary?.entitlement?.status === 'active'); const isAllowedWithoutSession = pathname.includes('onboarding') || pathname.includes('auth/') || pathname.includes('scanner') || pathname.includes('profile/billing'); const isAllowedWithoutEntitlement = pathname.includes('auth/') || pathname.includes('onboarding') || pathname.includes('scanner') || pathname.includes('profile/billing'); let content = null; if (isAppReady) { if (!session) { // Only redirect if we are not already on an auth-related page or the scanner if (!isAllowedWithoutSession) { content = ; } else { content = ( ); } } else if (!hasActiveEntitlement && !isLoadingBilling && !isAllowedWithoutEntitlement) { content = ; } else { content = ( <> ); } } return ( <> {content} {!splashAnimationComplete && ( setSplashAnimationComplete(true)} /> )} ); } export default function RootLayout() { let dbInitError: string | null = null; try { initDatabase(); } catch (e) { dbInitError = String(e); } if (dbInitError) { return ; } return ( ); }