App chrash + seo

This commit is contained in:
2026-05-10 22:37:01 +02:00
parent 9386ae1be7
commit 2658c37453
28 changed files with 13232 additions and 519 deletions

View File

@@ -15,7 +15,7 @@ import { useRouter } from 'expo-router';
import { useFocusEffect } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
@@ -233,7 +233,7 @@ export default function HomeScreen() {
const { layouts, registerLayout, startTour } = useCoachMarks();
const fabRef = useRef<View>(null);
const tourStartRequestedRef = useRef(false);
const posthog = usePostHog();
const posthog = useSafeAnalytics();
useFocusEffect(
React.useCallback(() => {

View File

@@ -1,12 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { Animated, AppState, Easing, Image, StyleSheet, Text, View } from 'react-native';
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 Purchases, { LOG_LEVEL } from 'react-native-purchases';
import { Platform } from 'react-native';
import Constants from 'expo-constants';
import { AppProvider, useApp } from '../context/AppContext';
import { CoachMarksProvider } from '../context/CoachMarksContext';
import { CoachMarksOverlay } from '../components/CoachMarksOverlay';
@@ -14,14 +11,57 @@ 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 { PostHogProvider, usePostHog } from 'posthog-react-native';
import { AnimatedSplashScreen } from '../components/AnimatedSplashScreen';
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync().catch(() => { });
const POSTHOG_API_KEY = process.env.EXPO_PUBLIC_POSTHOG_API_KEY || 'phc_FX6HRgx9NSpS5moxjMF6xyc37yMwjoeu6TbWUqNNKlk';
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 }) => (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24, backgroundColor: '#111813' }}>
<Text style={{ color: '#fff', fontSize: 18, fontWeight: '700', marginBottom: 8, textAlign: 'center' }}>
GreenLens could not start.
</Text>
<Text style={{ color: '#D6DED7', fontSize: 14, lineHeight: 20, textAlign: 'center' }}>
Please send this startup error to support.
</Text>
{details ? (
<Text style={{ color: '#FFB4A8', fontSize: 12, lineHeight: 17, marginTop: 16, textAlign: 'center' }}>
{details}
</Text>
) : null}
</View>
);
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 <StartupFallback details={this.state.errorMessage} />;
}
return this.props.children;
}
}
const ensureInstallConsistency = async (): Promise<boolean> => {
try {
@@ -51,9 +91,6 @@ const ensureInstallConsistency = async (): Promise<boolean> => {
}
};
import { AnimatedSplashScreen } from '../components/AnimatedSplashScreen';
function RootLayoutInner() {
const {
isDarkMode,
@@ -65,88 +102,37 @@ function RootLayoutInner() {
isInitializing,
isLoadingPlants,
isLoadingBilling,
syncRevenueCatState,
} = useApp();
const colors = useColors(isDarkMode, colorPalette);
const pathname = usePathname();
const router = useRouter();
const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent();
const [shareIntentEnabled, setShareIntentEnabled] = useState(false);
const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent({ disabled: !shareIntentEnabled });
const [installCheckDone, setInstallCheckDone] = useState(false);
const [splashAnimationComplete, setSplashAnimationComplete] = useState(false);
const [revenueCatReady, setRevenueCatReady] = useState(Constants.appOwnership === 'expo');
const posthog = usePostHog();
useEffect(() => {
// RevenueCat requires native store access — not available in Expo Go
const isExpoGo = Constants.appOwnership === 'expo';
if (isExpoGo) {
console.log('[RevenueCat] Skipping configure: running in Expo Go');
return;
}
let mounted = true;
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
const iosApiKey = process.env.EXPO_PUBLIC_REVENUECAT_IOS_API_KEY || 'appl_hrSpsuUuVstbHhYIDnOqYxPOnmR';
const androidApiKey = process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY || 'goog_placeholder';
if (Platform.OS === 'ios') {
Purchases.configure({ apiKey: iosApiKey });
} else if (Platform.OS === 'android') {
Purchases.configure({ apiKey: androidApiKey });
}
setRevenueCatReady(true);
}, []);
useEffect(() => {
const isExpoGo = Constants.appOwnership === 'expo';
if (isExpoGo || !revenueCatReady) {
return;
}
let cancelled = false;
(async () => {
try {
if (session?.serverUserId) {
await Purchases.logIn(session.serverUserId);
const customerInfo = await Purchases.getCustomerInfo();
if (!cancelled) {
await syncRevenueCatState(customerInfo as any, 'app_init');
}
} else {
await Purchases.logOut();
ExpoLinking.getInitialURL()
.then((url) => {
if (mounted && isShareIntentUrl(url)) {
setShareIntentEnabled(true);
}
} catch (error) {
console.error('Failed to align RevenueCat identity', error);
}
})();
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [revenueCatReady, session?.serverUserId, syncRevenueCatState]);
useEffect(() => {
if (session?.serverUserId) {
posthog.identify(session.serverUserId, {
email: session.email,
name: session.name,
});
} else if (session === null) {
posthog.reset();
}
}, [session, posthog]);
useEffect(() => {
posthog.capture('screen_viewed', { screen: pathname });
}, [pathname, posthog]);
useEffect(() => {
posthog.capture('app_opened');
const subscription = AppState.addEventListener('change', (nextState) => {
if (nextState === 'active') {
posthog.capture('app_opened');
const subscription = ExpoLinking.addEventListener('url', ({ url }) => {
if (isShareIntentUrl(url)) {
setShareIntentEnabled(true);
}
});
return () => subscription.remove();
}, [posthog]);
return () => {
mounted = false;
subscription.remove();
};
}, []);
useEffect(() => {
(async () => {
@@ -288,18 +274,24 @@ function RootLayoutInner() {
}
export default function RootLayout() {
initDatabase();
let dbInitError: string | null = null;
try {
initDatabase();
} catch (e) {
dbInitError = String(e);
}
if (dbInitError) {
return <StartupFallback details={`Database init failed: ${dbInitError}`} />;
}
return (
<PostHogProvider apiKey={POSTHOG_API_KEY} options={{
host: 'https://us.i.posthog.com',
enableSessionReplay: false,
}}>
<RootErrorBoundary>
<AppProvider>
<CoachMarksProvider>
<RootLayoutInner />
</CoachMarksProvider>
</AppProvider>
</PostHogProvider>
</RootErrorBoundary>
);
}

View File

@@ -19,7 +19,7 @@ import { useColors } from '../../constants/Colors';
import { AuthService } from '../../services/authService';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
const ONBOARDING_AUTH_BACKGROUND = {
light: '#fbfaf3',
@@ -29,7 +29,7 @@ const ONBOARDING_AUTH_BACKGROUND = {
export default function LoginScreen() {
const { isDarkMode, colorPalette, hydrateSession, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const screenBackground = isDarkMode
? ONBOARDING_AUTH_BACKGROUND.dark
: ONBOARDING_AUTH_BACKGROUND.light;

View File

@@ -19,7 +19,7 @@ import { AuthService } from '../../services/authService';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
const ONBOARDING_AUTH_BACKGROUND = {
light: '#fbfaf3',
@@ -29,7 +29,7 @@ const ONBOARDING_AUTH_BACKGROUND = {
export default function SignupScreen() {
const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const pendingPlant = getPendingPlant();
const screenBackground = isDarkMode
? ONBOARDING_AUTH_BACKGROUND.dark

View File

@@ -3,7 +3,7 @@ import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-nati
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
@@ -19,7 +19,7 @@ const PALETTE_SWATCHES: Record<ColorPalette, string[]> = {
export default function CustomizeOnboardingScreen() {
const router = useRouter();
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const {
session,
isDarkMode,

View File

@@ -3,7 +3,7 @@ import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
@@ -58,7 +58,7 @@ const getExperienceScreenCopy = (language: 'de' | 'en' | 'es') => {
export default function OnboardingExperienceScreen() {
const router = useRouter();
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const { session, isDarkMode, colorPalette, language, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;

View File

@@ -3,7 +3,7 @@ import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
@@ -62,7 +62,7 @@ const getGoalScreenCopy = (language: 'de' | 'en' | 'es') => {
export default function OnboardingGoalScreen() {
const router = useRouter();
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const { session, isDarkMode, colorPalette, language, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;

View File

@@ -3,7 +3,7 @@ import { ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View }
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
@@ -75,7 +75,7 @@ const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
export default function HealthCheckOnboardingScreen() {
const router = useRouter();
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const { isDarkMode, colorPalette, language, billingSummary } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;

View File

@@ -3,7 +3,7 @@ import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
@@ -79,7 +79,7 @@ const getSourceOnboardingCopy = (language: 'de' | 'en' | 'es') => {
export default function OnboardingSourceScreen() {
const router = useRouter();
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const { session, isDarkMode, colorPalette, language, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;

View File

@@ -1,11 +1,12 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, useWindowDimensions } from 'react-native';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, ActivityIndicator, Alert, Linking, BackHandler, ImageBackground, Platform, useWindowDimensions } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useFocusEffect } from '@react-navigation/native';
import Constants from 'expo-constants';
import Purchases, {
LOG_LEVEL,
PACKAGE_TYPE,
PRODUCT_CATEGORY,
PurchasesOffering,
@@ -13,7 +14,7 @@ import Purchases, {
PurchasesStoreProduct,
} from 'react-native-purchases';
import { useApp } from '../../context/AppContext';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../../services/analytics';
import { useColors } from '../../constants/Colors';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { Language } from '../../types';
@@ -82,6 +83,24 @@ const summarizeOfferingPackages = (offering: PurchasesOffering | null) => {
};
};
let revenueCatConfigured = false;
const ensureRevenueCatConfigured = () => {
if (revenueCatConfigured || Constants.appOwnership === 'expo') {
return;
}
Purchases.setLogLevel(LOG_LEVEL.WARN);
const iosApiKey = process.env.EXPO_PUBLIC_REVENUECAT_IOS_API_KEY || 'appl_hrSpsuUuVstbHhYIDnOqYxPOnmR';
const androidApiKey = process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY || 'goog_placeholder';
if (Platform.OS === 'ios') {
Purchases.configure({ apiKey: iosApiKey });
} else if (Platform.OS === 'android') {
Purchases.configure({ apiKey: androidApiKey });
}
revenueCatConfigured = true;
};
const getBillingCopy = (language: Language) => {
if (language === 'de') {
return {
@@ -319,7 +338,7 @@ export default function BillingScreen() {
const router = useRouter();
const { isDarkMode, language, billingSummary, isLoadingBilling, simulatePurchase, simulateWebhookEvent, syncRevenueCatState, colorPalette, session } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const copy = getBillingCopy(language);
const isExpoGo = Constants.appOwnership === 'expo';
const { height: windowHeight } = useWindowDimensions();
@@ -328,6 +347,7 @@ export default function BillingScreen() {
const [subModalVisible, setSubModalVisible] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [storeReady, setStoreReady] = useState(isExpoGo);
const [storeError, setStoreError] = useState<string | null>(null);
const [subscriptionPackages, setSubscriptionPackages] = useState<SubscriptionPackages>({});
const [topupProducts, setTopupProducts] = useState<TopupProducts>({});
const [selectedPaywallPlan, setSelectedPaywallPlan] = useState<PaywallPlanId>('yearly');
@@ -349,6 +369,7 @@ export default function BillingScreen() {
}
try {
ensureRevenueCatConfigured();
const [offerings, topups] = await Promise.all([
Purchases.getOfferings(),
Purchases.getProducts(['topup_small', 'topup_medium', 'topup_large'], PRODUCT_CATEGORY.NON_SUBSCRIPTION),
@@ -369,8 +390,12 @@ export default function BillingScreen() {
topup_medium: topups.find((product) => product.identifier === 'topup_medium'),
topup_large: topups.find((product) => product.identifier === 'topup_large'),
});
setStoreError(null);
} catch (error) {
console.warn('Failed to load RevenueCat products', error);
if (!cancelled) {
setStoreError('Purchases are temporarily unavailable. Please try again later.');
}
} finally {
if (!cancelled) {
setStoreReady(true);
@@ -386,12 +411,14 @@ export default function BillingScreen() {
}, [isExpoGo]);
useEffect(() => {
posthog.capture('paywall_viewed', { plan_id: planId });
try { posthog.capture('paywall_viewed', { plan_id: planId }); } catch {}
if (showPaywallPlans) {
posthog.capture('hard_paywall_viewed', {
plan_id: planId,
authenticated: Boolean(session),
});
try {
posthog.capture('hard_paywall_viewed', {
plan_id: planId,
authenticated: Boolean(session),
});
} catch {}
}
}, [posthog, planId, session?.serverUserId, showPaywallPlans]);
@@ -467,6 +494,11 @@ export default function BillingScreen() {
return;
}
if (!isExpoGo && storeError) {
Alert.alert('Purchases unavailable', storeError);
return;
}
setIsUpdating(true);
posthog.capture('purchase_initiated', { product_id: productId });
try {
@@ -483,6 +515,7 @@ export default function BillingScreen() {
await completeExpoGoSimulation(productId);
return;
} else {
ensureRevenueCatConfigured();
if (productId === 'monthly_pro' || productId === 'yearly_pro') {
if (planId === 'pro') {
await openAppleSubscriptions();
@@ -553,6 +586,7 @@ export default function BillingScreen() {
setIsUpdating(true);
try {
if (!isExpoGo) {
ensureRevenueCatConfigured();
const customerInfo = await Purchases.restorePurchases();
await syncRevenueCatState(customerInfo as any, 'restore');
}
@@ -681,10 +715,10 @@ export default function BillingScreen() {
style={[
styles.hardPaywallCta,
compactPaywall && styles.hardPaywallCtaCompact,
(!storeReady || isUpdating) && styles.disabledPlanCard,
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
]}
onPress={() => handlePurchase(selectedProductId)}
disabled={isUpdating || !storeReady}
disabled={isUpdating || !storeReady || Boolean(storeError)}
activeOpacity={0.9}
>
{isUpdating || !storeReady ? (
@@ -699,6 +733,9 @@ export default function BillingScreen() {
)}
</TouchableOpacity>
{storeError ? (
<Text style={styles.hardPaywallFooter}>{storeError}</Text>
) : null}
<Text style={styles.hardPaywallFooter}>{paywallFooterLabel}</Text>
<View style={styles.legalLinksRow}>
<TouchableOpacity onPress={() => Linking.openURL('https://greenlenspro.com/privacy')}>
@@ -778,7 +815,7 @@ export default function BillingScreen() {
<TouchableOpacity
style={[styles.paywallPlanCardPrimary, { borderColor: colors.primary, backgroundColor: colors.primary + '12' }]}
onPress={() => handlePurchase('yearly_pro')}
disabled={isUpdating || !storeReady}
disabled={isUpdating || !storeReady || Boolean(storeError)}
activeOpacity={0.9}
>
<View style={styles.planTopRow}>
@@ -805,10 +842,10 @@ export default function BillingScreen() {
style={[
styles.paywallPlanCardSecondary,
{ borderColor: colors.border },
(!storeReady || isUpdating) && styles.disabledPlanCard,
(!storeReady || isUpdating || Boolean(storeError)) && styles.disabledPlanCard,
]}
onPress={() => handlePurchase('monthly_pro')}
disabled={isUpdating || !storeReady}
disabled={isUpdating || !storeReady || Boolean(storeError)}
activeOpacity={0.9}
>
<View style={styles.planTopRow}>
@@ -876,7 +913,7 @@ export default function BillingScreen() {
}
]}
onPress={() => handlePurchase(pack.id)}
disabled={isUpdating || !storeReady}
disabled={isUpdating || !storeReady || Boolean(storeError)}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Ionicons name="flash" size={18} color={colors.primary} />
@@ -953,7 +990,7 @@ export default function BillingScreen() {
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
]}
onPress={() => handlePurchase('monthly_pro')}
disabled={isUpdating || !storeReady}
disabled={isUpdating || !storeReady || Boolean(storeError)}
>
<View style={{ flex: 1 }}>
<View style={styles.planHeaderRow}>
@@ -984,7 +1021,7 @@ export default function BillingScreen() {
planId === 'pro' && { borderColor: colors.primary, backgroundColor: colors.primary + '10' }
]}
onPress={() => handlePurchase('yearly_pro')}
disabled={isUpdating || !storeReady}
disabled={isUpdating || !storeReady || Boolean(storeError)}
>
<View style={{ flex: 1 }}>
<View style={styles.planHeaderRow}>
@@ -1070,8 +1107,8 @@ export default function BillingScreen() {
<TouchableOpacity
style={styles.offerDeclineBtn}
onPress={finalizeCancel}
disabled={isUpdating || !storeReady}
>
disabled={isUpdating}
>
<Text style={[styles.offerDeclineBtnText, { color: colors.textMuted }]}>{copy.offerDecline}</Text>
</TouchableOpacity>
</View>

View File

@@ -11,7 +11,7 @@ import * as ImageManipulator from 'expo-image-manipulator';
import * as Haptics from 'expo-haptics';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { usePostHog } from 'posthog-react-native';
import { useSafeAnalytics } from '../services/analytics';
import { useApp } from '../context/AppContext';
import { useColors } from '../constants/Colors';
import { PlantRecognitionService } from '../services/plantRecognitionService';
@@ -130,7 +130,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
export default function ScannerScreen() {
const params = useLocalSearchParams<{ mode?: string; plantId?: string; sharedImageUri?: string }>();
const posthog = usePostHog();
const posthog = useSafeAnalytics();
const {
isDarkMode,
colorPalette,