128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
// app/_layout.tsx
|
|
import { Stack, router } from 'expo-router';
|
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { View, ActivityIndicator, AppState, AppStateStatus } from 'react-native';
|
|
import { useUserStore } from '@/store/useUserStore';
|
|
import { getDatabase } from '@/lib/database';
|
|
import { isPinEnabled } from '@/lib/pinService';
|
|
import { ToastProvider } from '@/components/ui/Toast';
|
|
import '../global.css';
|
|
|
|
const LOCK_AFTER_SECONDS = 60;
|
|
|
|
export default function RootLayout() {
|
|
const [loading, setLoading] = useState(true);
|
|
const setUser = useUserStore((s) => s.setUser);
|
|
|
|
// Track when app was backgrounded
|
|
const backgroundedAt = useRef<number | null>(null);
|
|
const appState = useRef<AppStateStatus>(AppState.currentState);
|
|
|
|
useEffect(() => {
|
|
async function bootstrap() {
|
|
try {
|
|
const db = await getDatabase();
|
|
const user = await db.getFirstAsync<{
|
|
id: string;
|
|
display_name: string;
|
|
share_id: string;
|
|
}>('SELECT id, display_name, share_id FROM users WHERE is_self = 1 LIMIT 1');
|
|
if (user) {
|
|
setUser({
|
|
id: user.id,
|
|
displayName: user.display_name,
|
|
shareId: user.share_id,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('Bootstrap error:', e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
bootstrap();
|
|
}, []);
|
|
|
|
// PIN lock on resume
|
|
useEffect(() => {
|
|
const subscription = AppState.addEventListener(
|
|
'change',
|
|
async (nextState: AppStateStatus) => {
|
|
const prev = appState.current;
|
|
|
|
if (
|
|
(prev === 'active' || prev === 'inactive') &&
|
|
nextState === 'background'
|
|
) {
|
|
// App going to background — record timestamp
|
|
backgroundedAt.current = Date.now();
|
|
}
|
|
|
|
if (
|
|
(prev === 'background' || prev === 'inactive') &&
|
|
nextState === 'active'
|
|
) {
|
|
// App coming to foreground
|
|
const bgAt = backgroundedAt.current;
|
|
if (bgAt !== null) {
|
|
const elapsedSec = (Date.now() - bgAt) / 1000;
|
|
if (elapsedSec >= LOCK_AFTER_SECONDS) {
|
|
const pinOn = await isPinEnabled();
|
|
if (pinOn) {
|
|
router.replace('/lock');
|
|
}
|
|
}
|
|
}
|
|
backgroundedAt.current = null;
|
|
}
|
|
|
|
appState.current = nextState;
|
|
}
|
|
);
|
|
|
|
return () => subscription.remove();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#F8F4EF',
|
|
}}
|
|
>
|
|
<ActivityIndicator size="large" color="#1A6B72" />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
<ToastProvider>
|
|
<Stack screenOptions={{ headerShown: false }}>
|
|
<Stack.Screen name="onboarding" />
|
|
<Stack.Screen name="(tabs)" />
|
|
<Stack.Screen
|
|
name="lock"
|
|
options={{
|
|
presentation: 'fullScreenModal',
|
|
animation: 'fade',
|
|
gestureEnabled: false,
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="sync-progress"
|
|
options={{
|
|
presentation: 'fullScreenModal',
|
|
animation: 'slide_from_bottom',
|
|
}}
|
|
/>
|
|
</Stack>
|
|
</ToastProvider>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|