feat: integrate theme and i18n across all screens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
168
app/_layout.tsx
168
app/_layout.tsx
@@ -1,13 +1,18 @@
|
||||
/**
|
||||
* TabataFit Root Layout
|
||||
* Expo Router v3 + Inter font loading
|
||||
* Waits for font + store hydration before rendering
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
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,
|
||||
@@ -17,11 +22,29 @@ import {
|
||||
Inter_900Black,
|
||||
} from '@expo-google-fonts/inter'
|
||||
|
||||
import { DARK } from '@/src/shared/constants/colors'
|
||||
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 } from '@/src/shared/services/analytics'
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: false,
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
shouldShowBanner: false,
|
||||
shouldShowList: false,
|
||||
}),
|
||||
})
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
export default function RootLayout() {
|
||||
function RootLayoutInner() {
|
||||
const colors = useThemeColors()
|
||||
|
||||
const [fontsLoaded] = useFonts({
|
||||
Inter_400Regular,
|
||||
Inter_500Medium,
|
||||
@@ -30,59 +53,106 @@ export default function RootLayout() {
|
||||
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) {
|
||||
if (fontsLoaded && hydrated) {
|
||||
await SplashScreen.hideAsync()
|
||||
}
|
||||
}, [fontsLoaded])
|
||||
}, [fontsLoaded, hydrated])
|
||||
|
||||
if (!fontsLoaded) {
|
||||
if (!fontsLoaded || !hydrated) {
|
||||
return null
|
||||
}
|
||||
|
||||
const content = (
|
||||
<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_bottom',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="workout/category/[id]"
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="collection/[id]"
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="player/[id]"
|
||||
options={{
|
||||
presentation: 'fullScreenModal',
|
||||
animation: 'fade',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="complete/[id]"
|
||||
options={{
|
||||
animation: 'fade',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
)
|
||||
|
||||
// Skip PostHogProvider in dev to avoid SDK errors without a real API key
|
||||
if (__DEV__) {
|
||||
return content
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: DARK.BASE }} onLayout={onLayoutRootView}>
|
||||
<StatusBar style="light" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
contentStyle: { backgroundColor: DARK.BASE },
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen
|
||||
name="workout/[id]"
|
||||
options={{
|
||||
animation: 'slide_from_bottom',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="workout/category/[id]"
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="collection/[id]"
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="player/[id]"
|
||||
options={{
|
||||
presentation: 'fullScreenModal',
|
||||
animation: 'fade',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="complete/[id]"
|
||||
options={{
|
||||
animation: 'fade',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</View>
|
||||
<PostHogProvider client={getPostHogClient() ?? undefined} autocapture={{ captureScreens: true }}>
|
||||
{content}
|
||||
</PostHogProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<RootLayoutInner />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user