frontend/auth: add auth screens, login, sign up and reset password
Pedro Lucas Porcellis porcellis@eletrotupi.com · 6 months ago
c167b931829e73be76860df22b78f8ac0951550e Browse filesParents
ae44a25Changed files
8
Additions
+920
Deletions
-11
frontend/app/(tabs)/_layout.tsx
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
14 35
screenOptions={{
15 36
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
31 52
}}
32 53
/>
33 54
</Tabs>
34 55
);
35 56
}
frontend/app/(tabs)/index.tsx
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' }}
14 33
headerImage={
15 34
<Image
16 35
source={require('@/assets/images/partial-react-logo.png')}
17 36
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>
26 52
<ThemedText>
27 53
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
94 120
bottom: 0,
95 121
left: 0,
96 122
position: 'absolute',
97 123
},
98 124
});
frontend/app/_layout.tsx
1 1
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
2 2
import { Stack } from 'expo-router';
3 3
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)',
10 11
};
11 12
12 13
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
}
/dev/null → frontend/app/auth/_layout.tsx
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 +
}/dev/null → frontend/app/auth/forgot-password.tsx
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 +
});
/dev/null → frontend/app/auth/login.tsx
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 +
});
/dev/null → frontend/app/auth/reset-password.tsx
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 +
});
/dev/null → frontend/app/auth/signup.tsx
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 +
});