feat: implement authentication flow and session management
- Update RootLayout to handle authentication state and redirect users based on their auth status. - Create an Index component to redirect unauthenticated users to the login page. - Modify ApiAuthRepository to fetch user session data using access and refresh tokens. - Introduce a new container file to manage use case instances for authentication and filament operations. - Enhance authStore to validate tokens and fetch user data from the API. - Add babel-plugin-module-resolver for improved module imports. - Update package.json scripts for running the app on Android and iOS. - Add expo-camera dependency for camera functionalities. - Update tsconfig.json to include infrastructure path mapping.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Tabs, Redirect } from 'expo-router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Tabs, Redirect, useRouter } from 'expo-router';
|
||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
import { useFilamentStore } from '@store/filamentStore';
|
||||
import { usePresetStore } from '@store/presetStore';
|
||||
import { listFilamentsUseCase, listPresetsUseCase } from '@infrastructure/container';
|
||||
import { colors, radius, spacing } from '@shared/theme';
|
||||
|
||||
/**
|
||||
@@ -10,7 +13,27 @@ import { colors, radius, spacing } from '@shared/theme';
|
||||
* 4 abas + FAB central: Início | Estoque | [+] | Config | Perfil
|
||||
*/
|
||||
export default function AppLayout(): React.ReactElement {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const { isAuthenticated, user } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const { setFilaments, setLoading, setError } = useFilamentStore();
|
||||
const { setPresets } = usePresetStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
listFilamentsUseCase.execute(user.id),
|
||||
listPresetsUseCase.execute(user.id),
|
||||
]).then(([filaments, presets]) => {
|
||||
setFilaments(filaments);
|
||||
setPresets(presets);
|
||||
}).catch((err) => {
|
||||
console.error('bootstrap error', err);
|
||||
setError('Não foi possível carregar os dados.');
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [user, setFilaments, setPresets, setLoading, setError]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Redirect href="/(auth)/login" />;
|
||||
@@ -54,7 +77,12 @@ export default function AppLayout(): React.ReactElement {
|
||||
</View>
|
||||
),
|
||||
tabBarButton: (props) => (
|
||||
<TouchableOpacity {...props} style={styles.fabWrapper} activeOpacity={0.8} />
|
||||
<TouchableOpacity
|
||||
{...props}
|
||||
style={styles.fabWrapper}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => router.push('/(app)/inventory/new')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Screen } from '@presentation/components/layout/Screen';
|
||||
|
||||
/**
|
||||
* Tab "add" — apenas redireciona para o formulário de novo filamento.
|
||||
* O FAB central da tab bar chama esta rota.
|
||||
* Tab "add" — tela placeholder; a navegação é interceptada pelo FAB no _layout.
|
||||
*/
|
||||
export default function AddTab(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
router.replace('/(app)/inventory/new');
|
||||
}, [router]);
|
||||
|
||||
return <Screen />;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function HomeScreen(): React.ReactElement {
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.qrBtn}
|
||||
onPress={() => router.push('/(app)/qrcode/scan')}
|
||||
onPress={() => router.push('/(app)/scanner' as never)}
|
||||
>
|
||||
<Ionicons name="qr-code-outline" size={22} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { useFilamentStore } from '@store/filamentStore';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
@@ -23,19 +24,18 @@ export default function QRCodeScreen(): React.ReactElement {
|
||||
? `${filament.brand.toLowerCase()}-${(filament.model ?? '').toLowerCase()}-${filament.colorHex.replace('#', '').toLowerCase()}`.replace(/\s+/g, '-')
|
||||
: 'desconhecido';
|
||||
|
||||
const deepLink = `meowspool.app/f/${slug}`;
|
||||
const deepLink = `meowspool://filament/${id}`;
|
||||
|
||||
async function handleShare(): Promise<void> {
|
||||
try {
|
||||
await Share.share({ message: `meowspool://filament/${id}`, url: `https://${deepLink}` });
|
||||
await Share.share({ message: deepLink });
|
||||
} catch {
|
||||
// cancelled
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyLink(): Promise<void> {
|
||||
// expo-clipboard not installed — show alert as placeholder
|
||||
Alert.alert('Link copiado', deepLink);
|
||||
Alert.alert('Link', deepLink);
|
||||
}
|
||||
|
||||
if (!filament) {
|
||||
@@ -75,10 +75,12 @@ export default function QRCodeScreen(): React.ReactElement {
|
||||
{/* QR Code area */}
|
||||
<View style={styles.qrContainer}>
|
||||
<View style={styles.qrCard}>
|
||||
{/* Placeholder QR — real impl would use react-native-qrcode-svg */}
|
||||
<View style={styles.qrPlaceholder}>
|
||||
<Ionicons name="qr-code" size={160} color={colors.black} />
|
||||
</View>
|
||||
<QRCode
|
||||
value={deepLink}
|
||||
size={200}
|
||||
backgroundColor="#F5EFE0"
|
||||
color="#1E1B18"
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.qrHint}>Aponte a câmera para escanear</Text>
|
||||
<View style={styles.linkBadge}>
|
||||
@@ -149,12 +151,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.xl,
|
||||
padding: spacing[6],
|
||||
},
|
||||
qrPlaceholder: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
qrHint: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.sm,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert,
|
||||
} from 'react-native';
|
||||
@@ -10,6 +10,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { usePresetStore } from '@store/presetStore';
|
||||
import { useFilamentStore } from '@store/filamentStore';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
import { Input } from '@presentation/components/ui/Input';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { Card } from '@presentation/components/ui/Card';
|
||||
@@ -17,6 +18,7 @@ import { calcNetWeight } from '@domain/Filament';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import { MATERIALS, type Material } from '@shared/constants';
|
||||
import { formatWeight } from '@shared/utils/filament';
|
||||
import { createFilamentUseCase } from '@infrastructure/container';
|
||||
|
||||
const schema = z.object({
|
||||
brand: z.string().min(1, 'Marca obrigatória'),
|
||||
@@ -39,6 +41,7 @@ export default function NewFilamentScreen(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const { presets, systemPresets } = usePresetStore();
|
||||
const { addFilament } = useFilamentStore();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [selectedColor, setSelectedColor] = useState('#E05533');
|
||||
const [hexInput, setHexInput] = useState('#E05533');
|
||||
@@ -46,6 +49,13 @@ export default function NewFilamentScreen(): React.ReactElement {
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(systemPresets[0]?.id ?? null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Quando os presets carregam (via layout), seleciona o primeiro sistema automaticamente
|
||||
useEffect(() => {
|
||||
if (selectedPresetId === null && presets.length > 0) {
|
||||
setSelectedPresetId(presets.find((p) => p.isSystem)?.id ?? presets[0].id);
|
||||
}
|
||||
}, [presets, selectedPresetId]);
|
||||
|
||||
const { control, handleSubmit, watch, formState: { errors } } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { brand: '', model: '', totalWeightG: 0, tempHotendC: 210, tempBedC: 60, flowFactorPct: 100 },
|
||||
@@ -63,12 +73,28 @@ export default function NewFilamentScreen(): React.ReactElement {
|
||||
Alert.alert('Atenção', 'Selecione um preset de carretel.');
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
Alert.alert('Erro', 'Usuário não autenticado.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: CreateFilamentUseCase via container DI
|
||||
console.log('create filament', { ...data, material: selectedMaterial, colorHex: selectedColor, spoolPresetId: selectedPresetId });
|
||||
const filament = await createFilamentUseCase.execute(user.id, {
|
||||
material: selectedMaterial,
|
||||
brand: data.brand,
|
||||
model: data.model ?? null,
|
||||
colorHex: selectedColor,
|
||||
spoolPresetId: selectedPresetId,
|
||||
totalWeightG: data.totalWeightG,
|
||||
tempHotendC: data.tempHotendC ?? null,
|
||||
tempBedC: data.tempBedC ?? null,
|
||||
flowFactorPct: data.flowFactorPct ?? null,
|
||||
notes: data.notes ?? null,
|
||||
});
|
||||
addFilament(filament);
|
||||
router.back();
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error('create filament error', err);
|
||||
Alert.alert('Erro', 'Não foi possível salvar o filamento.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import { colors, typography, spacing } from '@shared/theme';
|
||||
|
||||
/**
|
||||
* Tela de scanner de QR Code
|
||||
* Lê deeplinks meowspool://filament/<id> e navega para o filamento.
|
||||
*/
|
||||
export default function ScannerScreen(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const handledRef = useRef(false);
|
||||
|
||||
function handleBarCodeScanned({ data }: { data: string }): void {
|
||||
if (handledRef.current) return;
|
||||
handledRef.current = true;
|
||||
|
||||
const match = data.match(/meowspool:\/\/filament\/([^/]+)/);
|
||||
if (match) {
|
||||
router.replace(`/(app)/inventory/${match[1]}` as never);
|
||||
} else {
|
||||
// QR não reconhecido — libera para próximo scan
|
||||
handledRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!permission) {
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.message}>Carregando câmera…</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (!permission.granted) {
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<View style={styles.center}>
|
||||
<Ionicons name="camera-outline" size={48} color={colors.textSecondary} />
|
||||
<Text style={styles.message}>Permissão de câmera necessária</Text>
|
||||
<TouchableOpacity style={styles.permBtn} onPress={requestPermission}>
|
||||
<Text style={styles.permBtnText}>Conceder permissão</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Escanear QR Code</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
{/* Camera */}
|
||||
<View style={styles.cameraWrapper}>
|
||||
<CameraView
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
facing="back"
|
||||
mode="picture"
|
||||
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
|
||||
onBarcodeScanned={handleBarCodeScanned}
|
||||
/>
|
||||
|
||||
{/* Viewfinder overlay */}
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.viewfinder}>
|
||||
<View style={[styles.corner, styles.cornerTL]} />
|
||||
<View style={[styles.corner, styles.cornerTR]} />
|
||||
<View style={[styles.corner, styles.cornerBL]} />
|
||||
<View style={[styles.corner, styles.cornerBR]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.hint}>
|
||||
<Text style={styles.hintText}>
|
||||
Aponte para o QR Code do filamento
|
||||
</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const CORNER = 24;
|
||||
const CORNER_THICKNESS = 3;
|
||||
const VIEWFINDER_SIZE = 240;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, backgroundColor: colors.bgBase },
|
||||
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing[4], paddingHorizontal: spacing[6] },
|
||||
message: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
permBtn: {
|
||||
backgroundColor: colors.accent,
|
||||
paddingHorizontal: spacing[5],
|
||||
paddingVertical: spacing[3],
|
||||
borderRadius: 8,
|
||||
},
|
||||
permBtnText: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.bgBase,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing[5],
|
||||
paddingVertical: spacing[4],
|
||||
},
|
||||
headerTitle: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.md,
|
||||
fontWeight: '600',
|
||||
color: colors.textPrimary,
|
||||
},
|
||||
cameraWrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.55)',
|
||||
},
|
||||
viewfinder: {
|
||||
width: VIEWFINDER_SIZE,
|
||||
height: VIEWFINDER_SIZE,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
corner: {
|
||||
position: 'absolute',
|
||||
width: CORNER,
|
||||
height: CORNER,
|
||||
borderColor: colors.accent,
|
||||
},
|
||||
cornerTL: { top: 0, left: 0, borderTopWidth: CORNER_THICKNESS, borderLeftWidth: CORNER_THICKNESS },
|
||||
cornerTR: { top: 0, right: 0, borderTopWidth: CORNER_THICKNESS, borderRightWidth: CORNER_THICKNESS },
|
||||
cornerBL: { bottom: 0, left: 0, borderBottomWidth: CORNER_THICKNESS, borderLeftWidth: CORNER_THICKNESS },
|
||||
cornerBR: { bottom: 0, right: 0, borderBottomWidth: CORNER_THICKNESS, borderRightWidth: CORNER_THICKNESS },
|
||||
hint: {
|
||||
paddingVertical: spacing[5],
|
||||
paddingHorizontal: spacing[5],
|
||||
alignItems: 'center',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
hintText: {
|
||||
fontFamily: typography.fontFamily.ui,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { Header } from '@presentation/components/layout/Header';
|
||||
import { Input } from '@presentation/components/ui/Input';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { colors, typography, spacing } from '@shared/theme';
|
||||
import { forgotPasswordUseCase } from '@infrastructure/container';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('E-mail inválido'),
|
||||
@@ -31,8 +32,7 @@ export default function ForgotPasswordScreen(): React.ReactElement {
|
||||
async function onSubmit(data: FormData): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: ForgotPasswordUseCase
|
||||
console.log('forgot-password', data);
|
||||
await forgotPasswordUseCase.execute(data.email);
|
||||
setSent(true);
|
||||
} catch {
|
||||
Alert.alert('Erro', 'Não foi possível enviar o e-mail.');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { Link, useRouter } from 'expo-router';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -9,6 +9,8 @@ import { Screen } from '@presentation/components/layout/Screen';
|
||||
import { Input } from '@presentation/components/ui/Input';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { colors, typography, spacing } from '@shared/theme';
|
||||
import { loginUseCase } from '@infrastructure/container';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('E-mail inválido'),
|
||||
@@ -23,6 +25,7 @@ type FormData = z.infer<typeof schema>;
|
||||
*/
|
||||
export default function LoginScreen(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const setSession = useAuthStore((s) => s.setSession);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
|
||||
@@ -34,10 +37,11 @@ export default function LoginScreen(): React.ReactElement {
|
||||
async function onSubmit(data: FormData): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: injetar LoginUseCase via container de DI
|
||||
console.log('login', data);
|
||||
const session = await loginUseCase.execute(data);
|
||||
await setSession(session);
|
||||
router.replace('/(app)/(tabs)/home');
|
||||
} catch (err) {
|
||||
console.error('login error', err);
|
||||
Alert.alert('Erro', 'E-mail ou senha incorretos.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Header } from '@presentation/components/layout/Header';
|
||||
import { Input } from '@presentation/components/ui/Input';
|
||||
import { Button } from '@presentation/components/ui/Button';
|
||||
import { colors, typography, spacing } from '@shared/theme';
|
||||
import { registerUseCase } from '@infrastructure/container';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('E-mail inválido'),
|
||||
@@ -29,6 +31,7 @@ type FormData = z.infer<typeof schema>;
|
||||
*/
|
||||
export default function RegisterScreen(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const setSession = useAuthStore((s) => s.setSession);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
|
||||
@@ -39,8 +42,8 @@ export default function RegisterScreen(): React.ReactElement {
|
||||
async function onSubmit(data: FormData): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// TODO: injetar RegisterUseCase
|
||||
console.log('register', data);
|
||||
const session = await registerUseCase.execute({ email: data.email, password: data.password });
|
||||
await setSession(session);
|
||||
router.replace('/(auth)/verify-email');
|
||||
} catch {
|
||||
Alert.alert('Erro', 'Não foi possível criar sua conta.');
|
||||
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack } from 'expo-router';
|
||||
import { Stack, useRouter, useSegments } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useAuthStore } from '@store/authStore';
|
||||
|
||||
@@ -8,18 +8,31 @@ import { useAuthStore } from '@store/authStore';
|
||||
* Carrega a sessão armazenada e controla o fluxo auth vs app.
|
||||
*/
|
||||
export default function RootLayout(): React.ReactElement | null {
|
||||
const { loadStoredSession, isLoading } = useAuthStore();
|
||||
const { loadStoredSession, isLoading, isAuthenticated } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const segments = useSegments();
|
||||
|
||||
useEffect(() => {
|
||||
loadStoredSession();
|
||||
}, [loadStoredSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
const inAuthGroup = segments[0] === '(auth)';
|
||||
if (isAuthenticated && inAuthGroup) {
|
||||
router.replace('/(app)/(tabs)/home');
|
||||
} else if (!isAuthenticated && !inAuthGroup) {
|
||||
router.replace('/(auth)/login');
|
||||
}
|
||||
}, [isAuthenticated, isLoading, segments, router]);
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusBar style="light" backgroundColor="transparent" translucent />
|
||||
<Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: '#1E1B18' } }}>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="(auth)" />
|
||||
<Stack.Screen name="(app)" />
|
||||
</Stack>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Redirect } from 'expo-router';
|
||||
|
||||
export default function Index() {
|
||||
return <Redirect href="/(auth)/login" />;
|
||||
}
|
||||
Reference in New Issue
Block a user