- Replace all Ionicons with native SF Symbols via expo-symbols SymbolView - Create reusable Icon wrapper component (src/shared/components/Icon.tsx) - Remove @expo/vector-icons and lucide-react dependencies - Refactor explore tab with filters, search, and category browsing - Add collections and programs data with Supabase integration - Add explore filter store and filter sheet - Update i18n strings (en, de, es, fr) for new explore features - Update test mocks and remove stale snapshots - Add user fitness level to user store and types
197 lines
5.0 KiB
TypeScript
197 lines
5.0 KiB
TypeScript
/**
|
|
* TabataFit Root Layout
|
|
* Expo Router v3 + Inter font loading
|
|
* Waits for font + store hydration before rendering
|
|
*/
|
|
|
|
import '@/src/shared/i18n'
|
|
import '@/src/shared/i18n/types'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { Stack } from 'expo-router'
|
|
import { StatusBar } from 'expo-status-bar'
|
|
import { View } from 'react-native'
|
|
import * as SplashScreen from 'expo-splash-screen'
|
|
import * as Notifications from 'expo-notifications'
|
|
import {
|
|
useFonts,
|
|
Inter_400Regular,
|
|
Inter_500Medium,
|
|
Inter_600SemiBold,
|
|
Inter_700Bold,
|
|
Inter_900Black,
|
|
} from '@expo-google-fonts/inter'
|
|
|
|
import { PostHogProvider } from 'posthog-react-native'
|
|
|
|
import { ThemeProvider, useThemeColors } from '@/src/shared/theme'
|
|
import { useUserStore } from '@/src/shared/stores'
|
|
import { useNotifications } from '@/src/shared/hooks'
|
|
import { initializePurchases } from '@/src/shared/services/purchases'
|
|
import { initializeAnalytics, getPostHogClient, trackScreen } from '@/src/shared/services/analytics'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
Notifications.setNotificationHandler({
|
|
handleNotification: async () => ({
|
|
shouldShowAlert: false,
|
|
shouldPlaySound: false,
|
|
shouldSetBadge: false,
|
|
shouldShowBanner: false,
|
|
shouldShowList: false,
|
|
}),
|
|
})
|
|
|
|
SplashScreen.preventAutoHideAsync()
|
|
|
|
// Create React Query Client
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
|
retry: 2,
|
|
refetchOnWindowFocus: false,
|
|
},
|
|
},
|
|
})
|
|
|
|
function RootLayoutInner() {
|
|
const colors = useThemeColors()
|
|
|
|
const [fontsLoaded] = useFonts({
|
|
Inter_400Regular,
|
|
Inter_500Medium,
|
|
Inter_600SemiBold,
|
|
Inter_700Bold,
|
|
Inter_900Black,
|
|
})
|
|
|
|
useNotifications()
|
|
|
|
// Wait for persisted store to hydrate from AsyncStorage
|
|
const [hydrated, setHydrated] = useState(useUserStore.persist.hasHydrated())
|
|
|
|
useEffect(() => {
|
|
const unsub = useUserStore.persist.onFinishHydration(() => setHydrated(true))
|
|
return unsub
|
|
}, [])
|
|
|
|
// Initialize RevenueCat + PostHog after hydration
|
|
useEffect(() => {
|
|
if (hydrated) {
|
|
initializePurchases().catch((err) => {
|
|
console.error('Failed to initialize RevenueCat:', err)
|
|
})
|
|
initializeAnalytics().catch((err) => {
|
|
console.error('Failed to initialize PostHog:', err)
|
|
})
|
|
}
|
|
}, [hydrated])
|
|
|
|
const onLayoutRootView = useCallback(async () => {
|
|
if (fontsLoaded && hydrated) {
|
|
await SplashScreen.hideAsync()
|
|
}
|
|
}, [fontsLoaded, hydrated])
|
|
|
|
if (!fontsLoaded || !hydrated) {
|
|
return null
|
|
}
|
|
|
|
const content = (
|
|
<QueryClientProvider client={queryClient}>
|
|
<View style={{ flex: 1, backgroundColor: colors.bg.base }} onLayout={onLayoutRootView}>
|
|
<StatusBar style={colors.statusBarStyle} />
|
|
<Stack
|
|
screenOptions={{
|
|
headerShown: false,
|
|
contentStyle: { backgroundColor: colors.bg.base },
|
|
animation: 'slide_from_right',
|
|
}}
|
|
>
|
|
<Stack.Screen name="(tabs)" />
|
|
<Stack.Screen
|
|
name="onboarding"
|
|
options={{
|
|
animation: 'fade',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="workout/[id]"
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="workout/category/[id]"
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="collection/[id]"
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="assessment"
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="player/[id]"
|
|
options={{
|
|
presentation: 'fullScreenModal',
|
|
animation: 'fade',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="complete/[id]"
|
|
options={{
|
|
animation: 'fade',
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="explore-filters"
|
|
options={{
|
|
presentation: 'formSheet',
|
|
headerShown: false,
|
|
sheetGrabberVisible: true,
|
|
sheetAllowedDetents: [0.5],
|
|
}}
|
|
/>
|
|
</Stack>
|
|
</View>
|
|
</QueryClientProvider>
|
|
)
|
|
|
|
const posthogClient = getPostHogClient()
|
|
|
|
// Only wrap with PostHogProvider if client is initialized
|
|
if (!posthogClient) {
|
|
return content
|
|
}
|
|
|
|
return (
|
|
<PostHogProvider
|
|
client={posthogClient}
|
|
autocapture={{
|
|
captureScreens: true,
|
|
captureTouches: true,
|
|
}}
|
|
>
|
|
{content}
|
|
</PostHogProvider>
|
|
)
|
|
}
|
|
|
|
export default function RootLayout() {
|
|
return (
|
|
<ThemeProvider>
|
|
<RootLayoutInner />
|
|
</ThemeProvider>
|
|
)
|
|
}
|