Files
MeowSpool/mobile/app/(auth)/login.tsx
T

241 lines
7.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import axios from 'axios';
import { Link, useRouter } from 'expo-router';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Ionicons } from '@expo/vector-icons';
import * as Google from 'expo-auth-session/providers/google';
import { Screen } from '@presentation/components/layout/Screen';
import { Input } from '@presentation/components/ui/Input';
import { Button } from '@presentation/components/ui/Button';
import { colors, typography, spacing } from '@shared/theme';
import { loginUseCase, googleLoginUseCase } from '@infrastructure/container';
import { useAuthStore } from '@store/authStore';
import { CatIcon } from '@presentation/components/icons/CatIcon';
const schema = z.object({
email: z.string().email('E-mail inválido'),
password: z.string().min(1, 'Senha obrigatória'),
});
type FormData = z.infer<typeof schema>;
/**
* Tela de Login — G3-0
* E-mail + Senha, "Esqueci minha senha" e OAuth Google.
*/
export default function LoginScreen(): React.ReactElement {
const router = useRouter();
const setSession = useAuthStore((s) => s.setSession);
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
// Hook do expo-auth-session para Google OAuth.
// Usa os client IDs de Android e iOS configurados via variáveis de ambiente.
const [_request, response, promptAsync] = Google.useAuthRequest({
androidClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID,
iosClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID_IOS,
});
// Reage ao resultado do fluxo OAuth assim que o browser fecha
useEffect(() => {
if (response?.type === 'success') {
const idToken = response.authentication?.idToken;
if (!idToken) {
Alert.alert('Erro', 'Não foi possível obter o token do Google.');
setIsGoogleLoading(false);
return;
}
googleLoginUseCase
.execute({ idToken })
.then((session) => setSession(session))
.then(() => router.replace('/(app)/(tabs)/home'))
.catch(() => Alert.alert('Erro', 'Não foi possível entrar com o Google.'))
.finally(() => setIsGoogleLoading(false));
} else if (response?.type === 'error') {
Alert.alert('Erro', 'Autenticação com Google cancelada ou falhou.');
setIsGoogleLoading(false);
} else if (response?.type === 'dismiss') {
setIsGoogleLoading(false);
}
}, [response]);
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '' },
});
async function onSubmit(data: FormData): Promise<void> {
setIsLoading(true);
try {
const session = await loginUseCase.execute(data);
await setSession(session);
router.replace('/(app)/(tabs)/home');
} catch (err) {
if (axios.isAxiosError(err) && err.response?.data?.code === 'EMAIL_NOT_VERIFIED') {
Alert.alert(
'E-mail não verificado',
'Confirme seu e-mail antes de entrar. Verifique sua caixa de entrada.',
[{ text: 'OK', onPress: () => router.push(`/(auth)/check-email?email=${encodeURIComponent(data.email)}`) }],
);
} else {
Alert.alert('Erro', 'E-mail ou senha incorretos.');
}
} finally {
setIsLoading(false);
}
}
function onGoogleLogin(): void {
setIsGoogleLoading(true);
// promptAsync abre o browser; o resultado chega via useEffect no `response`
promptAsync();
}
return (
<Screen scrollable keyboardAvoiding>
{/* Logo */}
<View style={styles.hero}>
<CatIcon size={48} color="#1E1B18" withBackground backgroundColor={colors.accent} />
<Text style={styles.appName}>MeowSpool</Text>
<Text style={styles.tagline}>Gerencie seus filamentos com precisão.</Text>
</View>
{/* Formulário */}
<View style={styles.form}>
<Controller
control={control}
name="email"
render={({ field }) => (
<Input
label="E-mail"
placeholder="seu@email.com"
keyboardType="email-address"
autoComplete="email"
leftIcon={<Ionicons name="mail-outline" size={18} color={colors.textSecondary} />}
error={errors.email?.message}
onChangeText={field.onChange}
value={field.value}
/>
)}
/>
<Controller
control={control}
name="password"
render={({ field }) => (
<Input
label="Senha"
placeholder="••••••••"
isPassword
leftIcon={<Ionicons name="lock-closed-outline" size={18} color={colors.textSecondary} />}
error={errors.password?.message}
onChangeText={field.onChange}
value={field.value}
/>
)}
/>
<TouchableOpacity onPress={() => router.push('/(auth)/forgot-password')} style={styles.forgotRow}>
<Text style={styles.forgotText}>Esqueci minha senha</Text>
</TouchableOpacity>
<Button label="Entrar" onPress={handleSubmit(onSubmit)} isLoading={isLoading} />
{/* Divisor */}
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>ou continue com</Text>
<View style={styles.dividerLine} />
</View>
{/* Google */}
<Button
label="Entrar com Google"
variant="secondary"
leftIcon={<Ionicons name="logo-google" size={18} color={colors.textPrimary} />}
onPress={onGoogleLogin}
isLoading={isGoogleLoading}
/>
{/* Criar conta */}
<View style={styles.footer}>
<Text style={styles.footerText}>Não tem uma conta? </Text>
<Link href="/(auth)/register" asChild>
<TouchableOpacity>
<Text style={styles.footerLink}>Criar conta</Text>
</TouchableOpacity>
</Link>
</View>
</View>
</Screen>
);
}
const styles = StyleSheet.create({
hero: {
alignItems: 'center',
paddingTop: spacing[12],
paddingBottom: spacing[10],
gap: spacing[2],
},
appName: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize['2xl'],
fontWeight: typography.fontWeight.bold,
color: colors.textPrimary,
},
tagline: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.base,
color: colors.textSecondary,
},
form: {
gap: spacing[4],
paddingBottom: spacing[8],
},
forgotRow: {
alignSelf: 'flex-end',
marginTop: -spacing[2],
},
forgotText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.accent,
},
divider: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing[3],
marginVertical: spacing[2],
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: colors.border,
},
dividerText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: spacing[2],
},
footerText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
footerLink: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.accent,
fontWeight: typography.fontWeight.semibold,
},
});