recover password feature

This commit is contained in:
2026-05-26 11:36:09 +01:00
parent b427fb0f85
commit b48f7783c9
12 changed files with 527 additions and 97 deletions

View File

@@ -0,0 +1,52 @@
import styles from '@/styles/screens/auth/recover.styles';
import { ReactNode } from 'react';
import {
Image,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
StatusBar,
Text,
View,
} from 'react-native';
type Props = {
title: string;
subtitle: string;
children: ReactNode;
};
export function RecoverScreenLayout({ title, subtitle, children }: Props) {
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}>
<StatusBar barStyle="light-content" />
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
bounces={false}>
<ImageBackground
source={require('@/assets/images/banner-login.png')}
style={styles.hero}
imageStyle={styles.heroImage}>
<View style={styles.overlay} />
<View style={styles.heroContent}>
<Image
source={require('@/assets/icons/logotipo-branco.png')}
style={styles.logo}
resizeMode="contain"
/>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
</ImageBackground>
<View style={styles.formCard}>{children}</View>
</ScrollView>
</KeyboardAvoidingView>
);
}

View File

@@ -9,7 +9,6 @@
export const API_BASE_URL = "https://apmtests.webclientes.com/pt/app";
export const API_BASE_URL_DEV = "https://apmtests.webclientes.com/pt/app";
export const API_URL_RECUPERAR_PALAVRA_PASSE = "https://gurudasviagens.pt/pt/areareservada/recuperarPassword/";
/**
* Endpoints da aplicação
* Adicione novos endpoints aqui conforme necessário
@@ -19,7 +18,10 @@ export const API_ENDPOINTS = {
USER_LOGIN: "/UserLogin",
USER_LOGOUT: "/UserLogout",
UPDATE_PROFILE: "/UserEditProfile",
RECOVER_PASSWORD: "/recoverPassword",
CONFIRM_TOKEN: "/confirmToken",
RESET_PASSWORD: "/resetPassword",
// Utilizador
USER_RESERVAS: "/getUserReservas",
USER_INFO: "/UserInfo",

View File

@@ -2,6 +2,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoJS from 'crypto-js';
import { createContext, useContext, useEffect, useState } from 'react';
import { API_CONFIG, API_ENDPOINTS, buildApiUrl } from '../config/api';
import { clearDownloadedDocuments } from '../services/documentSync';
import { clearUserSessionCache } from '../services/offlineStorage';
import { AuthContextType, AuthProviderProps, LoginResponse, User, UserData } from '../types';
// Contexto
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -74,6 +76,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
throw new Error(data.message || 'Erro ao fazer login');
}
// Limpar dados do utilizador anterior antes de guardar a nova sessão
await clearUserSessionCache();
await clearDownloadedDocuments();
// Se a resposta for bem-sucedida, Guardar token e user
if (data.token) {
setToken(data.token);
@@ -186,7 +192,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
setToken(null);
setError(null);
// Limpar AsyncStorage
await clearUserSessionCache();
await clearDownloadedDocuments();
// Limpar credenciais de sessão
await Promise.all([
AsyncStorage.removeItem(STORAGE_KEYS.TOKEN),
AsyncStorage.removeItem(STORAGE_KEYS.USER),

View File

@@ -128,6 +128,23 @@ export const verificarDocumentosParaDownload = async (
return para;
};
/** Remove documentos descarregados e checksums (ex.: no logout). */
export const clearDownloadedDocuments = async (): Promise<void> => {
try {
const keys = await AsyncStorage.getAllKeys();
const docKeys = keys.filter((key) => key.startsWith("@cruiseLovers:documento:"));
if (docKeys.length > 0) {
await AsyncStorage.multiRemove(docKeys);
}
const info = await FileSystem.getInfoAsync(DOCUMENTOS_DIR);
if (info.exists) {
await FileSystem.deleteAsync(DOCUMENTOS_DIR, { idempotent: true });
}
} catch (error) {
console.error("Erro ao limpar documentos locais:", error);
}
};
// Descarrega múltiplos documentos
export const downloadDocumentos = async (
documentos: DocumentoChecksum[],

View File

@@ -166,9 +166,11 @@ export const clearCache = async (): Promise<void> => {
export const clearReservasCache = async (): Promise<void> => {
try {
const keys = await AsyncStorage.getAllKeys();
const reservaKeys = keys.filter(key =>
key === STORAGE_KEYS.RESERVAS ||
key.startsWith(STORAGE_KEYS.RESERVA_DETAIL)
const reservaKeys = keys.filter(
(key) =>
key === STORAGE_KEYS.RESERVAS ||
key.startsWith(STORAGE_KEYS.RESERVA_DETAIL) ||
key.startsWith(STORAGE_KEYS.RESERVA_FULL),
);
await AsyncStorage.multiRemove(reservaKeys);
} catch (error) {
@@ -176,6 +178,22 @@ export const clearReservasCache = async (): Promise<void> => {
}
};
/** Dados de sessão do utilizador — mantém contactos da agência (partilhados). */
export const clearUserSessionCache = async (): Promise<void> => {
try {
const keys = await AsyncStorage.getAllKeys();
const sessionKeys = keys.filter(
(key) =>
key.startsWith("@cruiseLovers:") &&
key !== STORAGE_KEYS.CONTACTS &&
key !== STORAGE_KEYS.SOCIALS,
);
await AsyncStorage.multiRemove(sessionKeys);
} catch (error) {
console.error("Erro ao limpar cache da sessão:", error);
}
};
/**
* Armazena informações do perfil no cache
*/

View File

@@ -0,0 +1,50 @@
import { API_ENDPOINTS, buildApiUrl } from '@/assets/config/api';
import { ApiMessageResponse } from '@/assets/types';
import CryptoJS from 'crypto-js';
async function postToEndpoint(
endpoint: string,
fields: Record<string, string>,
): Promise<ApiMessageResponse> {
const formData = new FormData();
Object.entries(fields).forEach(([key, value]) => formData.append(key, value));
const response = await fetch(buildApiUrl(endpoint), {
method: 'POST',
headers: { Accept: 'application/json' },
body: formData,
});
return response.json();
}
export async function recoverPassword(email: string): Promise<ApiMessageResponse> {
return postToEndpoint(API_ENDPOINTS.RECOVER_PASSWORD, { email: email.trim() });
}
export async function confirmRecoveryToken(
email: string,
token: string,
): Promise<ApiMessageResponse> {
return postToEndpoint(API_ENDPOINTS.CONFIRM_TOKEN, {
email: email.trim(),
token: token.trim(),
});
}
export async function resetPassword(
email: string,
token: string,
password: string,
confirmPassword: string,
): Promise<ApiMessageResponse> {
const encryptedPassword = CryptoJS.MD5(password).toString();
const encryptedConfirm = CryptoJS.MD5(confirmPassword).toString();
return postToEndpoint(API_ENDPOINTS.RESET_PASSWORD, {
email: email.trim(),
token: token.trim(),
password: encryptedPassword,
confirmPassword: encryptedConfirm,
});
}

View File

@@ -34,6 +34,11 @@ export interface LoginResponse {
[key: string]: any;
}
export interface ApiMessageResponse {
status: number;
message?: string;
}
export interface UserData {
email: string;
nome: string;