frontend/auth: give the auth screens one layout
All five screens carried their own copy of the same keyboard avoiding scroll view, which is why the form re-centred and scrolled as soon as an error appeared. One shared screen pins content to the top, and login keeps the only animated logo
Pedro Lucas Porcellis porcellis@eletrotupi.com · 11 days ago
6bdc28a3adcc45cf37288df8777dfa536ce0f07a Browse filesParents
1144332Changed files
6
Additions
+458
Deletions
-564
frontend/app/auth/activate.tsx
1 1
import React, { useEffect, useRef, useState } from 'react';
2 -
import {
3 -
View,
4 -
StyleSheet,
5 -
ScrollView,
6 -
KeyboardAvoidingView,
7 -
} from 'react-native';
8 -
import { Text } from '@/components/ui/Text';
2 +
import { Pressable, StyleSheet, View } from 'react-native';
9 3
import { router } from 'expo-router';
10 -
import { SafeAreaView } from 'react-native-safe-area-context';
11 4
import { useThemeColor } from '@/hooks/use-theme-color';
12 5
import { Button, CodeInput } from '@/components/ui';
13 6
import type { CodeInputHandle } from '@/components/ui/CodeInput';
7 +
import { Text } from '@/components/ui/Text';
8 +
import { AuthScreen } from '@/components/auth/AuthScreen';
14 9
import { Colors } from '@/constants/theme';
15 10
import { useAuth } from '@/context/AuthContext';
16 11
import { useToast } from '@/context/ToastContext';
17 12
import { translateError } from '@/lib/errors/translations';
18 13
24 19
const [codeError, setCodeError] = useState<string>();
25 20
const [loading, setLoading] = useState(false);
26 21
const [secondsLeft, setSecondsLeft] = useState(RESEND_COOLDOWN_SECONDS);
27 22
const codeInputRef = useRef<CodeInputHandle>(null);
28 23
29 -
const { activate, requestActivateCode } = useAuth();
24 +
const { activate, requestActivateCode, logout } = useAuth();
30 25
const { showToast } = useToast();
31 26
const textColor = useThemeColor({}, 'text');
32 -
const backgroundColor = useThemeColor({}, 'background');
27 +
const tintColor = useThemeColor({}, 'tint');
33 28
34 29
useEffect(() => {
35 30
if (secondsLeft <= 0) return;
36 31
37 32
const timer = setTimeout(() => setSecondsLeft((value) => value - 1), 1000);
67 62
const handleCodeChange = (value: string) => {
68 63
setCode(value);
69 64
setCodeError(undefined);
70 65
};
71 66
67 +
// Abandons this half-created session. Nothing exists on an inactive account
68 +
// yet, so there is nothing to warn about — it just clears the local session
69 +
// and drops back to login.
70 +
const backToLogin = async () => {
71 +
await logout();
72 +
router.replace('/auth/login');
73 +
};
74 +
72 75
const resendCode = async () => {
73 76
if (secondsLeft > 0) return;
74 77
75 78
setSecondsLeft(RESEND_COOLDOWN_SECONDS);
76 79
try {
81 84
showToast('Falha ao reenviar o código. Tente novamente.', 'error');
82 85
}
83 86
};
84 87
85 88
return (
86 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
87 -
<KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
88 -
<ScrollView
89 -
contentContainerStyle={styles.scrollContainer}
90 -
showsVerticalScrollIndicator={false}
91 -
>
92 -
<View style={styles.header}>
93 -
<Text style={[styles.title, { color: textColor }]}>Ativar a conta</Text>
94 -
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
95 -
Digite o código de 6 dígitos enviado para seu email
96 -
</Text>
97 -
</View>
89 +
<AuthScreen
90 +
title="Ativar a conta"
91 +
subtitle="Digite o código de 6 dígitos enviado para seu email"
92 +
footer={
93 +
<View style={styles.footer}>
94 +
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
95 +
Não recebeu?{' '}
96 +
</Text>
98 97
99 -
<View style={styles.form}>
100 -
<CodeInput
101 -
ref={codeInputRef}
102 -
value={code}
103 -
onChange={handleCodeChange}
104 -
onComplete={submitCode}
105 -
length={CODE_LENGTH}
106 -
error={codeError}
107 -
editable={!loading}
108 -
accessibilityLabel="Código de ativação de 6 dígitos"
109 -
testIdPrefix="activate-code"
110 -
/>
98 +
<Button
99 +
variant="outline"
100 +
title={
101 +
secondsLeft > 0
102 +
? `Reenviar em ${secondsLeft}s`
103 +
: 'Reenviar um novo código'
104 +
}
105 +
onPress={resendCode}
106 +
disabled={secondsLeft > 0}
107 +
/>
111 108
112 -
<Button
113 -
title="Ativar conta"
114 -
onPress={() => submitCode(code)}
115 -
loading={loading}
116 -
style={styles.activateButton}
117 -
/>
118 -
</View>
119 -
120 -
<View style={styles.footer}>
121 -
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
122 -
Não recebeu?{' '}
109 +
<Pressable
110 +
onPress={backToLogin}
111 +
style={styles.backLink}
112 +
testID="button-back-to-login"
113 +
>
114 +
<Text style={[styles.backLinkText, { color: tintColor }]}>
115 +
Voltar para login
123 116
</Text>
117 +
</Pressable>
118 +
</View>
119 +
}
120 +
>
121 +
<View style={styles.form}>
122 +
<CodeInput
123 +
ref={codeInputRef}
124 +
value={code}
125 +
onChange={handleCodeChange}
126 +
onComplete={submitCode}
127 +
length={CODE_LENGTH}
128 +
error={codeError}
129 +
editable={!loading}
130 +
accessibilityLabel="Código de ativação de 6 dígitos"
131 +
testIdPrefix="activate-code"
132 +
/>
124 133
125 -
<Button
126 -
variant="outline"
127 -
title={
128 -
secondsLeft > 0
129 -
? `Reenviar em ${secondsLeft}s`
130 -
: 'Reenviar um novo código'
131 -
}
132 -
onPress={resendCode}
133 -
disabled={secondsLeft > 0}
134 -
/>
135 -
</View>
136 -
</ScrollView>
137 -
</KeyboardAvoidingView>
138 -
</SafeAreaView>
134 +
<Button
135 +
title="Ativar conta"
136 +
onPress={() => submitCode(code)}
137 +
loading={loading}
138 +
style={styles.activateButton}
139 +
/>
140 +
</View>
141 +
</AuthScreen>
139 142
);
140 143
}
141 144
142 145
const styles = StyleSheet.create({
143 -
container: {
144 -
flex: 1,
145 -
},
146 -
keyboardView: {
147 -
flex: 1,
148 -
},
149 -
scrollContainer: {
150 -
flexGrow: 1,
151 -
paddingHorizontal: 24,
152 -
justifyContent: 'center',
153 -
minHeight: '100%',
154 -
},
155 -
header: {
156 -
alignItems: 'center',
157 -
marginBottom: 40,
158 -
},
159 -
title: {
160 -
fontSize: 28,
161 -
fontWeight: 'bold',
162 -
marginBottom: 8,
163 -
},
164 -
subtitle: {
165 -
fontSize: 16,
166 -
textAlign: 'center',
167 -
},
168 146
form: {
169 147
marginBottom: 32,
170 148
},
171 149
activateButton: {
172 150
marginTop: 8,
173 151
},
174 152
footer: {
175 -
marginTop: 20,
176 153
borderTopColor: Colors.light.accentBlue,
177 154
borderTopWidth: 1,
178 155
paddingTop: 20,
179 156
},
180 157
footerText: {
181 158
textAlign: 'center',
182 159
marginBottom: 10,
160 +
},
161 +
backLink: {
162 +
marginTop: 20,
163 +
alignItems: 'center',
164 +
},
165 +
backLinkText: {
166 +
fontSize: 14,
167 +
fontWeight: '500',
183 168
},
184 169
});
frontend/app/auth/forgot-password.tsx
1 1
import React, { useState } from 'react';
2 -
import {
3 -
View,
4 -
StyleSheet,
5 -
ScrollView,
6 -
KeyboardAvoidingView,
7 -
} from 'react-native';
8 -
import { Text } from '@/components/ui/Text';
9 -
import { Link, router } from 'expo-router';
10 -
import { SafeAreaView } from 'react-native-safe-area-context';
2 +
import { StyleSheet, View } from 'react-native';
3 +
import { Link } from 'expo-router';
11 4
import { useThemeColor } from '@/hooks/use-theme-color';
12 5
import { Button, Input } from '@/components/ui';
6 +
import { Text } from '@/components/ui/Text';
7 +
import { AuthScreen } from '@/components/auth/AuthScreen';
13 8
import { useAuth } from '@/context/AuthContext';
14 9
import { useToast } from '@/context/ToastContext';
15 10
import { translateError } from '@/lib/errors/translations';
16 11
17 12
export default function ForgotPasswordScreen() {
18 13
const [email, setEmail] = useState('');
19 14
const [emailSent, setEmailSent] = useState(false);
20 -
const [resetToken, setResetToken] = useState(''); // Only for development
21 15
const [errors, setErrors] = useState<{ email?: string }>({});
22 16
23 17
const { forgotPassword } = useAuth();
24 18
const { showToast } = useToast();
25 19
const [loading, setLoading] = useState(false);
26 20
const textColor = useThemeColor({}, 'text');
27 -
const backgroundColor = useThemeColor({}, 'background');
28 21
const tintColor = useThemeColor({}, 'tint');
29 22
30 23
const validateForm = () => {
31 24
const newErrors: { email?: string } = {};
32 25
43 36
const handleForgotPassword = async () => {
44 37
if (!validateForm()) return;
45 38
46 39
setLoading(true);
47 40
try {
48 -
const data = await forgotPassword(email);
41 +
await forgotPassword(email);
49 42
setEmailSent(true);
50 -
// In development, show the reset token
51 -
if (data.token) {
52 -
setResetToken(data.token);
53 -
}
54 43
} catch (error) {
55 44
showToast(translateError(error) || 'Falha ao enviar link de recuperação', 'error');
56 45
} finally {
57 46
setLoading(false);
58 47
}
59 48
};
60 49
61 -
const handleUseToken = () => {
62 -
if (resetToken) {
63 -
router.push({
64 -
pathname: '/auth/reset-password',
65 -
params: { token: resetToken },
66 -
});
67 -
}
68 -
};
69 -
70 50
if (emailSent) {
71 51
return (
72 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
73 -
<View style={styles.successContainer}>
74 -
<Text style={[styles.title, { color: textColor }]}>Verifique seu email</Text>
75 -
<Text style={[styles.message, { color: textColor, opacity: 0.7 }]}>
76 -
Enviamos as instruções para resetar sua senha para {email}
77 -
</Text>
78 -
79 -
{/* Development only - show reset token */}
80 -
{resetToken && (
81 -
<View style={styles.devSection}>
82 -
<Text style={[styles.devTitle, { color: tintColor }]}>
83 -
Dev mode:
84 -
</Text>
85 -
<Text style={[styles.devText, { color: textColor, opacity: 0.7 }]}>
86 -
Token: {resetToken}
87 -
</Text>
88 -
<Button
89 -
title="Usar token para resetar senha"
90 -
onPress={handleUseToken}
91 -
style={styles.devButton}
92 -
/>
93 -
</View>
94 -
)}
95 -
52 +
<AuthScreen
53 +
title="Verifique seu email"
54 +
subtitle={`Enviamos as instruções para resetar sua senha para ${email}`}
55 +
footer={
96 56
<View style={styles.actions}>
97 57
<Button
98 58
title="Reenviar email"
99 59
variant="outline"
100 60
onPress={() => setEmailSent(false)}
101 -
style={styles.resendButton}
102 61
/>
103 62
104 63
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
105 64
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
106 65
Voltar para login
107 66
</Text>
108 67
</Link>
109 68
</View>
110 -
</View>
111 -
</SafeAreaView>
69 +
}
70 +
/>
112 71
);
113 72
}
114 73
115 74
return (
116 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
117 -
<KeyboardAvoidingView
118 -
behavior='height'
119 -
style={styles.keyboardView}
120 -
>
121 -
<ScrollView
122 -
contentContainerStyle={styles.scrollContainer}
123 -
showsVerticalScrollIndicator={false}
124 -
>
125 -
<View style={styles.header}>
126 -
<Text style={[styles.title, { color: textColor }]}>Esqueceu a senha?</Text>
127 -
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
128 -
Insira seu email para receber as instruções de recuperação
75 +
<AuthScreen
76 +
title="Esqueceu a senha?"
77 +
subtitle="Insira seu email para receber as instruções de recuperação"
78 +
footer={
79 +
<View style={styles.footer}>
80 +
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
81 +
Lembra tua senha?{' '}
82 +
</Text>
83 +
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
84 +
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
85 +
Faça login
129 86
</Text>
130 -
</View>
131 -
132 -
<View style={styles.form}>
133 -
<Input
134 -
label="Email"
135 -
type="email"
136 -
value={email}
137 -
onChangeText={setEmail}
138 -
placeholder="Qual seu email?"
139 -
error={errors.email}
140 -
autoCapitalize="none"
141 -
/>
142 -
143 -
<Button
144 -
title="Enviar link de recuperação"
145 -
onPress={handleForgotPassword}
146 -
loading={loading}
147 -
style={styles.resetButton}
148 -
/>
149 -
</View>
87 +
</Link>
88 +
</View>
89 +
}
90 +
>
91 +
<View style={styles.form}>
92 +
<Input
93 +
label="Email"
94 +
type="email"
95 +
value={email}
96 +
onChangeText={setEmail}
97 +
placeholder="Qual seu email?"
98 +
error={errors.email}
99 +
autoCapitalize="none"
100 +
/>
150 101
151 -
<View style={styles.footer}>
152 -
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
153 -
Lembra tua senha?{' '}
154 -
</Text>
155 -
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
156 -
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
157 -
Faça login
158 -
</Text>
159 -
</Link>
160 -
</View>
161 -
</ScrollView>
162 -
</KeyboardAvoidingView>
163 -
</SafeAreaView>
102 +
<Button
103 +
title="Enviar link de recuperação"
104 +
onPress={handleForgotPassword}
105 +
loading={loading}
106 +
style={styles.resetButton}
107 +
/>
108 +
</View>
109 +
</AuthScreen>
164 110
);
165 111
}
166 112
167 113
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 -
successContainer: {
181 -
flex: 1,
182 -
justifyContent: 'center',
183 -
paddingHorizontal: 24,
184 -
},
185 -
header: {
186 -
alignItems: 'center',
187 -
marginBottom: 40,
188 -
},
189 -
title: {
190 -
fontSize: 28,
191 -
fontWeight: 'bold',
192 -
marginBottom: 8,
193 -
textAlign: 'center',
194 -
},
195 -
subtitle: {
196 -
fontSize: 16,
197 -
textAlign: 'center',
198 -
lineHeight: 24,
199 -
},
200 -
message: {
201 -
fontSize: 16,
202 -
textAlign: 'center',
203 -
lineHeight: 24,
204 -
marginBottom: 32,
205 -
},
206 114
form: {
207 115
marginBottom: 32,
208 116
},
209 117
resetButton: {
210 118
marginTop: 8,
211 119
},
212 120
actions: {
213 121
gap: 16,
214 122
},
215 -
resendButton: {
216 -
marginBottom: 8,
217 -
},
218 -
devSection: {
219 -
marginBottom: 32,
220 -
padding: 16,
221 -
borderRadius: 8,
222 -
backgroundColor: 'rgba(74, 144, 226, 0.1)',
223 -
},
224 -
devTitle: {
225 -
fontSize: 14,
226 -
fontWeight: '600',
227 -
marginBottom: 8,
228 -
},
229 -
devText: {
230 -
fontSize: 12,
231 -
marginBottom: 12,
232 -
fontFamily: 'monospace',
233 -
},
234 -
devButton: {
235 -
marginTop: 8,
236 -
},
237 123
link: {
238 124
textAlign: 'center',
239 125
fontSize: 14,
240 126
fontWeight: '500',
241 127
},
242 128
footer: {
243 129
flexDirection: 'row',
244 130
justifyContent: 'center',
245 131
alignItems: 'center',
246 -
marginTop: 'auto',
247 -
paddingBottom: 20,
248 132
},
249 133
footerText: {
250 134
fontSize: 14,
251 135
},
252 136
});
frontend/app/auth/login.tsx
1 1
import React, { useState, useEffect } from "react";
2 -
import {
3 -
View,
4 -
StyleSheet,
5 -
ScrollView,
6 -
KeyboardAvoidingView,
7 -
Animated,
8 -
Easing,
9 -
Image,
10 -
} from "react-native";
11 -
import { Text } from "@/components/ui/Text";
2 +
import { Animated, Easing, Image, StyleSheet, View } from "react-native";
3 +
import { StatusBar } from "expo-status-bar";
12 4
import { Link, router } from "expo-router";
13 -
import { SafeAreaView } from "react-native-safe-area-context";
14 -
import { StatusBar } from "expo-status-bar";
15 5
import { useThemeColor } from "@/hooks/use-theme-color";
16 6
import { Button, Input } from "@/components/ui";
7 +
import { Text } from "@/components/ui/Text";
8 +
import { AuthScreen } from "@/components/auth/AuthScreen";
17 9
import { useAuth } from "@/context/AuthContext";
18 10
import { apiClient, ApiError } from "@/lib/api";
19 11
import { useToast } from "@/context/ToastContext";
20 12
import { translateError, translateFields } from "@/lib/errors/translations";
21 13
import { registerForPushNotifications } from "@/hooks/useRegisterPushToken";
29 21
const [loading, setLoading] = useState(false);
30 22
31 23
const { login, updateAuthUser } = useAuth();
32 24
const { showToast } = useToast();
33 25
const textColor = useThemeColor({}, "text");
34 -
const backgroundColor = useThemeColor({}, "background");
35 26
const tintColor = useThemeColor({}, "tint");
36 27
37 28
const [spinValue] = useState(() => new Animated.Value(0));
38 29
useEffect(() => {
39 30
const animation = Animated.loop(
87 78
const updateResponse = await apiClient.updateUser({
88 79
id: response.user.id,
89 80
pushToken: token,
90 81
});
91 82
92 -
console.log("UPDATE USER response: ", updateResponse);
93 -
94 83
if (updateResponse) {
95 84
updateAuthUser(updateResponse.user);
96 85
}
97 86
}
98 87
}
99 88
100 89
router.replace("/");
101 90
} catch (error) {
102 -
console.log("LOGIN error: ", error);
103 91
if (error instanceof ApiError && error.fields) {
104 92
setErrors(translateFields(error.fields));
105 93
} else {
106 -
showToast(translateError(error) || 'Falha ao fazer login', 'error');
94 +
showToast(translateError(error) || "Falha ao fazer login", "error");
107 95
}
108 96
} finally {
109 97
setLoading(false);
110 98
}
111 99
};
112 100
113 101
return (
114 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
115 -
<KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
116 -
<StatusBar style={'dark'} />
117 -
<ScrollView
118 -
contentContainerStyle={styles.scrollContainer}
119 -
showsVerticalScrollIndicator={false}
120 -
>
121 -
<View style={styles.logoContainer}>
122 -
<Animated.View style={{ transform: [{ rotate: spin }] }}>
123 -
<Image
124 -
source={require("@/assets/images/logo.png")}
125 -
style={styles.logo}
126 -
resizeMode="contain"
127 -
/>
128 -
</Animated.View>
129 -
</View>
130 -
131 -
<View style={styles.header}>
132 -
<Text style={[styles.title, { color: textColor }]} testID="text-welcome">
133 -
Bem vindo de volta
134 -
</Text>
135 -
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
136 -
Logue na sua conta nexo
137 -
</Text>
138 -
</View>
139 -
140 -
<View style={styles.form}>
141 -
<Input
142 -
label="Email"
143 -
type="email"
144 -
value={email}
145 -
onChangeText={setEmail}
146 -
placeholder="Qual seu email?"
147 -
error={errors.email}
148 -
autoCapitalize="none"
102 +
<>
103 +
<StatusBar style="dark" />
104 +
<AuthScreen
105 +
header={
106 +
<Animated.View
107 +
style={[styles.logoContainer, { transform: [{ rotate: spin }] }]}
108 +
>
109 +
<Image
110 +
source={require("@/assets/images/logo.png")}
111 +
style={styles.logo}
112 +
resizeMode="contain"
149 113
/>
150 -
151 -
<Input
152 -
label="Senha"
153 -
type="password"
154 -
value={password}
155 -
onChangeText={setPassword}
156 -
placeholder="Qual sua senha?"
157 -
error={errors.password}
158 -
showPasswordToggle
159 -
testID="input-password"
160 -
/>
161 -
162 -
<Button
163 -
title="Entrar"
164 -
onPress={handleLogin}
165 -
loading={loading}
166 -
style={styles.loginButton}
167 -
testID="button-login"
168 -
/>
169 -
170 -
<Link
171 -
href="/auth/forgot-password"
172 -
style={StyleSheet.flatten([styles.link, { color: tintColor }])}
173 -
asChild
174 -
>
175 -
<Text
176 -
style={StyleSheet.flatten([styles.footerText, { color: textColor, opacity: 0.7 }])}
177 -
>
178 -
Esqueceu a senha?
179 -
</Text>
180 -
</Link>
181 -
</View>
182 -
114 +
</Animated.View>
115 +
}
116 +
title="Bem vindo de volta"
117 +
titleTestID="text-welcome"
118 +
subtitle="Logue na sua conta nexo"
119 +
footer={
183 120
<View style={styles.footer}>
184 -
<Text
185 -
style={[styles.footerText, { color: textColor, opacity: 0.7 }]}
186 -
>
121 +
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
187 122
Não tem uma conta ainda?{" "}
188 123
</Text>
189 -
190 124
<Link
191 125
href="/auth/signup"
192 126
asChild
193 127
style={StyleSheet.flatten([styles.link, { color: tintColor }])}
194 128
>
195 129
<Text
196 -
style={StyleSheet.flatten([styles.footerText, { color: textColor, opacity: 0.7 }])}
130 +
style={StyleSheet.flatten([
131 +
styles.footerText,
132 +
{ color: tintColor },
133 +
])}
197 134
>
198 135
Cadastre-se
199 136
</Text>
200 137
</Link>
201 138
</View>
202 -
</ScrollView>
203 -
</KeyboardAvoidingView>
204 -
</SafeAreaView>
139 +
}
140 +
>
141 +
<View style={styles.form}>
142 +
<Input
143 +
label="Email"
144 +
type="email"
145 +
value={email}
146 +
onChangeText={setEmail}
147 +
placeholder="Qual seu email?"
148 +
error={errors.email}
149 +
autoCapitalize="none"
150 +
/>
151 +
152 +
<Input
153 +
label="Senha"
154 +
type="password"
155 +
value={password}
156 +
onChangeText={setPassword}
157 +
placeholder="Qual sua senha?"
158 +
error={errors.password}
159 +
showPasswordToggle
160 +
testID="input-password"
161 +
/>
162 +
163 +
<Button
164 +
title="Entrar"
165 +
onPress={handleLogin}
166 +
loading={loading}
167 +
style={styles.loginButton}
168 +
testID="button-login"
169 +
/>
170 +
171 +
<Link
172 +
href="/auth/forgot-password"
173 +
style={StyleSheet.flatten([styles.link, { color: tintColor }])}
174 +
asChild
175 +
>
176 +
<Text
177 +
style={StyleSheet.flatten([
178 +
styles.footerText,
179 +
{ color: textColor, opacity: 0.7 },
180 +
])}
181 +
>
182 +
Esqueceu a senha?
183 +
</Text>
184 +
</Link>
185 +
</View>
186 +
</AuthScreen>
187 +
</>
205 188
);
206 189
}
207 190
208 191
const styles = StyleSheet.create({
209 -
container: {
210 -
flex: 1,
211 -
},
212 -
keyboardView: {
213 -
flex: 1,
214 -
},
215 192
logoContainer: {
216 193
alignItems: "center",
217 194
marginBottom: 40,
218 195
marginTop: 10,
219 196
},
220 197
logo: {
221 198
width: 120,
222 199
height: 120,
223 200
},
224 -
scrollContainer: {
225 -
flexGrow: 1,
226 -
paddingHorizontal: 24,
227 -
justifyContent: "center",
228 -
minHeight: "100%",
229 -
},
230 -
header: {
231 -
alignItems: "center",
232 -
marginBottom: 40,
233 -
},
234 -
title: {
235 -
fontSize: 28,
236 -
fontWeight: "bold",
237 -
marginBottom: 8,
238 -
},
239 -
subtitle: {
240 -
fontSize: 16,
241 -
textAlign: "center",
242 -
},
243 201
form: {
244 202
marginBottom: 32,
245 203
},
246 204
loginButton: {
247 205
marginTop: 8,
254 212
},
255 213
footer: {
256 214
flexDirection: "row",
257 215
justifyContent: "center",
258 216
alignItems: "center",
259 -
marginTop: "auto",
260 -
paddingBottom: 40,
261 217
},
262 218
footerText: {
263 219
fontSize: 14,
264 220
},
265 221
});
frontend/app/auth/reset-password.tsx
1 1
import React, { useState } from 'react';
2 -
import {
3 -
View,
4 -
StyleSheet,
5 -
ScrollView,
6 -
KeyboardAvoidingView,
7 -
} from 'react-native';
8 -
import { Text } from '@/components/ui/Text';
2 +
import { StyleSheet, View } from 'react-native';
9 3
import { Link, useLocalSearchParams, router } from 'expo-router';
10 -
import { SafeAreaView } from 'react-native-safe-area-context';
11 4
import { useThemeColor } from '@/hooks/use-theme-color';
12 5
import { Button, Input } from '@/components/ui';
6 +
import { Text } from '@/components/ui/Text';
7 +
import { AuthScreen } from '@/components/auth/AuthScreen';
13 8
import { useAuth } from '@/context/AuthContext';
14 9
import { useToast } from '@/context/ToastContext';
15 10
import { translateError } from '@/lib/errors/translations';
16 11
17 12
export default function ResetPasswordScreen() {
25 20
26 21
const { resetPassword } = useAuth();
27 22
const { showToast } = useToast();
28 23
const [loading, setLoading] = useState(false);
29 24
const textColor = useThemeColor({}, 'text');
30 -
const backgroundColor = useThemeColor({}, 'background');
31 25
const tintColor = useThemeColor({}, 'tint');
32 26
33 27
const validateForm = () => {
34 28
const newErrors: typeof errors = {};
35 29
68 62
setLoading(false);
69 63
}
70 64
};
71 65
72 66
return (
73 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
74 -
<KeyboardAvoidingView
75 -
behavior='height'
76 -
style={styles.keyboardView}
77 -
>
78 -
<ScrollView
79 -
contentContainerStyle={styles.scrollContainer}
80 -
showsVerticalScrollIndicator={false}
81 -
>
82 -
<View style={styles.header}>
83 -
<Text style={[styles.title, { color: textColor }]}>Recuperar a conta</Text>
84 -
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
85 -
Digite uma nova senha para sua conta
67 +
<AuthScreen
68 +
title="Recuperar a conta"
69 +
subtitle="Digite uma nova senha para sua conta"
70 +
footer={
71 +
<View style={styles.footer}>
72 +
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
73 +
Lembrou da senha?{' '}
74 +
</Text>
75 +
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
76 +
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
77 +
Faça login
86 78
</Text>
87 -
</View>
88 -
89 -
<View style={styles.form}>
90 -
<Input
91 -
label="Nova senha"
92 -
type="password"
93 -
value={password}
94 -
onChangeText={setPassword}
95 -
placeholder="Digite uma nova senha"
96 -
error={errors.password}
97 -
showPasswordToggle
98 -
/>
99 -
100 -
<Input
101 -
label="Confirmar nova senha"
102 -
type="password"
103 -
value={confirmPassword}
104 -
onChangeText={setConfirmPassword}
105 -
placeholder="Confirme a nova senha"
106 -
error={errors.confirmPassword}
107 -
showPasswordToggle
108 -
/>
79 +
</Link>
80 +
</View>
81 +
}
82 +
>
83 +
<View style={styles.form}>
84 +
<Input
85 +
label="Nova senha"
86 +
type="password"
87 +
value={password}
88 +
onChangeText={setPassword}
89 +
placeholder="Digite uma nova senha"
90 +
error={errors.password}
91 +
showPasswordToggle
92 +
/>
109 93
110 -
<Button
111 -
title="Trocar senha"
112 -
onPress={handleResetPassword}
113 -
loading={loading}
114 -
style={styles.resetButton}
115 -
/>
116 -
</View>
94 +
<Input
95 +
label="Confirmar nova senha"
96 +
type="password"
97 +
value={confirmPassword}
98 +
onChangeText={setConfirmPassword}
99 +
placeholder="Confirme a nova senha"
100 +
error={errors.confirmPassword}
101 +
showPasswordToggle
102 +
/>
117 103
118 -
<View style={styles.footer}>
119 -
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
120 -
Lembrou da senha?{' '}
121 -
</Text>
122 -
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
123 -
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
124 -
Faça login
125 -
</Text>
126 -
</Link>
127 -
</View>
128 -
</ScrollView>
129 -
</KeyboardAvoidingView>
130 -
</SafeAreaView>
104 +
<Button
105 +
title="Trocar senha"
106 +
onPress={handleResetPassword}
107 +
loading={loading}
108 +
style={styles.resetButton}
109 +
/>
110 +
</View>
111 +
</AuthScreen>
131 112
);
132 113
}
133 114
134 115
const styles = StyleSheet.create({
135 -
container: {
136 -
flex: 1,
137 -
},
138 -
keyboardView: {
139 -
flex: 1,
140 -
},
141 -
scrollContainer: {
142 -
flexGrow: 1,
143 -
paddingHorizontal: 24,
144 -
justifyContent: 'center',
145 -
minHeight: '100%',
146 -
},
147 -
header: {
148 -
alignItems: 'center',
149 -
marginBottom: 40,
150 -
},
151 -
title: {
152 -
fontSize: 28,
153 -
fontWeight: 'bold',
154 -
marginBottom: 8,
155 -
},
156 -
subtitle: {
157 -
fontSize: 16,
158 -
textAlign: 'center',
159 -
},
160 116
form: {
161 117
marginBottom: 32,
162 118
},
163 119
resetButton: {
164 120
marginTop: 8,
170 126
},
171 127
footer: {
172 128
flexDirection: 'row',
173 129
justifyContent: 'center',
174 130
alignItems: 'center',
175 -
marginTop: 'auto',
176 -
paddingBottom: 20,
177 131
},
178 132
footerText: {
179 133
fontSize: 14,
180 134
},
181 135
});
frontend/app/auth/signup.tsx
1 1
import React, { useState } from 'react';
2 -
import {
3 -
View,
4 -
StyleSheet,
5 -
ScrollView,
6 -
KeyboardAvoidingView,
7 -
} from 'react-native';
8 -
import { Text } from '@/components/ui/Text';
2 +
import { Pressable, StyleSheet, View } from 'react-native';
3 +
import { Ionicons } from '@expo/vector-icons';
9 4
import { Link, router } from 'expo-router';
10 -
import { SafeAreaView } from 'react-native-safe-area-context';
11 5
import { useThemeColor } from '@/hooks/use-theme-color';
12 6
import { Button, Input } from '@/components/ui';
7 +
import { Text } from '@/components/ui/Text';
8 +
import { AuthScreen } from '@/components/auth/AuthScreen';
13 9
import { useAuth } from '@/context/AuthContext';
14 10
import { ApiError } from '@/lib/api';
15 11
import { useToast } from '@/context/ToastContext';
16 12
import { translateError, translateFields } from '@/lib/errors/translations';
17 13
19 15
const [firstName, setFirstName] = useState('');
20 16
const [lastName, setLastName] = useState('');
21 17
const [email, setEmail] = useState('');
22 18
const [password, setPassword] = useState('');
23 19
const [confirmPassword, setConfirmPassword] = useState('');
20 +
const [acceptedTerms, setAcceptedTerms] = useState(false);
24 21
const [errors, setErrors] = useState<{
25 22
firstName?: string;
26 23
email?: string;
27 24
password?: string;
28 25
confirmPassword?: string;
26 +
acceptedTerms?: string;
29 27
}>({});
30 28
31 29
const [loading, setLoading] = useState(false);
32 30
const { signup } = useAuth();
33 31
const { showToast } = useToast();
34 32
const textColor = useThemeColor({}, 'text');
35 -
const backgroundColor = useThemeColor({}, 'background');
36 33
const tintColor = useThemeColor({}, 'tint');
37 34
38 35
const validateForm = () => {
39 36
const newErrors: typeof errors = {};
40 37
58 55
newErrors.confirmPassword = 'Por favor confirme sua senha';
59 56
} else if (password !== confirmPassword) {
60 57
newErrors.confirmPassword = 'As senhas não coincidem';
61 58
}
62 59
60 +
if (!acceptedTerms) {
61 +
newErrors.acceptedTerms = 'Você precisa aceitar os termos para continuar';
62 +
}
63 +
63 64
setErrors(newErrors);
64 65
return Object.keys(newErrors).length === 0;
65 66
};
66 67
67 68
const handleSignup = async () => {
68 69
if (!validateForm()) return;
69 70
70 71
setLoading(true);
71 72
try {
72 -
await signup({ firstName, lastName, email, password });
73 +
await signup({ firstName, lastName, email, password, acceptedTerms });
73 74
router.replace('/');
74 75
} catch (error) {
75 76
if (error instanceof ApiError && error.fields) {
76 77
setErrors(translateFields(error.fields));
77 78
} else {
81 82
setLoading(false);
82 83
}
83 84
};
84 85
85 86
return (
86 -
<SafeAreaView style={[styles.container, { backgroundColor }]}>
87 -
<KeyboardAvoidingView
88 -
behavior='height'
89 -
style={styles.keyboardView}
90 -
>
91 -
<ScrollView
92 -
contentContainerStyle={styles.scrollContainer}
93 -
showsVerticalScrollIndicator={false}
94 -
>
95 -
<View style={styles.header}>
96 -
<Text style={[styles.title, { color: textColor }]}>Criar conta</Text>
97 -
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
98 -
Cadastre-se para começar a monitorar o seu humor.
87 +
<AuthScreen
88 +
title="Criar conta"
89 +
subtitle="Cadastre-se para começar a monitorar o seu humor."
90 +
footer={
91 +
<View style={styles.footer}>
92 +
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
93 +
Já tem uma conta?{' '}
94 +
</Text>
95 +
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
96 +
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
97 +
Faça login
99 98
</Text>
100 -
</View>
99 +
</Link>
100 +
</View>
101 +
}
102 +
>
103 +
<View style={styles.form}>
104 +
<Input
105 +
label="Nome"
106 +
value={firstName}
107 +
onChangeText={setFirstName}
108 +
placeholder="Digite seu nome"
109 +
error={errors.firstName}
110 +
/>
101 111
102 -
<View style={styles.form}>
103 -
<Input
104 -
label="Nome"
105 -
value={firstName}
106 -
onChangeText={setFirstName}
107 -
placeholder="Digite seu nome"
108 -
error={errors.firstName}
109 -
/>
112 +
<Input
113 +
label="Sobrenome (opcional)"
114 +
value={lastName}
115 +
onChangeText={setLastName}
116 +
placeholder="Digite seu sobrenome"
117 +
/>
110 118
111 -
<Input
112 -
label="Sobrenome (opcional)"
113 -
value={lastName}
114 -
onChangeText={setLastName}
115 -
placeholder="Digite seu sobrenome"
116 -
/>
119 +
<Input
120 +
label="Email"
121 +
type="email"
122 +
value={email}
123 +
onChangeText={setEmail}
124 +
placeholder="Qual seu email?"
125 +
error={errors.email}
126 +
autoCapitalize="none"
127 +
/>
117 128
118 -
<Input
119 -
label="Email"
120 -
type="email"
121 -
value={email}
122 -
onChangeText={setEmail}
123 -
placeholder="Qual seu email?"
124 -
error={errors.email}
125 -
autoCapitalize="none"
126 -
/>
129 +
<Input
130 +
label="Senha"
131 +
type="password"
132 +
value={password}
133 +
onChangeText={setPassword}
134 +
placeholder="Deve ter no min. 6 caracteres"
135 +
error={errors.password}
136 +
showPasswordToggle
137 +
/>
127 138
128 -
<Input
129 -
label="Senha"
130 -
type="password"
131 -
value={password}
132 -
onChangeText={setPassword}
133 -
placeholder="Deve ter no min. 6 caracteres"
134 -
error={errors.password}
135 -
showPasswordToggle
136 -
/>
139 +
<Input
140 +
label="Confirme a senha"
141 +
type="password"
142 +
value={confirmPassword}
143 +
onChangeText={setConfirmPassword}
144 +
placeholder="Confirme sua senha"
145 +
error={errors.confirmPassword}
146 +
showPasswordToggle
147 +
/>
137 148
138 -
<Input
139 -
label="Confirme a senha"
140 -
type="password"
141 -
value={confirmPassword}
142 -
onChangeText={setConfirmPassword}
143 -
placeholder="Confirme sua senha"
144 -
error={errors.confirmPassword}
145 -
showPasswordToggle
149 +
<View style={styles.termsRow}>
150 +
<Pressable
151 +
onPress={() => setAcceptedTerms((value) => !value)}
152 +
hitSlop={8}
153 +
accessibilityRole="checkbox"
154 +
accessibilityState={{ checked: acceptedTerms }}
155 +
testID="checkbox-terms"
156 +
>
157 +
<Ionicons
158 +
name={acceptedTerms ? 'checkbox' : 'square-outline'}
159 +
size={22}
160 +
color={acceptedTerms ? tintColor : textColor}
146 161
/>
162 +
</Pressable>
147 163
148 -
<Button
149 -
title="Criar conta"
150 -
onPress={handleSignup}
151 -
loading={loading}
152 -
style={styles.signupButton}
153 -
/>
154 -
</View>
164 +
<Text style={[styles.termsText, { color: textColor }]}>
165 +
Li e aceito os{' '}
166 +
</Text>
155 167
156 -
<View style={styles.footer}>
157 -
<Text style={[styles.footerText, { color: textColor, opacity: 0.7 }]}>
158 -
Já tem uma conta?{' '}
168 +
<Link href="/terms" asChild style={StyleSheet.flatten([styles.termsLink, { color: tintColor }])}>
169 +
<Text style={StyleSheet.flatten([styles.termsLink, { color: tintColor }])}>
170 +
Termos e Condições
159 171
</Text>
160 -
<Link href="/auth/login" asChild style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
161 -
<Text style={StyleSheet.flatten([styles.link, { color: tintColor }])}>
162 -
Faça login
163 -
</Text>
164 -
</Link>
165 -
</View>
166 -
</ScrollView>
167 -
</KeyboardAvoidingView>
168 -
</SafeAreaView>
172 +
</Link>
173 +
</View>
174 +
175 +
{errors.acceptedTerms && (
176 +
<Text style={[styles.termsError, { color: '#ef4444' }]}>
177 +
{errors.acceptedTerms}
178 +
</Text>
179 +
)}
180 +
181 +
<Button
182 +
title="Criar conta"
183 +
onPress={handleSignup}
184 +
loading={loading}
185 +
style={styles.signupButton}
186 +
/>
187 +
</View>
188 +
</AuthScreen>
169 189
);
170 190
}
171 191
172 192
const styles = StyleSheet.create({
173 -
container: {
174 -
flex: 1,
175 -
},
176 -
keyboardView: {
177 -
flex: 1,
193 +
form: {
194 +
marginBottom: 32,
178 195
},
179 -
scrollContainer: {
180 -
flexGrow: 1,
181 -
paddingHorizontal: 24,
182 -
justifyContent: 'center',
183 -
minHeight: '100%',
196 +
signupButton: {
197 +
marginTop: 8,
184 198
},
185 -
header: {
199 +
termsRow: {
200 +
flexDirection: 'row',
186 201
alignItems: 'center',
187 -
marginBottom: 40,
188 -
},
189 -
title: {
190 -
fontSize: 28,
191 -
fontWeight: 'bold',
202 +
flexWrap: 'wrap',
192 203
marginBottom: 8,
193 204
},
194 -
subtitle: {
195 -
fontSize: 16,
196 -
textAlign: 'center',
205 +
termsText: {
206 +
fontSize: 14,
197 207
},
198 -
form: {
199 -
marginBottom: 32,
208 +
termsLink: {
209 +
fontSize: 14,
210 +
fontWeight: '600',
200 211
},
201 -
signupButton: {
202 -
marginTop: 8,
212 +
termsError: {
213 +
fontSize: 12,
214 +
marginBottom: 8,
203 215
},
204 216
link: {
205 217
textAlign: 'center',
206 218
fontSize: 14,
207 219
fontWeight: '500',
208 220
},
209 221
footer: {
210 222
flexDirection: 'row',
211 223
justifyContent: 'center',
212 224
alignItems: 'center',
213 -
marginTop: 'auto',
214 -
paddingBottom: 20,
215 225
},
216 226
footerText: {
217 227
fontSize: 14,
218 228
},
219 229
});
/dev/null → frontend/components/auth/AuthScreen.tsx
1 +
import React from 'react';
2 +
import {
3 +
KeyboardAvoidingView,
4 +
ScrollView,
5 +
StyleSheet,
6 +
View,
7 +
type StyleProp,
8 +
type ViewStyle,
9 +
} from 'react-native';
10 +
import { SafeAreaView } from 'react-native-safe-area-context';
11 +
import { Text } from '@/components/ui/Text';
12 +
import { useThemeColor } from '@/hooks/use-theme-color';
13 +
14 +
interface AuthScreenProps {
15 +
title?: string;
16 +
subtitle?: string;
17 +
titleTestID?: string;
18 +
// Sits above the title — login puts its animated logo here.
19 +
header?: React.ReactNode;
20 +
footer?: React.ReactNode;
21 +
children?: React.ReactNode;
22 +
contentContainerStyle?: StyleProp<ViewStyle>;
23 +
}
24 +
25 +
// The one layout every auth screen shares. Top-aligned on purpose: a form whose
26 +
// error rows grow must not re-center (and scroll) the whole screen when it does.
27 +
export function AuthScreen({
28 +
title,
29 +
subtitle,
30 +
titleTestID,
31 +
header,
32 +
footer,
33 +
children,
34 +
contentContainerStyle,
35 +
}: AuthScreenProps) {
36 +
const textColor = useThemeColor({}, 'text');
37 +
const backgroundColor = useThemeColor({}, 'background');
38 +
39 +
return (
40 +
<SafeAreaView style={[styles.container, { backgroundColor }]}>
41 +
<KeyboardAvoidingView behavior="height" style={styles.keyboardView}>
42 +
<ScrollView
43 +
contentContainerStyle={[styles.scrollContainer, contentContainerStyle]}
44 +
showsVerticalScrollIndicator={false}
45 +
keyboardShouldPersistTaps="handled"
46 +
>
47 +
{header}
48 +
49 +
{(title || subtitle) && (
50 +
<View style={styles.header}>
51 +
{title && (
52 +
<Text testID={titleTestID} style={[styles.title, { color: textColor }]}>
53 +
{title}
54 +
</Text>
55 +
)}
56 +
{subtitle && (
57 +
<Text style={[styles.subtitle, { color: textColor, opacity: 0.7 }]}>
58 +
{subtitle}
59 +
</Text>
60 +
)}
61 +
</View>
62 +
)}
63 +
64 +
{children}
65 +
66 +
{footer && <View style={styles.footer}>{footer}</View>}
67 +
</ScrollView>
68 +
</KeyboardAvoidingView>
69 +
</SafeAreaView>
70 +
);
71 +
}
72 +
73 +
const styles = StyleSheet.create({
74 +
container: {
75 +
flex: 1,
76 +
},
77 +
keyboardView: {
78 +
flex: 1,
79 +
},
80 +
scrollContainer: {
81 +
flexGrow: 1,
82 +
paddingHorizontal: 24,
83 +
paddingTop: 24,
84 +
paddingBottom: 24,
85 +
},
86 +
header: {
87 +
alignItems: 'center',
88 +
marginBottom: 40,
89 +
},
90 +
title: {
91 +
fontSize: 28,
92 +
fontWeight: 'bold',
93 +
marginBottom: 8,
94 +
textAlign: 'center',
95 +
},
96 +
subtitle: {
97 +
fontSize: 16,
98 +
textAlign: 'center',
99 +
lineHeight: 24,
100 +
},
101 +
footer: {
102 +
marginTop: 'auto',
103 +
paddingTop: 24,
104 +
},
105 +
});