eletrotupi / tcc/ commit / c167b93

frontend/auth: add auth screens, login, sign up and reset password

Pedro Lucas Porcellis porcellis@eletrotupi.com · 6 months ago c167b931829e73be76860df22b78f8ac0951550e Browse files
Parents ae44a25
Changed 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 +
});