eletrotupi / tcc/ frontend/app/auth/login.tsx2d27b5593cd012f3160a84c9560b68e2590925d8
6.9 KB | 265 lines Raw
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";
12
import { Link, router } from "expo-router";
13
import { SafeAreaView } from "react-native-safe-area-context";
14
import { StatusBar } from "expo-status-bar";
15
import { useThemeColor } from "@/hooks/use-theme-color";
16
import { Button, Input } from "@/components/ui";
17
import { useAuth } from "@/context/AuthContext";
18
import { apiClient, ApiError } from "@/lib/api";
19
import { useToast } from "@/context/ToastContext";
20
import { translateError, translateFields } from "@/lib/errors/translations";
21
import { registerForPushNotifications } from "@/hooks/useRegisterPushToken";
22

23
export default function LoginScreen() {
24
  const [email, setEmail] = useState("");
25
  const [password, setPassword] = useState("");
26
  const [errors, setErrors] = useState<{ email?: string; password?: string }>(
27
    {},
28
  );
29
  const [loading, setLoading] = useState(false);
30

31
  const { login, updateAuthUser } = useAuth();
32
  const { showToast } = useToast();
33
  const textColor = useThemeColor({}, "text");
34
  const backgroundColor = useThemeColor({}, "background");
35
  const tintColor = useThemeColor({}, "tint");
36

37
  const [spinValue] = useState(() => new Animated.Value(0));
38
  useEffect(() => {
39
    const animation = Animated.loop(
40
      Animated.timing(spinValue, {
41
        toValue: 1,
42
        duration: 12000, // 12s per full turn (slow)
43
        easing: Easing.linear,
44
        useNativeDriver: true,
45
      })
46
    );
47

48
    animation.start();
49

50
    return () => {
51
      animation.stop();
52
    };
53
  }, [spinValue]);
54

55
  const spin = spinValue.interpolate({
56
    inputRange: [0, 1],
57
    outputRange: ["0deg", "360deg"],
58
  });
59

60
  const validateForm = () => {
61
    const newErrors: { email?: string; password?: string } = {};
62

63
    if (!email) {
64
      newErrors.email = "Email é obrigatório";
65
    } else if (!/\S+@\S+\.\S+/.test(email)) {
66
      newErrors.email = "Por favor, use um email válido";
67
    }
68

69
    if (!password) {
70
      newErrors.password = "Senha é obrigatória";
71
    }
72

73
    setErrors(newErrors);
74
    return Object.keys(newErrors).length === 0;
75
  };
76

77
  const handleLogin = async () => {
78
    if (!validateForm()) return;
79

80
    setLoading(true);
81
    try {
82
      const response = await login(email, password);
83

84
      if (response?.user) {
85
        const token = await registerForPushNotifications();
86
        if (token) {
87
          const updateResponse = await apiClient.updateUser({
88
            id: response.user.id,
89
            pushToken: token,
90
          });
91

92
          console.log("UPDATE USER response: ", updateResponse);
93

94
          if (updateResponse) {
95
            updateAuthUser(updateResponse.user);
96
          }
97
        }
98
      }
99

100
      router.replace("/");
101
    } catch (error) {
102
      console.log("LOGIN error: ", error);
103
      if (error instanceof ApiError && error.fields) {
104
        setErrors(translateFields(error.fields));
105
      } else {
106
        showToast(translateError(error) || 'Falha ao fazer login', 'error');
107
      }
108
    } finally {
109
      setLoading(false);
110
    }
111
  };
112

113
  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"
149
            />
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

183
          <View style={styles.footer}>
184
            <Text
185
              style={[styles.footerText, { color: textColor, opacity: 0.7 }]}
186
            >
187
              Não tem uma conta ainda?{" "}
188
            </Text>
189

190
            <Link
191
              href="/auth/signup"
192
              asChild
193
              style={StyleSheet.flatten([styles.link, { color: tintColor }])}
194
            >
195
              <Text
196
                style={StyleSheet.flatten([styles.footerText, { color: textColor, opacity: 0.7 }])}
197
              >
198
                Cadastre-se
199
              </Text>
200
            </Link>
201
          </View>
202
        </ScrollView>
203
      </KeyboardAvoidingView>
204
    </SafeAreaView>
205
  );
206
}
207

208
const styles = StyleSheet.create({
209
  container: {
210
    flex: 1,
211
  },
212
  keyboardView: {
213
    flex: 1,
214
  },
215
  logoContainer: {
216
    alignItems: "center",
217
    marginBottom: 40,
218
    marginTop: 10,
219
  },
220
  logo: {
221
    width: 120,
222
    height: 120,
223
  },
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
  form: {
244
    marginBottom: 32,
245
  },
246
  loginButton: {
247
    marginTop: 8,
248
    marginBottom: 16,
249
  },
250
  link: {
251
    textAlign: "center",
252
    fontSize: 14,
253
    fontWeight: "500",
254
  },
255
  footer: {
256
    flexDirection: "row",
257
    justifyContent: "center",
258
    alignItems: "center",
259
    marginTop: "auto",
260
    paddingBottom: 40,
261
  },
262
  footerText: {
263
    fontSize: 14,
264
  },
265
});