/**
* TabataFit Root Layout
* Expo Router v3 — SF Pro system font (no custom font loading)
* Waits for store hydration before rendering
*/
import '@/src/shared/i18n'
import '@/src/shared/i18n/types'
import { useState, useEffect, useCallback, Component } from 'react'
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import { View, Text, Pressable, StyleSheet } from 'react-native'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import { PostHogProvider } from 'posthog-react-native'
import { ThemeProvider, useThemeColors } from '@/src/shared/theme'
import { TEXT, NAVY, GREEN } from '@/src/shared/constants/colors'
import { useUserStore } from '@/src/shared/stores'
import { useNotifications } from '@/src/shared/hooks'
import { OfflineBanner } from '@/src/shared/components/OfflineBanner'
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()
// ─── Error Boundary (F-108) ────────────────────────────────────────────────
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<{ children: React.ReactNode }, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, info.componentStack)
}
private handleRetry = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
return (
⚠️
Something went wrong
{this.state.error?.message ?? 'An unexpected error occurred.'}
Try again
)
}
return this.props.children
}
}
const errorStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: NAVY[900],
alignItems: 'center',
justifyContent: 'center',
padding: 32,
},
emoji: { fontSize: 48, marginBottom: 16 },
title: {
fontSize: 22,
fontWeight: '700',
color: TEXT.PRIMARY,
marginBottom: 8,
},
message: {
fontSize: 15,
fontWeight: '400',
color: TEXT.SECONDARY,
textAlign: 'center',
marginBottom: 24,
lineHeight: 20,
},
button: {
backgroundColor: '#00C896',
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
borderCurve: 'continuous',
minHeight: 44,
},
buttonText: {
fontSize: 17,
fontWeight: '600',
color: NAVY[900],
},
})
// 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()
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 (hydrated) {
await SplashScreen.hideAsync()
}
}, [hydrated])
if (!hydrated) {
return null
}
const content = (
)
const posthogClient = getPostHogClient()
// Only wrap with PostHogProvider if client is initialized
if (!posthogClient) {
return content
}
return (
{content}
)
}
export default function RootLayout() {
return (
)
}