import React, { useMemo, useState, useRef, useEffect } from 'react'; import { ActivityIndicator, Alert, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View, Dimensions, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; 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 { useApp } from '../../context/AppContext'; import { useColors } from '../../constants/Colors'; import { ThemeBackdrop } from '../../components/ThemeBackdrop'; import { SafeImage } from '../../components/SafeImage'; import { Plant } from '../../types'; import { useCoachMarks } from '../../context/CoachMarksContext'; import { OnboardingProgressService } from '../../services/onboardingProgressService'; const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window'); type FilterKey = 'all' | 'today' | 'week' | 'healthy' | 'dormant'; const DAY_MS = 24 * 60 * 60 * 1000; const CONTENT_BOTTOM_PADDING = 12; const FAB_BOTTOM_OFFSET = 16; type OnboardingStepId = 'scan' | 'reminder' | 'lexicon' | 'theme' | 'collection'; function OnboardingChecklist({ plants, appearanceMode, colorPalette, lexiconExplored, customizationDone, colors, router, t, getLexiconSearchHistory, registerLayout, posthog, }: { plants: Plant[]; appearanceMode: string; colorPalette: string; lexiconExplored: boolean; customizationDone: boolean; colors: any; router: any; t: any; getLexiconSearchHistory: () => string[]; registerLayout: (key: string, layout: { x: number; y: number; width: number; height: number }) => void; posthog: any; }) { const cardRef = useRef(null); const previousProgressRef = useRef(null); const lexiconHistoryCount = getLexiconSearchHistory().length; const plantsCount = plants.length; const reminderReady = plants.some((plant) => Boolean(plant.notificationsEnabled)); const themeCustomized = customizationDone || appearanceMode !== 'system' || colorPalette !== 'forest'; const lexiconCompleted = lexiconExplored || lexiconHistoryCount > 0; const checklist = [ { id: 'scan' as const, label: t.stepScan, completed: plantsCount > 0, icon: 'camera-outline' }, { id: 'reminder' as const, label: t.stepReminder, completed: reminderReady, icon: 'notifications-outline' }, { id: 'lexicon' as const, label: t.stepLexicon, completed: lexiconCompleted, icon: 'search-outline' }, { id: 'theme' as const, label: t.stepTheme, completed: themeCustomized, icon: 'color-palette-outline' }, { id: 'collection' as const, label: t.stepCollection, completed: plantsCount >= 3, icon: 'albums-outline' }, ]; const completedCount = checklist.filter((item) => item.completed).length; const progressRatio = completedCount / checklist.length; const progressWidth: `${number}%` = completedCount === 0 ? '0%' : `${Math.max(progressRatio * 100, 12)}%`; const nextItem = checklist.find((item) => !item.completed) ?? null; useEffect(() => { if (previousProgressRef.current === completedCount) return; previousProgressRef.current = completedCount; posthog.capture('onboarding_progress_updated', { completed_count: completedCount, total_count: checklist.length, next_step_id: nextItem?.id ?? 'complete', }); }, [completedCount, checklist.length, nextItem?.id, posthog]); if (completedCount === checklist.length) { return null; } const navigateToStep = (stepId: OnboardingStepId) => { posthog.capture('onboarding_step_opened', { step_id: stepId, completed_count: completedCount, }); if (stepId === 'scan' || stepId === 'collection') { router.push('/scanner'); return; } if (stepId === 'reminder') { if (plants[0]?.id) { router.push(`/plant/${plants[0].id}`); } else { router.push('/scanner'); } return; } if (stepId === 'lexicon') { router.push('/lexicon'); return; } router.push('/onboarding/customize'); }; return ( { cardRef.current?.measureInWindow((x, y, width, height) => { registerLayout('onboarding_checklist', { x, y, width, height }); }); }} > {t.onboardingChecklistIntro} {t.onboardingChecklistTitle} {(nextItem ? t.onboardingChecklistNextLabel : t.onboardingChecklistDone).replace('{0}', nextItem?.label ?? '')} {t.onboardingChecklistProgress .replace('{0}', completedCount.toString()) .replace('{1}', checklist.length.toString())} {checklist.map((item) => ( navigateToStep(item.id)} disabled={item.completed} > {item.label} {!item.completed && } ))} ); } const getDaysUntilWatering = (plant: Plant): number => { const lastWateredTs = new Date(plant.lastWatered).getTime(); if (Number.isNaN(lastWateredTs)) return 0; const dueTs = lastWateredTs + (plant.careInfo.waterIntervalDays * DAY_MS); const remainingMs = dueTs - Date.now(); if (remainingMs <= 0) return 0; return Math.ceil(remainingMs / DAY_MS); }; export default function HomeScreen() { const { session, plants, isLoadingPlants, profileImageUri, profileName, billingSummary, isLoadingBilling, t, isDarkMode, appearanceMode, colorPalette, getLexiconSearchHistory, } = useApp(); const colors = useColors(isDarkMode, colorPalette); const router = useRouter(); const insets = useSafeAreaInsets(); const [activeFilter, setActiveFilter] = useState('all'); const [onboardingSignals, setOnboardingSignals] = useState({ lexiconExplored: false, customizationDone: false, }); const { layouts, registerLayout, startTour } = useCoachMarks(); const fabRef = useRef(null); const tourStartRequestedRef = useRef(false); const posthog = usePostHog(); useFocusEffect( React.useCallback(() => { if (!session?.userId) { setOnboardingSignals({ lexiconExplored: false, customizationDone: false, }); return; } setOnboardingSignals(OnboardingProgressService.getSignals(session.userId)); }, [session?.userId]), ); // Tour nach Registrierung starten useEffect(() => { let cancelled = false; let retryTimer: ReturnType | null = null; const tourSteps = [ { elementKey: 'fab', title: t.tourFabTitle, description: t.tourFabDesc, tooltipSide: 'above' as const, }, { elementKey: 'tab_search', title: t.tourSearchTitle, description: t.tourSearchDesc, tooltipSide: 'above' as const, }, { elementKey: 'tab_profile', title: t.tourProfileTitle, description: t.tourProfileDesc, tooltipSide: 'above' as const, }, { elementKey: 'onboarding_checklist', title: t.tourChecklistTitle, description: t.tourChecklistDesc, tooltipSide: 'below' as const, }, ]; const registerTabLayouts = () => { const tabBarBottom = SCREEN_H - 85; const tabW = SCREEN_W / 3; registerLayout('tab_search', { x: tabW, y: tabBarBottom + 8, width: tabW, height: 52 }); registerLayout('tab_profile', { x: tabW * 2, y: tabBarBottom + 8, width: tabW, height: 52 }); }; const checkTour = async () => { if (tourStartRequestedRef.current) return; const flag = await AsyncStorage.getItem('greenlens_show_tour'); if (flag !== 'true') return; tourStartRequestedRef.current = true; const startWhenReady = async (attempt = 0) => { if (cancelled) return; registerTabLayouts(); if (!layouts.fab && attempt < 10) { retryTimer = setTimeout(() => startWhenReady(attempt + 1), 250); return; } startTour(tourSteps); await AsyncStorage.removeItem('greenlens_show_tour'); tourStartRequestedRef.current = false; }; retryTimer = setTimeout(() => startWhenReady(), 1000); }; checkTour(); return () => { cancelled = true; if (retryTimer) { clearTimeout(retryTimer); } }; }, [layouts, registerLayout, startTour, t.tourChecklistDesc, t.tourChecklistTitle, t.tourFabDesc, t.tourFabTitle, t.tourProfileDesc, t.tourProfileTitle, t.tourSearchDesc, t.tourSearchTitle]); const copy = t; const greetingText = useMemo(() => { const hour = new Date().getHours(); if (hour < 12) return copy.greetingMorning; if (hour < 18) return copy.greetingAfternoon; return copy.greetingEvening; }, [copy.greetingAfternoon, copy.greetingEvening, copy.greetingMorning]); const creditsText = useMemo(() => { if (isLoadingBilling && !billingSummary) { return '...'; } if (!billingSummary) { return `-- ${copy.creditsLabel}`; } return `${billingSummary.credits.available} ${copy.creditsLabel}`; }, [billingSummary, copy.creditsLabel, isLoadingBilling]); const thirstyCount = useMemo( () => plants.filter(plant => getDaysUntilWatering(plant) === 0).length, [plants] ); const dueTodayPlants = useMemo( () => plants.filter(plant => getDaysUntilWatering(plant) === 0), [plants] ); const filteredPlants = useMemo(() => { return plants.filter((plant) => { if (activeFilter === 'all') return true; const daysUntil = getDaysUntilWatering(plant); if (activeFilter === 'today') return daysUntil === 0; if (activeFilter === 'week') return daysUntil <= 7; if (activeFilter === 'healthy') return daysUntil >= 2; return plant.careInfo.waterIntervalDays >= 14; }); }, [plants, activeFilter]); const chips: Array<{ key: FilterKey; label: string }> = [ { key: 'all', label: copy.all }, { key: 'today', label: copy.today }, { key: 'week', label: copy.week }, { key: 'healthy', label: copy.healthy }, { key: 'dormant', label: copy.dormant }, ]; const handleBellPress = () => { setActiveFilter('today'); if (dueTodayPlants.length === 0) { Alert.alert(copy.reminderTitle, copy.reminderNone); return; } const previewNames = dueTodayPlants .slice(0, 6) .map((plant) => `- ${plant.name}`) .join('\n'); const remainingCount = dueTodayPlants.length - 6; const remainingText = remainingCount > 0 ? `\n+ ${remainingCount} ${copy.more}` : ''; Alert.alert( copy.reminderTitle, `${copy.reminderDue.replace('{0}', dueTodayPlants.length.toString())}\n\n${previewNames}${remainingText}` ); }; if (isLoadingPlants) { return ( ); } return ( {profileImageUri ? ( ) : ( )} {greetingText} {profileName || ''} {creditsText} {copy.needsWaterToday} {copy.plantsThirsty.replace('{0}', thirstyCount.toString())} {copy.viewSchedule} {chips.map(chip => ( setActiveFilter(chip.key)} activeOpacity={0.85} > {chip.label} ))} {copy.collectionTitle} {copy.collectionCount.replace('{0}', filteredPlants.length.toString())} {filteredPlants.length === 0 ? ( {plants.length === 0 ? copy.emptyCollectionTitle : copy.noneInFilter} {plants.length === 0 ? copy.emptyCollectionHint : copy.noneInFilter} {plants.length === 0 && ( router.push('/scanner')} activeOpacity={0.86} > {copy.scanFirstPlant} )} ) : ( filteredPlants.map((plant) => { const daysUntil = getDaysUntilWatering(plant); const thirsty = daysUntil === 0; const nextWaterText = thirsty ? copy.today : t.inXDays.replace('{0}', daysUntil.toString()); return ( router.push(`/plant/${plant.id}`)} > {plant.name} {plant.botanicalName} {thirsty ? copy.thirsty : copy.healthyStatus} {copy.nextWaterLabel}: {nextWaterText} ); }) )} router.push('/scanner')} onLayout={() => { fabRef.current?.measureInWindow((x, y, width, height) => { registerLayout('fab', { x, y, width, height }); }); }} > ); } const styles = StyleSheet.create({ container: { flex: 1, }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, content: { paddingHorizontal: 24, paddingTop: 14, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 26, }, headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1, }, headerTextBlock: { flex: 1, minWidth: 0, }, avatarWrap: { width: 48, height: 48, borderRadius: 16, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, avatarImage: { width: '100%', height: '100%', }, greetingText: { fontSize: 13, fontWeight: '600', }, nameRow: { marginTop: 1, flexDirection: 'row', alignItems: 'center', gap: 8, minWidth: 0, }, nameText: { fontSize: 21, fontWeight: '700', letterSpacing: 0.1, flexShrink: 1, }, creditsPill: { borderRadius: 999, borderWidth: 1, paddingHorizontal: 8, paddingVertical: 3, }, creditsText: { fontSize: 10, fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', }, bellBtn: { width: 46, height: 46, borderRadius: 16, borderWidth: 1, alignItems: 'center', justifyContent: 'center', shadowOpacity: 0.08, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, priorityCard: { borderRadius: 32, padding: 24, marginBottom: 20, overflow: 'hidden', }, priorityLabelRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 8, }, priorityLabel: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.9, }, priorityTitle: { fontSize: 29, fontWeight: '700', lineHeight: 36, maxWidth: 260, marginBottom: 14, }, priorityButton: { alignSelf: 'flex-start', borderRadius: 16, borderWidth: 1, paddingHorizontal: 16, paddingVertical: 10, flexDirection: 'row', alignItems: 'center', gap: 6, }, priorityButtonText: { fontSize: 13, fontWeight: '700', }, priorityBgIcon: { position: 'absolute', right: -18, bottom: -22, }, filterRow: { gap: 10, paddingVertical: 2, paddingRight: 4, marginBottom: 16, }, collectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, }, collectionTitle: { fontSize: 19, fontWeight: '700', letterSpacing: 0.2, }, collectionCountPill: { borderRadius: 999, borderWidth: 1, paddingHorizontal: 10, paddingVertical: 4, }, collectionCountText: { fontSize: 11, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.4, }, filterChip: { paddingHorizontal: 22, paddingVertical: 10, borderRadius: 16, borderWidth: 1, }, filterChipActive: { paddingHorizontal: 22, paddingVertical: 10, borderRadius: 16, shadowOpacity: 0.24, shadowRadius: 10, shadowOffset: { width: 0, height: 3 }, elevation: 3, }, filterText: { fontSize: 13, fontWeight: '700', }, filterTextActive: { fontSize: 13, fontWeight: '700', }, plantCard: { borderRadius: 30, borderWidth: 1, padding: 15, flexDirection: 'row', alignItems: 'flex-start', gap: 14, marginBottom: 14, shadowOpacity: 0.1, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 3, }, plantImageWrap: { borderWidth: 1, borderRadius: 20, overflow: 'hidden', }, plantImage: { width: 98, height: 98, borderRadius: 20, }, plantBody: { flex: 1, paddingTop: 2, }, plantHeadRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 8, gap: 8, }, plantTitleCol: { flex: 1, minWidth: 0, }, plantName: { fontWeight: '700', fontSize: 19, letterSpacing: 0.15, }, botanicalName: { fontSize: 12.5, fontStyle: 'italic', fontWeight: '500', marginTop: 2, opacity: 0.95, }, metaRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap', }, statusThirsty: { borderRadius: 20, paddingHorizontal: 10, paddingVertical: 4, }, statusHealthy: { borderRadius: 20, paddingHorizontal: 10, paddingVertical: 4, }, statusThirstyText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4, }, statusHealthyText: { fontSize: 10, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4, }, waterMeta: { fontSize: 10, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.4, }, nextWaterPill: { borderRadius: 999, borderWidth: 1, paddingHorizontal: 9, paddingVertical: 4, flexDirection: 'row', alignItems: 'center', gap: 4, }, emptyState: { borderRadius: 24, borderWidth: 1, paddingVertical: 32, paddingHorizontal: 22, alignItems: 'center', gap: 10, }, emptyIconWrap: { width: 56, height: 56, borderRadius: 18, alignItems: 'center', justifyContent: 'center', }, emptyTitle: { fontSize: 18, fontWeight: '700', textAlign: 'center', }, emptyText: { fontSize: 13, fontWeight: '500', textAlign: 'center', lineHeight: 20, }, emptyCta: { marginTop: 8, borderRadius: 999, paddingHorizontal: 16, paddingVertical: 10, flexDirection: 'row', alignItems: 'center', gap: 8, shadowOpacity: 0.3, shadowRadius: 10, shadowOffset: { width: 0, height: 4 }, elevation: 4, }, emptyCtaText: { fontSize: 13, fontWeight: '700', }, fab: { position: 'absolute', right: 24, width: 66, height: 66, borderRadius: 33, borderWidth: 4, alignItems: 'center', justifyContent: 'center', shadowOpacity: 0.38, shadowRadius: 14, shadowOffset: { width: 0, height: 5 }, elevation: 9, }, checklistCard: { borderRadius: 24, borderWidth: 1, padding: 20, marginBottom: 20, }, checklistHeader: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, marginBottom: 14, }, checklistHeaderCopy: { flex: 1, gap: 4, }, checklistEyebrow: { fontSize: 12, fontWeight: '700', letterSpacing: 0.4, textTransform: 'uppercase', }, checklistTitle: { fontSize: 16, fontWeight: '700', }, checklistSubtitle: { fontSize: 13, lineHeight: 18, }, checklistProgressPill: { borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6, }, checklistProgressText: { fontSize: 12, fontWeight: '700', }, progressTrack: { height: 8, borderRadius: 999, overflow: 'hidden', marginBottom: 16, }, progressFill: { height: '100%', borderRadius: 999, }, checklistGrid: { gap: 12, }, checklistItem: { flexDirection: 'row', alignItems: 'center', gap: 12, }, checkIcon: { width: 32, height: 32, borderRadius: 16, justifyContent: 'center', alignItems: 'center', }, checklistText: { flex: 1, fontSize: 14, fontWeight: '500', }, });