feat: implement email verification and password reset features

- Add EmailTokenService for handling email verification and password reset tokens.
- Create EmailService for sending verification and reset emails via SMTP.
- Update AuthService to handle email verification status during login.
- Modify user registration to redirect to a check email screen instead of issuing a token.
- Implement resend verification email functionality.
- Add deep link handling for email verification and password reset in the mobile app.
- Update mobile app routes and components to support new email verification flow.
- Enhance error handling for unverified emails during login attempts.
- Update configuration to include SMTP settings for email service.
This commit is contained in:
2026-03-14 23:38:15 -03:00
parent f5f4e878b4
commit d5af338ee1
38 changed files with 1146 additions and 75 deletions
+136
View File
@@ -0,0 +1,136 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams } from 'expo-router';
import { Screen } from '@presentation/components/layout/Screen';
import { Header } from '@presentation/components/layout/Header';
import { Button } from '@presentation/components/ui/Button';
import { colors, typography, spacing, radius } from '@shared/theme';
import { resendVerificationUseCase } from '@infrastructure/container';
const COOLDOWN_SECS = 60;
/**
* Tela de Verificação de E-mail — RU-0
* Mostrada após o cadastro bem-sucedido.
*/
export default function CheckEmailScreen(): React.ReactElement {
const { email } = useLocalSearchParams<{ email: string }>();
const [isLoading, setIsLoading] = useState(false);
const [cooldown, setCooldown] = useState(0);
const [sent, setSent] = useState(false);
useEffect(() => {
if (cooldown <= 0) return;
const timer = setTimeout(() => setCooldown((c) => c - 1), 1000);
return () => clearTimeout(timer);
}, [cooldown]);
async function handleResend(): Promise<void> {
if (!email || cooldown > 0) return;
setIsLoading(true);
try {
await resendVerificationUseCase.execute(email);
setSent(true);
setCooldown(COOLDOWN_SECS);
} finally {
setIsLoading(false);
}
}
return (
<Screen>
<Header title="Verificar e-mail" showBack />
<View style={styles.content}>
<View style={styles.iconWrapper}>
<Ionicons name="mail-outline" size={64} color={colors.accent} />
</View>
<Text style={styles.title}>Confirme seu e-mail</Text>
<Text style={styles.description}>
Enviamos um link de verificação para o seu e-mail. Clique no link para ativar sua conta e poder fazer login.
</Text>
{email && (
<Button
label={
cooldown > 0
? `Reenviar em ${cooldown}s`
: sent
? 'Reenviar e-mail'
: 'Reenviar e-mail'
}
variant="secondary"
onPress={handleResend}
isLoading={isLoading}
disabled={cooldown > 0}
style={styles.resendButton}
/>
)}
{sent && cooldown > 0 && (
<Text style={styles.sentText}>E-mail reenviado! Verifique sua caixa de entrada.</Text>
)}
<View style={styles.hint}>
<Ionicons name="information-circle-outline" size={16} color={colors.textSecondary} />
<Text style={styles.hintText}>Não recebeu? Verifique sua pasta de spam.</Text>
</View>
</View>
</Screen>
);
}
const styles = StyleSheet.create({
content: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: spacing[5],
paddingHorizontal: spacing[4],
},
iconWrapper: {
width: 100,
height: 100,
borderRadius: radius.xl,
backgroundColor: colors.bgSurface,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing[2],
},
title: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold,
color: colors.textPrimary,
textAlign: 'center',
},
description: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.base,
color: colors.textSecondary,
textAlign: 'center',
lineHeight: typography.fontSize.base * 1.6,
},
resendButton: {
width: '100%',
},
sentText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.accent,
textAlign: 'center',
},
hint: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing[2],
marginTop: spacing[2],
},
hintText: {
fontFamily: typography.fontFamily.ui,
fontSize: typography.fontSize.sm,
color: colors.textSecondary,
},
});