From: Thibaud Moustier Date: Wed, 25 Feb 2026 21:09:16 +0000 (+0100) Subject: Mobile : Creation fenetre Account + modification Dashboard + autres X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=144fe4291dcd4be0300d5289235f52a11d1b3aed;p=pdw25-26 Mobile : Creation fenetre Account + modification Dashboard + autres --- diff --git a/Wallette/mobile/App.tsx b/Wallette/mobile/App.tsx index 0e33c56..d33c4c3 100644 --- a/Wallette/mobile/App.tsx +++ b/Wallette/mobile/App.tsx @@ -1,6 +1,6 @@ -import { NavigationContainer } from "@react-navigation/native"; +import { NavigationContainer, createNavigationContainerRef } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { TouchableOpacity, View, Text } from "react-native"; +import { TouchableOpacity, View, Text, Alert as RNAlert } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useEffect, useState } from "react"; @@ -11,8 +11,11 @@ import AlertsScreen from "./src/screens/AlertsScreen"; import StrategyScreen from "./src/screens/StrategyScreen"; import WalletScreen from "./src/screens/WalletScreen"; import AuthScreen from "./src/screens/AuthScreen"; +import AccountScreen from "./src/screens/AccountScreen"; +import AboutScreen from "./src/screens/AboutScreen"; import { loadSession, clearSession } from "./src/utils/sessionStorage"; +import AccountMenu from "./src/components/AccountMenu"; export type RootStackParamList = { Dashboard: undefined; @@ -21,21 +24,31 @@ export type RootStackParamList = { Alerts: undefined; Strategy: undefined; Wallet: undefined; + Account: undefined; + About: undefined; }; const Stack = createNativeStackNavigator(); +// ✅ navigationRef (permet de naviguer hors des screens, ex: depuis un Modal) +export const navigationRef = createNavigationContainerRef(); + export default function App() { const [ready, setReady] = useState(false); const [isAuthed, setIsAuthed] = useState(false); + const [menuVisible, setMenuVisible] = useState(false); + const [sessionEmail, setSessionEmail] = useState("—"); + useEffect(() => { let active = true; async function init() { const s = await loadSession(); if (!active) return; + setIsAuthed(!!s); + setSessionEmail(s?.email ?? "—"); setReady(true); } @@ -46,6 +59,27 @@ export default function App() { }; }, []); + const doLogout = () => { + RNAlert.alert("Déconnexion", "Se déconnecter du compte ?", [ + { text: "Annuler", style: "cancel" }, + { + text: "Déconnexion", + style: "destructive", + onPress: async () => { + await clearSession(); + setIsAuthed(false); + setSessionEmail("—"); + }, + }, + ]); + }; + + const go = (route: keyof RootStackParamList) => { + // ✅ safe guard : navigation prête ? + if (!navigationRef.isReady()) return; + navigationRef.navigate(route); + }; + if (!ready) { return ( @@ -58,16 +92,27 @@ export default function App() { if (!isAuthed) { return ( { + onAuthenticated={async () => { + const s = await loadSession(); + setSessionEmail(s?.email ?? "—"); setIsAuthed(true); }} /> ); } - // Connecté -> stack normal return ( - + + {/* ✅ Modal menu Compte (navigue grâce au navigationRef) */} + setMenuVisible(false)} + onGoAccount={() => go("Account")} + onGoAbout={() => go("About")} + onLogout={doLogout} + /> + ( - {/* ⚙️ Settings */} - navigation.navigate("Settings")}> - + {/* 👤 Menu Compte */} + setMenuVisible(true)}> + - {/* ⎋ Logout */} - { - await clearSession(); - setIsAuthed(false); - }} - > - + {/* ⚙️ Paramètres */} + navigation.navigate("Settings")}> + ), @@ -99,10 +139,11 @@ export default function App() { - - {() => setIsAuthed(false)} />} - + + + + + ); diff --git a/Wallette/mobile/src/components/AccountMenu.tsx b/Wallette/mobile/src/components/AccountMenu.tsx new file mode 100644 index 0000000..2be54cb --- /dev/null +++ b/Wallette/mobile/src/components/AccountMenu.tsx @@ -0,0 +1,119 @@ +import { Modal, View, Text, StyleSheet, TouchableOpacity, Pressable } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { ui } from "./ui/uiStyles"; + +type Props = { + visible: boolean; + email: string; + onClose: () => void; + + onGoAccount: () => void; + onGoAbout: () => void; + onLogout: () => void; +}; + +export default function AccountMenu({ + visible, + email, + onClose, + onGoAccount, + onGoAbout, + onLogout, +}: Props) { + return ( + + {/* Overlay */} + + {/* Stop propagation */} + null}> + + + + Compte + {email} + + + + + + + { onClose(); onGoAccount(); }}> + + Modification du compte + + + + { onClose(); onGoAbout(); }}> + + À propos + + + + { onClose(); onLogout(); }}> + + Déconnexion + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: "rgba(15, 23, 42, 0.35)", + justifyContent: "flex-start", + paddingTop: 70, + paddingHorizontal: 16, + }, + + card: { + backgroundColor: "#fff", + borderRadius: 14, + borderWidth: 1, + borderColor: "#e5e7eb", + padding: 12, + }, + + headerRow: { + flexDirection: "row", + alignItems: "center", + gap: 10, + paddingBottom: 10, + borderBottomWidth: 1, + borderBottomColor: "#e5e7eb", + marginBottom: 10, + }, + + title: { + fontSize: 16, + fontWeight: "900", + color: "#0f172a", + }, + + item: { + flexDirection: "row", + alignItems: "center", + gap: 10, + paddingVertical: 12, + paddingHorizontal: 10, + borderRadius: 12, + }, + + itemDanger: { + marginTop: 6, + backgroundColor: "#dc262611", + }, + + itemIcon: { + width: 22, + opacity: 0.9, + }, + + itemText: { + flex: 1, + fontWeight: "900", + color: "#0f172a", + }, +}); \ No newline at end of file diff --git a/Wallette/mobile/src/models/AccountProfile.ts b/Wallette/mobile/src/models/AccountProfile.ts new file mode 100644 index 0000000..93ebefb --- /dev/null +++ b/Wallette/mobile/src/models/AccountProfile.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..ab72035 --- /dev/null +++ b/Wallette/mobile/src/screens/AboutScreen.tsx @@ -0,0 +1,164 @@ +import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert as RNAlert } 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"; + +/** + * 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) + * + * Quand l'API arrive : POST /auth/update-profile + update password. + */ +export default function AccountScreen() { + const [profile, setProfile] = useState(null); + const [userId, setUserId] = useState(""); + + 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); + + useEffect(() => { + async function init() { + const [session, p] = await Promise.all([loadSession(), loadAccountProfile()]); + setUserId(session?.userId ?? ""); + setProfile(p); + setDisplayName(p.displayName); + setEmail(p.email); + } + init(); + }, []); + + if (!profile) { + return ( + + Chargement du compte… + + ); + } + + const handleSave = async () => { + setInfo(null); + + const normalizedEmail = email.trim().toLowerCase(); + const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail); + + if (!isValidEmail) { + setInfo("Email invalide."); + return; + } + + // 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)."); + return; + } + if (password !== password2) { + setInfo("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(), + }; + + await saveAccountProfile(updated); + setProfile(updated); + setPassword(""); + setPassword2(""); + setInfo("Profil sauvegardé ✅"); + }; + + return ( + + + Modification du compte + + + Informations + + UserId (session) + {userId || "—"} + + Nom affiché + + + Email + + + + + Mot de passe (UI prête) + + Le changement réel du mot de passe sera branché quand l’API auth sera disponible. + + + Nouveau mot de passe + + + Confirmation + + + + {!!info && {info}} + + + Sauvegarder + + + + ); +} + +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, + fontFamily: "monospace", + color: "#0f172a", + opacity: 0.8, + }, + + input: { + borderWidth: 1, + borderColor: "#e5e7eb", + borderRadius: 10, + paddingHorizontal: 12, + paddingVertical: 10, + marginTop: 8, + backgroundColor: "#fff", + color: "#0f172a", + }, +}); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/AccountScreen.tsx b/Wallette/mobile/src/screens/AccountScreen.tsx new file mode 100644 index 0000000..ab72035 --- /dev/null +++ b/Wallette/mobile/src/screens/AccountScreen.tsx @@ -0,0 +1,164 @@ +import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert as RNAlert } 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"; + +/** + * 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) + * + * Quand l'API arrive : POST /auth/update-profile + update password. + */ +export default function AccountScreen() { + const [profile, setProfile] = useState(null); + const [userId, setUserId] = useState(""); + + 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); + + useEffect(() => { + async function init() { + const [session, p] = await Promise.all([loadSession(), loadAccountProfile()]); + setUserId(session?.userId ?? ""); + setProfile(p); + setDisplayName(p.displayName); + setEmail(p.email); + } + init(); + }, []); + + if (!profile) { + return ( + + Chargement du compte… + + ); + } + + const handleSave = async () => { + setInfo(null); + + const normalizedEmail = email.trim().toLowerCase(); + const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail); + + if (!isValidEmail) { + setInfo("Email invalide."); + return; + } + + // 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)."); + return; + } + if (password !== password2) { + setInfo("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(), + }; + + await saveAccountProfile(updated); + setProfile(updated); + setPassword(""); + setPassword2(""); + setInfo("Profil sauvegardé ✅"); + }; + + return ( + + + Modification du compte + + + Informations + + UserId (session) + {userId || "—"} + + Nom affiché + + + Email + + + + + Mot de passe (UI prête) + + Le changement réel du mot de passe sera branché quand l’API auth sera disponible. + + + Nouveau mot de passe + + + Confirmation + + + + {!!info && {info}} + + + Sauvegarder + + + + ); +} + +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, + fontFamily: "monospace", + color: "#0f172a", + opacity: 0.8, + }, + + input: { + borderWidth: 1, + borderColor: "#e5e7eb", + borderRadius: 10, + paddingHorizontal: 12, + paddingVertical: 10, + marginTop: 8, + backgroundColor: "#fff", + color: "#0f172a", + }, +}); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/AlertsScreen.tsx b/Wallette/mobile/src/screens/AlertsScreen.tsx index dff674a..7668c21 100644 --- a/Wallette/mobile/src/screens/AlertsScreen.tsx +++ b/Wallette/mobile/src/screens/AlertsScreen.tsx @@ -1,20 +1,25 @@ import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert as RNAlert } from "react-native"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { SafeAreaView } from "react-native-safe-area-context"; +import { ui } from "../components/ui/uiStyles"; import type { Alert } from "../types/Alert"; import { alertStore } from "../services/alertStore"; -import { ui } from "../components/ui/uiStyles"; /** - * Types locaux (évite dépendre d'exports qui ne sont pas toujours présents) + * AlertsScreen + * ----------- + * Liste des alertes reçues (Socket.IO) stockées dans alertStore. + * + * Objectifs : + * - Affichage clair des enums (CRITICAL/WARNING/INFO, BUY/SELL/HOLD/STOP_LOSS) + * - Tri : CRITICAL > WARNING > INFO, puis récent -> ancien + * - Filtre (par défaut : CRITICAL) + * - Clear avec confirmation */ -type AlertLevel = "CRITICAL" | "WARNING" | "INFO"; -type TradeDecision = "BUY" | "SELL" | "HOLD" | "STOP_LOSS"; - -type AlertFilter = "ALL" | AlertLevel; +type Filter = "CRITICAL" | "WARNING" | "INFO" | "ALL"; -function levelRank(level: AlertLevel) { +function severityRank(level: Alert["alertLevel"]): number { switch (level) { case "CRITICAL": return 3; @@ -26,19 +31,7 @@ function levelRank(level: AlertLevel) { } } -function getLevelColor(level: AlertLevel): string { - switch (level) { - case "CRITICAL": - return "#dc2626"; - case "WARNING": - return "#ca8a04"; - case "INFO": - default: - return "#2563eb"; - } -} - -function getActionColor(action: TradeDecision): string { +function actionColor(action: Alert["action"]): string { switch (action) { case "BUY": return "#16a34a"; @@ -52,74 +45,77 @@ function getActionColor(action: TradeDecision): string { } } -/** - * AlertsScreen - * ------------ - * Affiche les alertes reçues via Socket.IO (stockées dans alertStore). - * - * - Filtre optionnel (ALL / CRITICAL / WARNING / INFO) - * - Par défaut : ALL avec tri CRITICAL > WARNING > INFO puis plus récent - * - Bouton Clear avec confirmation - */ +function levelColor(level: Alert["alertLevel"]): string { + switch (level) { + case "CRITICAL": + return "#b91c1c"; + case "WARNING": + return "#ca8a04"; + case "INFO": + default: + return "#2563eb"; + } +} + +function formatDate(ms: number): string { + return new Date(ms).toLocaleString(); +} + export default function AlertsScreen() { - const [alerts, setAlerts] = useState([]); - const [filter, setFilter] = useState("ALL"); + const [items, setItems] = useState([]); + const [filter, setFilter] = useState("CRITICAL"); - // abonnement au store (temps réel) useEffect(() => { - const unsub = alertStore.subscribe(setAlerts); + // Load initial + setItems(alertStore.getAll?.() ?? []); + + // Live updates if subscribe exists + const unsub = alertStore.subscribe?.((all: Alert[]) => { + setItems(all); + }); + return () => { - unsub(); + if (typeof unsub === "function") unsub(); }; }, []); - const filteredAndSorted = useMemo(() => { - const filtered = - filter === "ALL" - ? alerts - : alerts.filter((a) => (a.alertLevel as AlertLevel) === filter); - - return [...filtered].sort((a, b) => { - const la = a.alertLevel as AlertLevel; - const lb = b.alertLevel as AlertLevel; + const filteredSorted = useMemo(() => { + const base = + filter === "ALL" ? items : items.filter((a) => a.alertLevel === filter); - // tri par niveau d'alerte (desc) - const byLevel = levelRank(lb) - levelRank(la); - if (byLevel !== 0) return byLevel; - - // tri par date (desc) si dispo - const ta = a.timestamp ?? 0; - const tb = b.timestamp ?? 0; - return tb - ta; + return [...base].sort((a, b) => { + // 1) severity + const r = severityRank(b.alertLevel) - severityRank(a.alertLevel); + if (r !== 0) return r; + // 2) timestamp desc + return (b.timestamp ?? 0) - (a.timestamp ?? 0); }); - }, [alerts, filter]); - - const confirmClear = () => { - if (alerts.length === 0) return; + }, [items, filter]); + const handleClear = useCallback(() => { RNAlert.alert( "Supprimer les alertes ?", - "Cette action efface la liste locale des alertes reçues (cela ne touche pas la base de données).", + "Cette action supprime les alertes stockées localement (alertStore).", [ { text: "Annuler", style: "cancel" }, { text: "Supprimer", style: "destructive", - onPress: () => alertStore.clear(), + onPress: () => { + alertStore.clear?.(); + setItems(alertStore.getAll?.() ?? []); + }, }, ] ); - }; + }, []); - const FilterButton = ({ value, label }: { value: AlertFilter; label: string }) => { + const FilterButton = ({ value, label }: { value: Filter; label: string }) => { const active = filter === value; return ( setFilter(value)} + style={[styles.filterBtn, active && styles.filterBtnActive]} > {label} @@ -132,67 +128,57 @@ export default function AlertsScreen() { `alert-${idx}`} + data={filteredSorted} + keyExtractor={(it, idx) => + it.id ?? + `${it.timestamp}-${it.pair}-${it.action}-${it.alertLevel}-${idx}` + } ListHeaderComponent={ - + - Alertes + Alertes - + Clear - - Filtre : {filter === "ALL" ? "Toutes" : filter} — Total : {alerts.length} - - - - - + + + - - Tri par défaut : CRITICAL > WARNING > INFO, puis plus récent. + + Tri : CRITICAL → WARNING → INFO, puis récent → ancien. } ListEmptyComponent={ Aucune alerte - En attente d’alertes Socket.IO… + Aucune alerte reçue pour ce filtre. } renderItem={({ item }) => { - const lvl = item.alertLevel as AlertLevel; - const act = item.action as TradeDecision; - - const lvlColor = getLevelColor(lvl); - const actColor = getActionColor(act); + const aColor = actionColor(item.action); + const lColor = levelColor(item.alertLevel); return ( {item.pair} - - {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"} - + {formatDate(item.timestamp)} - - - {lvl} + + + {item.action} - - {act} + + {item.alertLevel} @@ -201,14 +187,13 @@ export default function AlertsScreen() { {(item.confidence * 100).toFixed(0)}% - {typeof item.price === "number" && ( - - Prix - {item.price.toFixed(2)} - - )} - {item.reason} + + {!!item.price && ( + + Prix (optionnel) : {item.price.toFixed(2)} + + )} ); }} @@ -223,6 +208,12 @@ const styles = StyleSheet.create({ backgroundColor: ui.screen.backgroundColor, }, + screenTitle: { + fontSize: 22, + fontWeight: "900", + color: "#0f172a", + }, + headerRow: { flexDirection: "row", justifyContent: "space-between", @@ -237,9 +228,7 @@ const styles = StyleSheet.create({ borderColor: "#e5e7eb", backgroundColor: "#fff", }, - clearBtnDisabled: { - opacity: 0.5, - }, + clearBtnText: { fontWeight: "900", color: "#dc2626", @@ -249,27 +238,34 @@ const styles = StyleSheet.create({ flexDirection: "row", flexWrap: "wrap", gap: 8, - marginTop: 10, + marginTop: 12, + marginBottom: 10, }, + filterBtn: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 999, borderWidth: 1, - }, - filterBtnInactive: { borderColor: "#e5e7eb", backgroundColor: "#fff", }, filterBtnActive: { - borderColor: "#14532D", - backgroundColor: "#14532D22", + borderColor: "#0f172a", }, filterText: { fontWeight: "900", color: "#0f172a", + opacity: 0.7, }, filterTextActive: { - color: "#14532D", + opacity: 1, + }, + + badgesRow: { + flexDirection: "row", + gap: 8, + marginTop: 10, + flexWrap: "wrap", }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index d1e60f4..d97a3e6 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -348,7 +348,7 @@ export default function DashboardScreen() { navigation.navigate("Alerts" as never)}> - Urgence + Alertes diff --git a/Wallette/mobile/src/types/Alert.ts b/Wallette/mobile/src/types/Alert.ts index a59d4b4..e747535 100644 --- a/Wallette/mobile/src/types/Alert.ts +++ b/Wallette/mobile/src/types/Alert.ts @@ -1,9 +1,24 @@ +/** + * Alert + * ----- + * Contrat mobile (Socket.IO / API). + * On garde des enums clairs comme demandé : + * - alertLevel : CRITICAL / WARNING / INFO + * - action : BUY / SELL / HOLD / STOP_LOSS + * + * id est optionnel : utile pour React (keyExtractor), + * mais le serveur peut ne pas l'envoyer au début. + */ export interface Alert { - action: "BUY" | "SELL" | "HOLD"; + id?: string; + + action: "BUY" | "SELL" | "HOLD" | "STOP_LOSS"; pair: string; // ex: "BTC/EUR" confidence: number; // 0..1 reason: string; + alertLevel: "INFO" | "WARNING" | "CRITICAL"; timestamp: number; // Unix ms + price?: number; } \ No newline at end of file diff --git a/Wallette/mobile/src/utils/accountStorage.ts b/Wallette/mobile/src/utils/accountStorage.ts new file mode 100644 index 0000000..cf15063 --- /dev/null +++ b/Wallette/mobile/src/utils/accountStorage.ts @@ -0,0 +1,50 @@ +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