+++ /dev/null
-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
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 }}>
{/* 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 */}
<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 */}
<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>
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",
-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>
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 (
<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}>
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,
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
+++ /dev/null
-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
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> {
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;
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,
return { ok: true, user };
}
-/**
- * Vérifie mot de passe pour un login (email OU username).
- */
export async function verifyLogin(params: {
login: string;
password: string;
}
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