frontend/auth: add auth screens, login, sign up and reset password
Parents:
ae44a258 file(s) changed
- frontend/app/(tabs)/_layout.tsx +23 -2
- frontend/app/(tabs)/index.tsx +28 -2
- frontend/app/_layout.tsx +17 -7
- frontend/app/auth/_layout.tsx +31 -0
- frontend/app/auth/forgot-password.tsx +251 -0
- frontend/app/auth/login.tsx +172 -0
- frontend/app/auth/reset-password.tsx +184 -0
- frontend/app/auth/signup.tsx +214 -0
frontend/app/(tabs)/_layout.tsx
@@ -1,13 +1,34 @@
1 - import { Tabs } from 'expo-router';
2 - import React from 'react';
1 + import { Tabs, Redirect, router } from 'expo-router';
2 + import React, { useEffect } from 'react';
3 + import { View, ActivityIndicator } from 'react-native';
3 4
4 5 import { HapticTab } from '@/components/misc/haptic-tab';
5 6 import { IconSymbol } from '@/components/ui/icon-symbol';
6 7 import { Colors } from '@/constants/theme';
7 8 import { useColorScheme } from '@/hooks/use-color-scheme';
9 + import { useAuth } from '@/context/AuthContext';
8 10
9 11 export default function TabLayout() {
10 12 const colorScheme = useColorScheme();
13 + const { isAuthenticated, isLoading } = useAuth();
14 +
15 + useEffect(() => {
16 + if (!isLoading && !isAuthenticated) {
17 + router.replace('/auth/login');
18 + }
19 + }, [isAuthenticated, isLoading]);
20 +
21 + if (isLoading) {
22 + return (
23 + <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
24 + <ActivityIndicator size="large" />
25 + </View>
26 + );
27 + }
28 +
29 + if (!isAuthenticated) {
30 + return <Redirect href="/auth/login" />;
31 + }
11 32
12 33 return (
13 34 <Tabs
frontend/app/(tabs)/index.tsx
@@ -1,13 +1,32 @@
1 1 import { Image } from 'expo-image';
2 - import { Platform, StyleSheet } from 'react-native';
2 + import { Platform, StyleSheet, TouchableOpacity, Alert } from 'react-native';
3 3
4 4 import { HelloWave } from '@/components/hello-wave';
5 5 import ParallaxScrollView from '@/components/misc/parallax-scroll-view';
6 6 import { ThemedText } from '@/components/misc/themed-text';
7 7 import { ThemedView } from '@/components/misc/themed-view';
8 8 import { Link } from 'expo-router';
9 + import { useAuth } from '@/context/AuthContext';
10 + import { Button } from '@/components/ui';
9 11
10 12 export default function HomeScreen() {
13 + const { user, logout } = useAuth();
14 +
15 + const handleLogout = async () => {
16 + Alert.alert(
17 + 'Sair',
18 + 'Tem certeza que quer sair?',
19 + [
20 + { text: 'Cancelar', style: 'cancel' },
21 + {
22 + text: 'Sair',
23 + style: 'destructive',
24 + onPress: logout
25 + },
26 + ]
27 + );
28 + };
29 +
11 30 return (
12 31 <ParallaxScrollView
13 32 headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
@@ -18,8 +37,15 @@ style={styles.reactLogo}
18 37 />
19 38 }>
20 39 <ThemedView style={styles.titleContainer}>
21 - <ThemedText type="title">Welcome!</ThemedText>
40 + <ThemedText type="title">Bem vindo{user?.firstName ? `, ${user.firstName}` : ''}!</ThemedText>
22 41 <HelloWave />
42 + </ThemedView>
43 + <ThemedView style={styles.stepContainer}>
44 + <Button
45 + title="Sair"
46 + variant="outline"
47 + onPress={handleLogout}
48 + />
23 49 </ThemedView>
24 50 <ThemedView style={styles.stepContainer}>
25 51 <ThemedText type="subtitle">Step 1: Try it</ThemedText>
frontend/app/_layout.tsx
@@ -4,6 +4,7 @@ import { StatusBar } from 'expo-status-bar';
4 4 import 'react-native-reanimated';
5 5
6 6 import { useColorScheme } from '@/hooks/use-color-scheme';
7 + import { AuthProvider } from '@/context/AuthContext';
7 8
8 9 export const unstable_settings = {
9 10 anchor: '(tabs)',
@@ -13,12 +14,21 @@ export default function RootLayout() {
13 14 const colorScheme = useColorScheme();
14 15
15 16 return (
16 - <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
17 - <Stack>
18 - <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
19 - <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
20 - </Stack>
21 - <StatusBar style="auto" />
22 - </ThemeProvider>
17 + <AuthProvider>
18 + <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
19 + <Stack>
20 + <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
21 + <Stack.Screen
22 + name="auth"
23 + options={{
24 + headerShown: false,
25 + presentation: 'modal'
26 + }}
27 + />
28 + <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
29 + </Stack>
30 + <StatusBar style="auto" />
31 + </ThemeProvider>
32 + </AuthProvider>
23 33 );
24 34 }
frontend/app/auth/_layout.tsx
@@ -0,0 +1,32 @@
1 + import { Stack } from 'expo-router';
2 +
3 + export default function AuthLayout() {
4 + return (
5 + <Stack>
6 + <Stack.Screen
7 + name="login"
8 + options={{
9 + headerShown: false,
10 + }}
11 + />
12 + <Stack.Screen
13 + name="signup"
14 + options={{
15 + headerShown: false,
16 + }}
17 + />
18 + <Stack.Screen
19 + name="forgot-password"
20 + options={{
21 + headerShown: false,
22 + }}
23 + />
24 + <Stack.Screen
25 + name="reset-password"
26 + options={{
27 + headerShown: false,
28 + }}
29 + />
30 + </Stack>
31 + );
32 + }
frontend/app/auth/forgot-password.tsx
@@ -0,0 +1,251 @@
1 + import React, { useState } from 'react';
2 + import {
3 + View,
4 + Text,
5 + StyleSheet,
6 + ScrollView,
7 + KeyboardAvoidingView,
8 + Platform,
9 + Alert,
10 + } from 'react-native';
11 + import { Link, router } from 'expo-router';
12 + import { SafeAreaView } from 'react-native-safe-area-context';
13 + import { useThemeColor } from '@/hooks/use-theme-color';
14 + import { Button, Input } from '@/components/ui';
15 + import { useAuth } from '@/context/AuthContext';
16 +
17 + export default function ForgotPasswordScreen() {
18 + const [email, setEmail] = useState('');
19 + const [emailSent, setEmailSent] = useState(false);
20 + const [resetToken, setResetToken] = useState(''); // Only for development
21 + const [errors, setErrors] = useState<{ email?: string }>({});
22 +
23 + const { forgotPassword } = useAuth();
24 + const [loading, setLoading] = useState(false);
25 + const textColor = useThemeColor({}, 'text');
26 + const backgroundColor = useThemeColor({}, 'background');
27 + const tintColor = useThemeColor({}, 'tint');
28 +
29 + const validateForm = () => {
30 + const newErrors: { email?: string } = {};
31 +
32 + if (!email) {
33 + newErrors.email = 'Email is required';
34 + } else if (!/\S+@\S+\.\S+/.test(email)) {
35 + newErrors.email = 'Please enter a valid email';
36 + }
37 +
38 + setErrors(newErrors);
39 + return Object.keys(newErrors).length === 0;
40 + };
41 +
42 + const handleForgotPassword = async () => {
43 + if (!validateForm()) return;
44 +
45 + setLoading(true);
46 + try {
47 + const data = await forgotPassword(email);
48 + setEmailSent(true);
49 + // In development, show the reset token
50 + if (data.token) {
51 + setResetToken(data.token);
52 + }
53 + } catch (error: any) {
54 + Alert.alert('Erro: ', error.message || 'Falha ao enviar link de recuperação');
55 + } finally {
56 + setLoading(false);
57 + }
58 + };
59 +
60 + const handleUseToken = () => {
61 + if (resetToken) {
62 + router.push({
63 + pathname: '/auth/reset-password',
64 + params: { token: resetToken },
65 + });
66 + }
67 + };
68 +
69 + if (emailSent) {
70 + return (
71 + <SafeAreaView style={[styles.container, { backgroundColor }]}>
72 + <View style={styles.successContainer}>
73 + <Text style={[styles.title, { color: textColor }]}>Verifique seu email</Text>
74 + <Text style={[styles.message, { color: textColor, opacity: 0.7 }]}>
75 + Enviamos as instruções para resetar sua senha para {email}
76 + </Text>
77 +
78 + {/* Development only - show reset token */}
79 + {resetToken && (
80 + <View style={styles.devSection}>
81 + <Text style={[styles.devTitle, { color: tintColor }]}>
82 + Dev mode:
83 + </Text>
84 + <Text style={[styles.devText, { color: textColor, opacity: 0.7 }]}>
85 + Token: {resetToken}
86 + </Text>
87 + <Button
88 + title="Usar token para resetar senha"
89 + onPress={handleUseToken}
90 + style={styles.devButton}
91 + />
92 + </View>
93 + )}
94 +
95 + <View style={styles.actions}>
96 + <Button
97 + title="Reenviar email"
98 + variant="outline"
99 + onPress={() => setEmailSent(false)}
100 + style={styles.resendButton}
101 + />
102 +
103 + <Link href="/auth/login" asChild style={[styles.link, { color: tintColor }]}>
104 + <Text style={[styles.link, { color: tintColor }]}>
105 + Voltar para login
106 + </Text>
107 + </Link>
108 + </View>
109 + </View>
110 + </SafeAreaView>
111 + );
112 + }
113 +
114 + return (
115 + <SafeAreaView style={[styles.container, { backgroundColor }]}>
116 + <KeyboardAvoidingView
117 + behavior='height'
118 + style={styles.keyboardView}
119 + >
120 + <ScrollView
121 + contentContainerStyle={styles.scrollContainer}
122 + showsVerticalScrollIndicator={false}
123 + >
124 + <View style={styles.header}>
125 + <Text style={[styles.title, { color: textColor }]}>Esqueceu a senha?</Text>
126 + <Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
127 + Insira seu email para receber as instruções de recuperação
128 + </Text>
129 + </View>
130 +
131 + <View style={styles.form}>
132 + <Input
133 + label="Email"
134 + type="email"
135 + value={email}
136 + onChangeText={setEmail}
137 + placeholder="Qual seu email?"
138 + error={errors.email}
139 + autoCapitalize="none"
140 + />
141 +
142 + <Button
143 + title="Enviar link de recuperação"
144 + onPress={handleForgotPassword}
145 + loading={loading}
146 + style={styles.resetButton}
147 + />
148 + </View>
149 +
150 + <View style={styles.footer}>
151 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
152 + Lembra tua senha?{' '}
153 + </Text>
154 + <Link href="/auth/login" asChild style={[styles.link, { color: tintColor }]}>
155 + <Text style={[styles.link, { color: tintColor }]}>
156 + Faça login
157 + </Text>
158 + </Link>
159 + </View>
160 + </ScrollView>
161 + </KeyboardAvoidingView>
162 + </SafeAreaView>
163 + );
164 + }
165 +
166 + const styles = StyleSheet.create({
167 + container: {
168 + flex: 1,
169 + },
170 + keyboardView: {
171 + flex: 1,
172 + },
173 + scrollContainer: {
174 + flexGrow: 1,
175 + paddingHorizontal: 24,
176 + justifyContent: 'center',
177 + minHeight: '100%',
178 + },
179 + successContainer: {
180 + flex: 1,
181 + justifyContent: 'center',
182 + paddingHorizontal: 24,
183 + },
184 + header: {
185 + alignItems: 'center',
186 + marginBottom: 40,
187 + },
188 + title: {
189 + fontSize: 28,
190 + fontWeight: 'bold',
191 + marginBottom: 8,
192 + textAlign: 'center',
193 + },
194 + subtitle: {
195 + fontSize: 16,
196 + textAlign: 'center',
197 + lineHeight: 24,
198 + },
199 + message: {
200 + fontSize: 16,
201 + textAlign: 'center',
202 + lineHeight: 24,
203 + marginBottom: 32,
204 + },
205 + form: {
206 + marginBottom: 32,
207 + },
208 + resetButton: {
209 + marginTop: 8,
210 + },
211 + actions: {
212 + gap: 16,
213 + },
214 + resendButton: {
215 + marginBottom: 8,
216 + },
217 + devSection: {
218 + marginBottom: 32,
219 + padding: 16,
220 + borderRadius: 8,
221 + backgroundColor: 'rgba(74, 144, 226, 0.1)',
222 + },
223 + devTitle: {
224 + fontSize: 14,
225 + fontWeight: '600',
226 + marginBottom: 8,
227 + },
228 + devText: {
229 + fontSize: 12,
230 + marginBottom: 12,
231 + fontFamily: 'monospace',
232 + },
233 + devButton: {
234 + marginTop: 8,
235 + },
236 + link: {
237 + textAlign: 'center',
238 + fontSize: 14,
239 + fontWeight: '500',
240 + },
241 + footer: {
242 + flexDirection: 'row',
243 + justifyContent: 'center',
244 + alignItems: 'center',
245 + marginTop: 'auto',
246 + paddingBottom: 20,
247 + },
248 + footerText: {
249 + fontSize: 14,
250 + },
251 + });
frontend/app/auth/login.tsx
@@ -0,0 +1,172 @@
1 + import React, { useState } from 'react';
2 + import {
3 + View,
4 + Text,
5 + StyleSheet,
6 + ScrollView,
7 + KeyboardAvoidingView,
8 + Platform,
9 + Alert,
10 + } from 'react-native';
11 + import { Link, router } from 'expo-router';
12 + import { SafeAreaView } from 'react-native-safe-area-context';
13 + import { useThemeColor } from '@/hooks/use-theme-color';
14 + import { Button, Input } from '@/components/ui';
15 + import { useAuth } from '@/context/AuthContext';
16 +
17 + export default function LoginScreen() {
18 + const [email, setEmail] = useState('');
19 + const [password, setPassword] = useState('');
20 + const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
21 +
22 + const { login, isLoading } = useAuth();
23 + const textColor = useThemeColor({}, 'text');
24 + const backgroundColor = useThemeColor({}, 'background');
25 + const tintColor = useThemeColor({}, 'tint');
26 +
27 + const validateForm = () => {
28 + const newErrors: { email?: string; password?: string } = {};
29 +
30 + if (!email) {
31 + newErrors.email = 'Email é obrigatório';
32 + } else if (!/\S+@\S+\.\S+/.test(email)) {
33 + newErrors.email = 'Por favor, use um email válido';
34 + }
35 +
36 + if (!password) {
37 + newErrors.password = 'Senha é obrigatória';
38 + }
39 +
40 + setErrors(newErrors);
41 + return Object.keys(newErrors).length === 0;
42 + };
43 +
44 + const handleLogin = async () => {
45 + if (!validateForm()) return;
46 +
47 + try {
48 + await login(email, password);
49 + router.replace('/(tabs)');
50 + } catch (error: any) {
51 + Alert.alert('Erro: ', error.message || 'Falha ao fazer login');
52 + }
53 + };
54 +
55 + return (
56 + <SafeAreaView style={[styles.container, { backgroundColor }]}>
57 + <KeyboardAvoidingView
58 + behavior='height'
59 + style={styles.keyboardView}
60 + >
61 + <ScrollView
62 + contentContainerStyle={styles.scrollContainer}
63 + showsVerticalScrollIndicator={false}
64 + >
65 + <View style={styles.header}>
66 + <Text style={[styles.title, { color: textColor }]}>Bem vindo de volta</Text>
67 + <Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
68 + Logue na sua conta
69 + </Text>
70 + </View>
71 +
72 + <View style={styles.form}>
73 + <Input
74 + label="Email"
75 + type="email"
76 + value={email}
77 + onChangeText={setEmail}
78 + placeholder="Qual seu email?"
79 + error={errors.email}
80 + autoCapitalize="none"
81 + />
82 +
83 + <Input
84 + label="Senha"
85 + type="password"
86 + value={password}
87 + onChangeText={setPassword}
88 + placeholder="Qual sua senha?"
89 + error={errors.password}
90 + showPasswordToggle
91 + />
92 +
93 + <Button
94 + title="Entrar"
95 + onPress={handleLogin}
96 + loading={isLoading}
97 + style={styles.loginButton}
98 + />
99 +
100 + <Link href="/auth/forgot-password" style={[styles.link, {color: tintColor }]} asChild>
101 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
102 + Esqueceu a senha?
103 + </Text>
104 + </Link>
105 + </View>
106 +
107 + <View style={styles.footer}>
108 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
109 + Não tem uma conta ainda?{' '}
110 + </Text>
111 +
112 + <Link href="/auth/signup" asChild style={[styles.link, { color: tintColor }]}>
113 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
114 + Cadastre-se
115 + </Text>
116 + </Link>
117 + </View>
118 + </ScrollView>
119 + </KeyboardAvoidingView>
120 + </SafeAreaView>
121 + );
122 + }
123 +
124 + const styles = StyleSheet.create({
125 + container: {
126 + flex: 1,
127 + },
128 + keyboardView: {
129 + flex: 1,
130 + },
131 + scrollContainer: {
132 + flexGrow: 1,
133 + paddingHorizontal: 24,
134 + justifyContent: 'center',
135 + minHeight: '100%',
136 + },
137 + header: {
138 + alignItems: 'center',
139 + marginBottom: 40,
140 + },
141 + title: {
142 + fontSize: 28,
143 + fontWeight: 'bold',
144 + marginBottom: 8,
145 + },
146 + subtitle: {
147 + fontSize: 16,
148 + textAlign: 'center',
149 + },
150 + form: {
151 + marginBottom: 32,
152 + },
153 + loginButton: {
154 + marginTop: 8,
155 + marginBottom: 16,
156 + },
157 + link: {
158 + textAlign: 'center',
159 + fontSize: 14,
160 + fontWeight: '500',
161 + },
162 + footer: {
163 + flexDirection: 'row',
164 + justifyContent: 'center',
165 + alignItems: 'center',
166 + marginTop: 'auto',
167 + paddingBottom: 20,
168 + },
169 + footerText: {
170 + fontSize: 14,
171 + },
172 + });
frontend/app/auth/reset-password.tsx
@@ -0,0 +1,184 @@
1 + import React, { useState } from 'react';
2 + import {
3 + View,
4 + Text,
5 + StyleSheet,
6 + ScrollView,
7 + KeyboardAvoidingView,
8 + Platform,
9 + Alert,
10 + } from 'react-native';
11 + import { Link, useLocalSearchParams, router } from 'expo-router';
12 + import { SafeAreaView } from 'react-native-safe-area-context';
13 + import { useThemeColor } from '@/hooks/use-theme-color';
14 + import { Button, Input } from '@/components/ui';
15 + import { useAuth } from '@/context/AuthContext';
16 +
17 + export default function ResetPasswordScreen() {
18 + const { token } = useLocalSearchParams<{ token: string }>();
19 + const [password, setPassword] = useState('');
20 + const [confirmPassword, setConfirmPassword] = useState('');
21 + const [errors, setErrors] = useState<{
22 + password?: string;
23 + confirmPassword?: string;
24 + }>({});
25 +
26 + const { resetPassword } = useAuth();
27 + const [loading, setLoading] = useState(false);
28 + const textColor = useThemeColor({}, 'text');
29 + const backgroundColor = useThemeColor({}, 'background');
30 + const tintColor = useThemeColor({}, 'tint');
31 +
32 + const validateForm = () => {
33 + const newErrors: typeof errors = {};
34 +
35 + if (!password) {
36 + newErrors.password = 'A senha é obrigatória';
37 + } else if (password.length < 6) {
38 + newErrors.password = 'A senha deve conter pelo menos 6 caracteres';
39 + }
40 +
41 + if (!confirmPassword) {
42 + newErrors.confirmPassword = 'Por favor confirme sua senha';
43 + } else if (password !== confirmPassword) {
44 + newErrors.confirmPassword = 'As senhas não coincidem';
45 + }
46 +
47 + setErrors(newErrors);
48 + return Object.keys(newErrors).length === 0;
49 + };
50 +
51 + const handleResetPassword = async () => {
52 + if (!validateForm()) return;
53 +
54 + if (!token) {
55 + Alert.alert('Erro: ', 'Token de reset não encontrado. Por favor, solicite um novo link de recuperação.');
56 + return;
57 + }
58 +
59 + setLoading(true);
60 + try {
61 + await resetPassword(token, password);
62 + Alert.alert('Sucesso!', 'Senha recuperada com sucesso', [
63 + {
64 + text: 'OK',
65 + onPress: () => router.replace('/auth/login'),
66 + },
67 + ]);
68 + } catch (error: any) {
69 + Alert.alert('Erro: ', error.message || 'Falha em recuperar a conta');
70 + } finally {
71 + setLoading(false);
72 + }
73 + };
74 +
75 + return (
76 + <SafeAreaView style={[styles.container, { backgroundColor }]}>
77 + <KeyboardAvoidingView
78 + behavior='height'
79 + style={styles.keyboardView}
80 + >
81 + <ScrollView
82 + contentContainerStyle={styles.scrollContainer}
83 + showsVerticalScrollIndicator={false}
84 + >
85 + <View style={styles.header}>
86 + <Text style={[styles.title, { color: textColor }]}>Recuperar a conta</Text>
87 + <Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
88 + Digite uma nova senha para sua conta
89 + </Text>
90 + </View>
91 +
92 + <View style={styles.form}>
93 + <Input
94 + label="Nova senha"
95 + type="password"
96 + value={password}
97 + onChangeText={setPassword}
98 + placeholder="Digite uma nova senha"
99 + error={errors.password}
100 + showPasswordToggle
101 + />
102 +
103 + <Input
104 + label="Confirmar nova senha"
105 + type="password"
106 + value={confirmPassword}
107 + onChangeText={setConfirmPassword}
108 + placeholder="Confirme a nova senha"
109 + error={errors.confirmPassword}
110 + showPasswordToggle
111 + />
112 +
113 + <Button
114 + title="Trocar senha"
115 + onPress={handleResetPassword}
116 + loading={loading}
117 + style={styles.resetButton}
118 + />
119 + </View>
120 +
121 + <View style={styles.footer}>
122 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
123 + Lembrou da senha?{' '}
124 + </Text>
125 + <Link href="/auth/login" asChild style={[styles.link, { color: tintColor }]}>
126 + <Text style={[styles.link, { color: tintColor }]}>
127 + Faça login
128 + </Text>
129 + </Link>
130 + </View>
131 + </ScrollView>
132 + </KeyboardAvoidingView>
133 + </SafeAreaView>
134 + );
135 + }
136 +
137 + const styles = StyleSheet.create({
138 + container: {
139 + flex: 1,
140 + },
141 + keyboardView: {
142 + flex: 1,
143 + },
144 + scrollContainer: {
145 + flexGrow: 1,
146 + paddingHorizontal: 24,
147 + justifyContent: 'center',
148 + minHeight: '100%',
149 + },
150 + header: {
151 + alignItems: 'center',
152 + marginBottom: 40,
153 + },
154 + title: {
155 + fontSize: 28,
156 + fontWeight: 'bold',
157 + marginBottom: 8,
158 + },
159 + subtitle: {
160 + fontSize: 16,
161 + textAlign: 'center',
162 + },
163 + form: {
164 + marginBottom: 32,
165 + },
166 + resetButton: {
167 + marginTop: 8,
168 + },
169 + link: {
170 + textAlign: 'center',
171 + fontSize: 14,
172 + fontWeight: '500',
173 + },
174 + footer: {
175 + flexDirection: 'row',
176 + justifyContent: 'center',
177 + alignItems: 'center',
178 + marginTop: 'auto',
179 + paddingBottom: 20,
180 + },
181 + footerText: {
182 + fontSize: 14,
183 + },
184 + });
frontend/app/auth/signup.tsx
@@ -0,0 +1,214 @@
1 + import React, { useState } from 'react';
2 + import {
3 + View,
4 + Text,
5 + StyleSheet,
6 + ScrollView,
7 + KeyboardAvoidingView,
8 + Platform,
9 + Alert,
10 + } from 'react-native';
11 + import { Link, router } from 'expo-router';
12 + import { SafeAreaView } from 'react-native-safe-area-context';
13 + import { useThemeColor } from '@/hooks/use-theme-color';
14 + import { Button, Input } from '@/components/ui';
15 + import { useAuth } from '@/context/AuthContext';
16 +
17 + export default function SignupScreen() {
18 + const [firstName, setFirstName] = useState('');
19 + const [lastName, setLastName] = useState('');
20 + const [email, setEmail] = useState('');
21 + const [password, setPassword] = useState('');
22 + const [confirmPassword, setConfirmPassword] = useState('');
23 + const [errors, setErrors] = useState<{
24 + firstName?: string;
25 + email?: string;
26 + password?: string;
27 + confirmPassword?: string;
28 + }>({});
29 +
30 + const { signup, isLoading } = useAuth();
31 + const textColor = useThemeColor({}, 'text');
32 + const backgroundColor = useThemeColor({}, 'background');
33 + const tintColor = useThemeColor({}, 'tint');
34 +
35 + const validateForm = () => {
36 + const newErrors: typeof errors = {};
37 +
38 + if (!firstName.trim()) {
39 + newErrors.firstName = 'O nome é obrigatório';
40 + }
41 +
42 + if (!email) {
43 + newErrors.email = 'Email é obrigatório';
44 + } else if (!/\S+@\S+\.\S+/.test(email)) {
45 + newErrors.email = 'Por favor entre um email válido';
46 + }
47 +
48 + if (!password) {
49 + newErrors.password = 'Senha é obrigatório';
50 + } else if (password.length < 6) {
51 + newErrors.password = 'A senha deve conter pelo menos 6 caracteres';
52 + }
53 +
54 + if (!confirmPassword) {
55 + newErrors.confirmPassword = 'Por favor confirme sua senha';
56 + } else if (password !== confirmPassword) {
57 + newErrors.confirmPassword = 'As senhas não coincidem';
58 + }
59 +
60 + setErrors(newErrors);
61 + return Object.keys(newErrors).length === 0;
62 + };
63 +
64 + const handleSignup = async () => {
65 + if (!validateForm()) return;
66 +
67 + try {
68 + await signup({
69 + first_name: firstName,
70 + last_name: lastName || undefined,
71 + email,
72 + password,
73 + });
74 + router.replace('/(tabs)');
75 + } catch (error: any) {
76 + Alert.alert('Erro:', error.message || 'Falha no cadastro. Tente novamente.');
77 + }
78 + };
79 +
80 + return (
81 + <SafeAreaView style={[styles.container, { backgroundColor }]}>
82 + <KeyboardAvoidingView
83 + behavior='height'
84 + style={styles.keyboardView}
85 + >
86 + <ScrollView
87 + contentContainerStyle={styles.scrollContainer}
88 + showsVerticalScrollIndicator={false}
89 + >
90 + <View style={styles.header}>
91 + <Text style={[styles.title, { color: textColor }]}>Criar conta</Text>
92 + <Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
93 + Cadastre-se para começar a monitorar o seu humor.
94 + </Text>
95 + </View>
96 +
97 + <View style={styles.form}>
98 + <Input
99 + label="Nome"
100 + value={firstName}
101 + onChangeText={setFirstName}
102 + placeholder="Digite seu nome"
103 + error={errors.firstName}
104 + />
105 +
106 + <Input
107 + label="Sobrenome (opcional)"
108 + value={lastName}
109 + onChangeText={setLastName}
110 + placeholder="Digite seu sobrenome"
111 + />
112 +
113 + <Input
114 + label="Email"
115 + type="email"
116 + value={email}
117 + onChangeText={setEmail}
118 + placeholder="Qual seu email?"
119 + error={errors.email}
120 + autoCapitalize="none"
121 + />
122 +
123 + <Input
124 + label="Password"
125 + type="password"
126 + value={password}
127 + onChangeText={setPassword}
128 + placeholder="Escolha uma senha (min. 6 caracteres)"
129 + error={errors.password}
130 + showPasswordToggle
131 + />
132 +
133 + <Input
134 + label="Confirme a senha"
135 + type="password"
136 + value={confirmPassword}
137 + onChangeText={setConfirmPassword}
138 + placeholder="Confirme sua senha"
139 + error={errors.confirmPassword}
140 + showPasswordToggle
141 + />
142 +
143 + <Button
144 + title="Criar conta"
145 + onPress={handleSignup}
146 + loading={isLoading}
147 + style={styles.signupButton}
148 + />
149 + </View>
150 +
151 + <View style={styles.footer}>
152 + <Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
153 + Já tem uma conta?{' '}
154 + </Text>
155 + <Link href="/auth/login" asChild style={[styles.link, { color: tintColor }]}>
156 + <Text style={[styles.link, { color: tintColor }]}>
157 + Faça login
158 + </Text>
159 + </Link>
160 + </View>
161 + </ScrollView>
162 + </KeyboardAvoidingView>
163 + </SafeAreaView>
164 + );
165 + }
166 +
167 + const styles = StyleSheet.create({
168 + container: {
169 + flex: 1,
170 + },
171 + keyboardView: {
172 + flex: 1,
173 + },
174 + scrollContainer: {
175 + flexGrow: 1,
176 + paddingHorizontal: 24,
177 + justifyContent: 'center',
178 + minHeight: '100%',
179 + },
180 + header: {
181 + alignItems: 'center',
182 + marginBottom: 40,
183 + },
184 + title: {
185 + fontSize: 28,
186 + fontWeight: 'bold',
187 + marginBottom: 8,
188 + },
189 + subtitle: {
190 + fontSize: 16,
191 + textAlign: 'center',
192 + },
193 + form: {
194 + marginBottom: 32,
195 + },
196 + signupButton: {
197 + marginTop: 8,
198 + },
199 + link: {
200 + textAlign: 'center',
201 + fontSize: 14,
202 + fontWeight: '500',
203 + },
204 + footer: {
205 + flexDirection: 'row',
206 + justifyContent: 'center',
207 + alignItems: 'center',
208 + marginTop: 'auto',
209 + paddingBottom: 20,
210 + },
211 + footerText: {
212 + fontSize: 14,
213 + },
214 + });