From 0158826049132bf76540f6643c2dbd5d0efcd62b Mon Sep 17 00:00:00 2001 From: Thibaud Moustier Date: Fri, 27 Feb 2026 21:15:30 +0100 Subject: [PATCH] "Mobile : Supp accountStroage et AccountProfile - rempalcement par authUsersStorage" --- Wallette/mobile/src/models/AccountProfile.ts | 7 - Wallette/mobile/src/screens/AboutScreen.tsx | 98 ++++++++++-- Wallette/mobile/src/screens/AccountScreen.tsx | 145 ++++++++++-------- Wallette/mobile/src/utils/accountStorage.ts | 50 ------ Wallette/mobile/src/utils/authUsersStorage.ts | 67 ++++++-- 5 files changed, 224 insertions(+), 143 deletions(-) delete mode 100644 Wallette/mobile/src/models/AccountProfile.ts delete mode 100644 Wallette/mobile/src/utils/accountStorage.ts diff --git a/Wallette/mobile/src/models/AccountProfile.ts b/Wallette/mobile/src/models/AccountProfile.ts deleted file mode 100644 index 93ebefb..0000000 --- a/Wallette/mobile/src/models/AccountProfile.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface AccountProfile { - displayName: string; - email: string; - // On ne stocke JAMAIS un mot de passe en clair. - // Ici on simule seulement un champ pour l'UI (Step 4 sans API). - updatedAtMs: number; -} \ No newline at end of file diff --git a/Wallette/mobile/src/screens/AboutScreen.tsx b/Wallette/mobile/src/screens/AboutScreen.tsx index f6ddadc..02e683e 100644 --- a/Wallette/mobile/src/screens/AboutScreen.tsx +++ b/Wallette/mobile/src/screens/AboutScreen.tsx @@ -5,22 +5,65 @@ import { useEffect, useState } from "react"; import { ui } from "../components/ui/uiStyles"; import { loadSession } from "../utils/sessionStorage"; -import { loadAccountProfile } from "../utils/accountStorage"; +import { findUserById } from "../utils/authUsersStorage"; +/** + * AboutScreen + * ------------------------ + * "Détails du compte" + sections À propos / Support / Version. + * Les infos affichées viennent du compte local (AuthUsersStorage) : + * - displayName (optionnel) + * - username + * - email + */ export default function AboutScreen() { const [email, setEmail] = useState("—"); const [displayName, setDisplayName] = useState("Utilisateur"); + const [username, setUsername] = useState("—"); + const [userId, setUserId] = useState("—"); useEffect(() => { + let active = true; + async function init() { - const [session, profile] = await Promise.all([loadSession(), loadAccountProfile()]); - setEmail(profile?.email ?? session?.email ?? "—"); - setDisplayName(profile?.displayName ?? "Utilisateur"); + const session = await loadSession(); + if (!active) return; + + if (!session) { + setEmail("—"); + setDisplayName("Utilisateur"); + setUsername("—"); + setUserId("—"); + return; + } + + setUserId(session.userId); + setEmail(session.email); + + const user = await findUserById(session.userId); + if (!active) return; + + setUsername(user?.username ?? session.userId.replace("user_", "")); + setDisplayName(user?.displayName ?? "Utilisateur"); + setEmail(user?.email ?? session.email); } + init(); + + return () => { + active = false; + }; }, []); - const Row = ({ icon, title, subtitle }: { icon: any; title: string; subtitle?: string }) => ( + const Row = ({ + icon, + title, + subtitle, + }: { + icon: any; + title: string; + subtitle?: string; + }) => ( @@ -39,12 +82,25 @@ export default function AboutScreen() { {/* Carte profil (style WF-09) */} - + {displayName} - {email} + {email} + + @{username} + + + + UserId + {userId} + {/* À propos */} @@ -52,7 +108,11 @@ export default function AboutScreen() { À propos - + {/* Support */} @@ -69,8 +129,8 @@ export default function AboutScreen() { Build : Step 4 (sans API) - {/* Bouton discret */} - + {/* Bouton discret (optionnel) */} + Voir les licences (optionnel) @@ -85,6 +145,24 @@ const styles = StyleSheet.create({ profileRow: { flexDirection: "row", alignItems: "center", gap: 12 }, profileName: { fontSize: 18, fontWeight: "900", color: "#0f172a" }, + metaBox: { + marginTop: 12, + borderTopWidth: 1, + borderTopColor: "#e5e7eb", + paddingTop: 10, + }, + metaLabel: { + fontWeight: "900", + color: "#0f172a", + opacity: 0.7, + marginBottom: 4, + }, + metaValue: { + fontFamily: "monospace", + color: "#0f172a", + opacity: 0.85, + }, + row: { flexDirection: "row", alignItems: "center", diff --git a/Wallette/mobile/src/screens/AccountScreen.tsx b/Wallette/mobile/src/screens/AccountScreen.tsx index ab72035..d7be829 100644 --- a/Wallette/mobile/src/screens/AccountScreen.tsx +++ b/Wallette/mobile/src/screens/AccountScreen.tsx @@ -1,47 +1,68 @@ -import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert as RNAlert } from "react-native"; +import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native"; import { useEffect, useState } from "react"; import { SafeAreaView } from "react-native-safe-area-context"; import { ui } from "../components/ui/uiStyles"; import { loadSession } from "../utils/sessionStorage"; -import { loadAccountProfile, saveAccountProfile } from "../utils/accountStorage"; -import type { AccountProfile } from "../models/AccountProfile"; +import { findUserById, updateCurrentUserProfile } from "../utils/authUsersStorage"; /** * AccountScreen (Step 4 - sans API) * -------------------------------- - * UI prête pour email + mot de passe. - * En réalité : - * - on ne stocke pas le mdp - * - on sauvegarde seulement displayName + email (local) + * - Email : affiché mais NON modifiable (important) + * - Username : affiché mais NON modifiable (sinon userId local casse) + * - DisplayName : modifiable + * - Mot de passe : modifiable (hash local) * - * Quand l'API arrive : POST /auth/update-profile + update password. + * Plus tard : remplacement par une route API (DB). */ export default function AccountScreen() { - const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [userId, setUserId] = useState(""); + const [email, setEmail] = useState(""); // affichage only + const [username, setUsername] = useState(""); // affichage only const [displayName, setDisplayName] = useState(""); - const [email, setEmail] = useState(""); - // Champs UI (non persistés) pour mot de passe const [password, setPassword] = useState(""); const [password2, setPassword2] = useState(""); const [info, setInfo] = useState(null); + const [error, setError] = useState(null); useEffect(() => { + let active = true; + async function init() { - const [session, p] = await Promise.all([loadSession(), loadAccountProfile()]); - setUserId(session?.userId ?? ""); - setProfile(p); - setDisplayName(p.displayName); - setEmail(p.email); + const session = await loadSession(); + if (!session) { + if (!active) return; + setError("Session absente."); + setLoading(false); + return; + } + + const u = await findUserById(session.userId); + + if (!active) return; + + setUserId(session.userId); + setEmail(u?.email ?? session.email); + setUsername(u?.username ?? session.userId.replace("user_", "")); + setDisplayName(u?.displayName ?? ""); + + setLoading(false); } + init(); + + return () => { + active = false; + }; }, []); - if (!profile) { + if (loading) { return ( Chargement du compte… @@ -51,45 +72,37 @@ export default function AccountScreen() { const handleSave = async () => { setInfo(null); + setError(null); - const normalizedEmail = email.trim().toLowerCase(); - const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail); - - if (!isValidEmail) { - setInfo("Email invalide."); - return; - } + // Mot de passe : optionnel, mais si rempli => validations + const wantsPasswordChange = password.trim().length > 0 || password2.trim().length > 0; - // Mot de passe : UI prête, mais pas stocké sans API - if (password || password2) { - if (password.length < 6) { - setInfo("Mot de passe trop court (min 6)."); + if (wantsPasswordChange) { + if (password.trim().length < 6) { + setError("Mot de passe trop court (min 6)."); return; } if (password !== password2) { - setInfo("Les mots de passe ne correspondent pas."); + setError("Les mots de passe ne correspondent pas."); return; } - - // Sans API : on ne peut pas réellement changer le mdp - RNAlert.alert( - "Mot de passe", - "Sans API, ce changement n’est pas appliqué (UI prête).", - [{ text: "OK" }] - ); } - const updated: AccountProfile = { - displayName: displayName.trim() || "Utilisateur", - email: normalizedEmail, - updatedAtMs: Date.now(), - }; + const res = await updateCurrentUserProfile({ + displayName, + newPassword: wantsPasswordChange ? password : undefined, + }); - await saveAccountProfile(updated); - setProfile(updated); + if (!res.ok) { + setError(res.message); + return; + } + + // reset password fields setPassword(""); setPassword2(""); - setInfo("Profil sauvegardé ✅"); + + setInfo("Profil sauvegardé ✅ (local, en attendant l’API)."); }; return ( @@ -100,35 +113,37 @@ export default function AccountScreen() { Informations - UserId (session) + UserId {userId || "—"} + Email (non modifiable) + + + Nom d’utilisateur (non modifiable) + + Nom affiché - - - Email - + + + + ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation). + - Mot de passe (UI prête) + Mot de passe - Le changement réel du mot de passe sera branché quand l’API auth sera disponible. + Change optionnel. (Local pour l’instant, sera relié à l’API plus tard.) Nouveau mot de passe - + Confirmation - + + {!!error && {error}} {!!info && {info}} @@ -141,8 +156,8 @@ export default function AccountScreen() { const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor }, + title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" }, - fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 }, mono: { marginTop: 6, @@ -161,4 +176,14 @@ const styles = StyleSheet.create({ backgroundColor: "#fff", color: "#0f172a", }, + + inputDisabled: { + backgroundColor: "#f3f4f6", + color: "#111827", + opacity: 0.8, + }, + + fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 }, + + errorText: { color: "#dc2626", fontWeight: "900", marginBottom: 10 }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/utils/accountStorage.ts b/Wallette/mobile/src/utils/accountStorage.ts deleted file mode 100644 index cf15063..0000000 --- a/Wallette/mobile/src/utils/accountStorage.ts +++ /dev/null @@ -1,50 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import type { AccountProfile } from "../models/AccountProfile"; -import { loadSession } from "./sessionStorage"; - -function keyFor(userId: string) { - return `accountProfile:${userId}`; -} - -const DEFAULT_PROFILE: AccountProfile = { - displayName: "Utilisateur", - email: "demo@example.com", - updatedAtMs: Date.now(), -}; - -export async function loadAccountProfile(): Promise { - const session = await loadSession(); - if (!session) return DEFAULT_PROFILE; - - const raw = await AsyncStorage.getItem(keyFor(session.userId)); - if (!raw) { - // Par défaut, on reprend l’email de session - return { ...DEFAULT_PROFILE, email: session.email }; - } - - try { - const parsed = JSON.parse(raw) as Partial; - return { - ...DEFAULT_PROFILE, - ...parsed, - email: String(parsed.email ?? session.email), - displayName: String(parsed.displayName ?? DEFAULT_PROFILE.displayName), - updatedAtMs: Number(parsed.updatedAtMs ?? Date.now()), - }; - } catch { - return { ...DEFAULT_PROFILE, email: session.email }; - } -} - -export async function saveAccountProfile(profile: AccountProfile): Promise { - const session = await loadSession(); - if (!session) return; - - const safe: AccountProfile = { - displayName: profile.displayName.trim() || "Utilisateur", - email: profile.email.trim().toLowerCase() || session.email, - updatedAtMs: Date.now(), - }; - - await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(safe)); -} \ No newline at end of file diff --git a/Wallette/mobile/src/utils/authUsersStorage.ts b/Wallette/mobile/src/utils/authUsersStorage.ts index aa45bf5..0ab6c42 100644 --- a/Wallette/mobile/src/utils/authUsersStorage.ts +++ b/Wallette/mobile/src/utils/authUsersStorage.ts @@ -1,15 +1,9 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import * as Crypto from "expo-crypto"; import type { AuthUser } from "../models/AuthUser"; +import { loadSession } from "./sessionStorage"; const KEY = "authUsers"; - -/** - * ⚠️ Salt local (demo) - * ------------------- - * Pour un vrai système, le salt doit être par user et géré côté serveur + bcrypt/argon2. - * Ici : on fait une version éducative + défendable (pas de mdp en clair). - */ const LOCAL_SALT = "wall-e-tte_local_salt_v1"; async function sha256(input: string): Promise { @@ -48,11 +42,11 @@ export async function findUserByLogin(login: string): Promise { return found ?? null; } -/** - * Crée un utilisateur local (Register). - * - Vérifie unicité email + username - * - Hash le mot de passe - */ +export async function findUserById(userId: string): Promise { + const users = await loadUsers(); + return users.find((u) => u.userId === userId) ?? null; +} + export async function createUser(params: { email: string; username: string; @@ -75,7 +69,7 @@ export async function createUser(params: { const passwordHash = await hashPassword(params.password); const user: AuthUser = { - userId: `user_${username}`, // stable et simple pour le local + userId: `user_${username}`, email, username, displayName: displayName || undefined, @@ -87,9 +81,6 @@ export async function createUser(params: { return { ok: true, user }; } -/** - * Vérifie mot de passe pour un login (email OU username). - */ export async function verifyLogin(params: { login: string; password: string; @@ -105,4 +96,48 @@ export async function verifyLogin(params: { } return { ok: true, user }; +} + +/** + * Update profil (local, Step 4 sans API) + * - email NON modifiable ici (on le garde tel quel) + * - username NON modifiable ici (sinon ça casse userId local) + * - displayName modifiable + * - password modifiable (hashé) + * + * Plus tard : remplacer par un appel REST vers le serveur. + */ +export async function updateCurrentUserProfile(params: { + displayName?: string; + newPassword?: string; +}): Promise<{ ok: true; user: AuthUser } | { ok: false; message: string }> { + const session = await loadSession(); + if (!session) return { ok: false, message: "Session absente." }; + + const users = await loadUsers(); + const idx = users.findIndex((u) => u.userId === session.userId); + if (idx < 0) return { ok: false, message: "Utilisateur introuvable." }; + + const current = users[idx]; + + let next: AuthUser = { ...current }; + + // displayName (optionnel) + if (typeof params.displayName === "string") { + const dn = params.displayName.trim(); + next.displayName = dn ? dn : undefined; + } + + // mot de passe (optionnel) + if (params.newPassword) { + if (params.newPassword.trim().length < 6) { + return { ok: false, message: "Mot de passe trop court (min 6)." }; + } + next.passwordHash = await hashPassword(params.newPassword); + } + + users[idx] = next; + await saveUsers(users); + + return { ok: true, user: next }; } \ No newline at end of file -- 2.50.1