]> git.digitality.be Git - pdw25-26/commitdiff
"Mobile : Supp accountStroage et AccountProfile - rempalcement par authUsersStorage"
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Fri, 27 Feb 2026 20:15:30 +0000 (21:15 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Fri, 27 Feb 2026 20:15:30 +0000 (21:15 +0100)
Wallette/mobile/src/models/AccountProfile.ts [deleted file]
Wallette/mobile/src/screens/AboutScreen.tsx
Wallette/mobile/src/screens/AccountScreen.tsx
Wallette/mobile/src/utils/accountStorage.ts [deleted file]
Wallette/mobile/src/utils/authUsersStorage.ts

diff --git a/Wallette/mobile/src/models/AccountProfile.ts b/Wallette/mobile/src/models/AccountProfile.ts
deleted file mode 100644 (file)
index 93ebefb..0000000
+++ /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
index f6ddadcf05b49dcfe9241e0d44f2006b9693863a..02e683ecc58cb9e77a067f859c5eec1ac59d9b34 100644 (file)
@@ -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<string>("—");
   const [displayName, setDisplayName] = useState<string>("Utilisateur");
+  const [username, setUsername] = useState<string>("—");
+  const [userId, setUserId] = useState<string>("—");
 
   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;
+  }) => (
     <View style={styles.row}>
       <Ionicons name={icon} size={18} color="#0f172a" style={{ opacity: 0.75 }} />
       <View style={{ flex: 1 }}>
@@ -39,12 +82,25 @@ export default function AboutScreen() {
         {/* Carte profil (style WF-09) */}
         <View style={ui.card}>
           <View style={styles.profileRow}>
-            <Ionicons name="person-circle-outline" size={52} color="#0f172a" style={{ opacity: 0.75 }} />
+            <Ionicons
+              name="person-circle-outline"
+              size={52}
+              color="#0f172a"
+              style={{ opacity: 0.75 }}
+            />
             <View style={{ flex: 1 }}>
               <Text style={styles.profileName}>{displayName}</Text>
-              <Text style={ui.muted}>{email}</Text>
+              <Text style={ui.muted} numberOfLines={1}>{email}</Text>
+              <Text style={[ui.muted, { marginTop: 2 }]} numberOfLines={1}>
+                @{username}
+              </Text>
             </View>
           </View>
+
+          <View style={styles.metaBox}>
+            <Text style={styles.metaLabel}>UserId</Text>
+            <Text style={styles.metaValue} numberOfLines={1}>{userId}</Text>
+          </View>
         </View>
 
         {/* À propos */}
@@ -52,7 +108,11 @@ export default function AboutScreen() {
           <Text style={ui.title}>À propos</Text>
 
           <Row icon="sparkles-outline" title="Wall-e-tte" subtitle="Conseiller crypto — projet PDW 2025-2026" />
-          <Row icon="shield-checkmark-outline" title="Avertissement" subtitle="Outil éducatif : ce n’est pas un conseil financier." />
+          <Row
+            icon="shield-checkmark-outline"
+            title="Avertissement"
+            subtitle="Outil éducatif : ce n’est pas un conseil financier."
+          />
         </View>
 
         {/* Support */}
@@ -69,8 +129,8 @@ export default function AboutScreen() {
           <Text style={[ui.muted, { marginTop: 6 }]}>Build : Step 4 (sans API)</Text>
         </View>
 
-        {/* Bouton discret */}
-        <TouchableOpacity style={[styles.secondaryButton]}>
+        {/* Bouton discret (optionnel) */}
+        <TouchableOpacity style={styles.secondaryButton}>
           <Text style={styles.secondaryButtonText}>Voir les licences (optionnel)</Text>
         </TouchableOpacity>
       </View>
@@ -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",
index ab72035298716abd25a4a15cc61e264a2f659872..d7be82965aa1e71cef344427cb864c449a2e10ac 100644 (file)
@@ -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<AccountProfile | null>(null);
+  const [loading, setLoading] = useState(true);
+
   const [userId, setUserId] = useState<string>("");
 
+  const [email, setEmail] = useState<string>(""); // affichage only
+  const [username, setUsername] = useState<string>(""); // affichage only
   const [displayName, setDisplayName] = useState<string>("");
-  const [email, setEmail] = useState<string>("");
 
-  // Champs UI (non persistés) pour mot de passe
   const [password, setPassword] = useState<string>("");
   const [password2, setPassword2] = useState<string>("");
 
   const [info, setInfo] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(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 (
       <View style={ui.centered}>
         <Text>Chargement du compte…</Text>
@@ -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() {
         <View style={ui.card}>
           <Text style={ui.title}>Informations</Text>
 
-          <Text style={ui.muted}>UserId (session)</Text>
+          <Text style={ui.muted}>UserId</Text>
           <Text style={styles.mono}>{userId || "—"}</Text>
 
+          <Text style={[ui.muted, { marginTop: 10 }]}>Email (non modifiable)</Text>
+          <TextInput value={email} editable={false} style={[styles.input, styles.inputDisabled]} />
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur (non modifiable)</Text>
+          <TextInput value={username} editable={false} style={[styles.input, styles.inputDisabled]} />
+
           <Text style={[ui.muted, { marginTop: 10 }]}>Nom affiché</Text>
-          <TextInput value={displayName} onChangeText={setDisplayName} style={styles.input} />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Email</Text>
-          <TextInput
-            value={email}
-            onChangeText={setEmail}
-            autoCapitalize="none"
-            keyboardType="email-address"
-            style={styles.input}
-          />
+          <TextInput value={displayName} onChangeText={setDisplayName} placeholder="ex: Thibaud M." style={styles.input} />
+
+          <Text style={[ui.muted, { marginTop: 12 }]}>
+            ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation).
+          </Text>
         </View>
 
         <View style={ui.card}>
-          <Text style={ui.title}>Mot de passe (UI prête)</Text>
+          <Text style={ui.title}>Mot de passe</Text>
           <Text style={ui.muted}>
-            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.)
           </Text>
 
           <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
-          <TextInput value={password} onChangeText={setPassword} secureTextEntry style={styles.input} />
+          <TextInput value={password} onChangeText={setPassword} secureTextEntry placeholder="min 6 caractères" style={styles.input} />
 
           <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
-          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry style={styles.input} />
+          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry placeholder="retaper le mot de passe" style={styles.input} />
         </View>
 
+        {!!error && <Text style={styles.errorText}>{error}</Text>}
         {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
 
         <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
@@ -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 (file)
index cf15063..0000000
+++ /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<AccountProfile> {
-  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<AccountProfile>;
-    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<void> {
-  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
index aa45bf5c0045102df31721616e1d3b0f04596364..0ab6c424ff2af212cdecd5c0ce83d49e2a05d350 100644 (file)
@@ -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<string> {
@@ -48,11 +42,11 @@ export async function findUserByLogin(login: string): Promise<AuthUser | null> {
   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<AuthUser | null> {
+  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