]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Creation fenetre Account + modification Dashboard + autres
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 21:09:16 +0000 (22:09 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 21:09:16 +0000 (22:09 +0100)
Wallette/mobile/App.tsx
Wallette/mobile/src/components/AccountMenu.tsx [new file with mode: 0644]
Wallette/mobile/src/models/AccountProfile.ts [new file with mode: 0644]
Wallette/mobile/src/screens/AboutScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/screens/AccountScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/screens/AlertsScreen.tsx
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/types/Alert.ts
Wallette/mobile/src/utils/accountStorage.ts [new file with mode: 0644]

index 0e33c56024e3cf5cde106a21796410ad9e74aad6..d33c4c3850c8c3c8a2aeaf1733701fca46f32d82 100644 (file)
@@ -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<RootStackParamList>();
 
+// ✅ navigationRef (permet de naviguer hors des screens, ex: depuis un Modal)
+export const navigationRef = createNavigationContainerRef<RootStackParamList>();
+
 export default function App() {
   const [ready, setReady] = useState(false);
   const [isAuthed, setIsAuthed] = useState(false);
 
+  const [menuVisible, setMenuVisible] = useState(false);
+  const [sessionEmail, setSessionEmail] = useState<string>("—");
+
   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 (
       <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
@@ -58,16 +92,27 @@ export default function App() {
   if (!isAuthed) {
     return (
       <AuthScreen
-        onAuthenticated={() => {
+        onAuthenticated={async () => {
+          const s = await loadSession();
+          setSessionEmail(s?.email ?? "—");
           setIsAuthed(true);
         }}
       />
     );
   }
 
-  // Connecté -> stack normal
   return (
-    <NavigationContainer>
+    <NavigationContainer ref={navigationRef}>
+      {/* ✅ Modal menu Compte (navigue grâce au navigationRef) */}
+      <AccountMenu
+        visible={menuVisible}
+        email={sessionEmail}
+        onClose={() => setMenuVisible(false)}
+        onGoAccount={() => go("Account")}
+        onGoAbout={() => go("About")}
+        onLogout={doLogout}
+      />
+
       <Stack.Navigator id="MainStack" initialRouteName="Dashboard">
         <Stack.Screen
           name="Dashboard"
@@ -76,19 +121,14 @@ export default function App() {
             title: "Dashboard",
             headerRight: () => (
               <View style={{ flexDirection: "row", gap: 12 }}>
-                {/* ⚙️ Settings */}
-                <TouchableOpacity onPress={() => navigation.navigate("Settings")}>
-                  <Ionicons name="settings-outline" size={22} color="#0f172a" />
+                {/* 👤 Menu Compte */}
+                <TouchableOpacity onPress={() => setMenuVisible(true)}>
+                  <Ionicons name="person-circle-outline" size={24} color="#0f172a" />
                 </TouchableOpacity>
 
-                {/* ⎋ Logout */}
-                <TouchableOpacity
-                  onPress={async () => {
-                    await clearSession();
-                    setIsAuthed(false);
-                  }}
-                >
-                  <Ionicons name="log-out-outline" size={22} color="#0f172a" />
+                {/* ⚙️ Paramètres */}
+                <TouchableOpacity onPress={() => navigation.navigate("Settings")}>
+                  <Ionicons name="settings-outline" size={22} color="#0f172a" />
                 </TouchableOpacity>
               </View>
             ),
@@ -99,10 +139,11 @@ export default function App() {
         <Stack.Screen name="Strategy" component={StrategyScreen} options={{ title: "Stratégie" }} />
         <Stack.Screen name="Alerts" component={AlertsScreen} options={{ title: "Alertes" }} />
         <Stack.Screen name="History" component={HistoryScreen} options={{ title: "Historique" }} />
-        <Stack.Screen name="Settings" options={{ title: "Paramètres" }}
->
-  {() => <SettingsScreen onLogout={() => setIsAuthed(false)} />}
-</Stack.Screen>
+
+        <Stack.Screen name="Settings" component={SettingsScreen} options={{ title: "Paramètres" }} />
+
+        <Stack.Screen name="Account" component={AccountScreen} options={{ title: "Compte" }} />
+        <Stack.Screen name="About" component={AboutScreen} options={{ title: "À propos" }} />
       </Stack.Navigator>
     </NavigationContainer>
   );
diff --git a/Wallette/mobile/src/components/AccountMenu.tsx b/Wallette/mobile/src/components/AccountMenu.tsx
new file mode 100644 (file)
index 0000000..2be54cb
--- /dev/null
@@ -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 (
+    <Modal transparent visible={visible} animationType="fade" onRequestClose={onClose}>
+      {/* Overlay */}
+      <Pressable style={styles.overlay} onPress={onClose}>
+        {/* Stop propagation */}
+        <Pressable style={styles.card} onPress={() => null}>
+          <View style={styles.headerRow}>
+            <Ionicons name="person-circle-outline" size={28} color="#0f172a" style={{ opacity: 0.8 }} />
+            <View style={{ flex: 1 }}>
+              <Text style={styles.title}>Compte</Text>
+              <Text style={ui.muted} numberOfLines={1}>{email}</Text>
+            </View>
+            <TouchableOpacity onPress={onClose}>
+              <Ionicons name="close" size={22} color="#0f172a" style={{ opacity: 0.7 }} />
+            </TouchableOpacity>
+          </View>
+
+          <TouchableOpacity style={styles.item} onPress={() => { onClose(); onGoAccount(); }}>
+            <Ionicons name="create-outline" size={18} color="#0f172a" style={styles.itemIcon} />
+            <Text style={styles.itemText}>Modification du compte</Text>
+            <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
+          </TouchableOpacity>
+
+          <TouchableOpacity style={styles.item} onPress={() => { onClose(); onGoAbout(); }}>
+            <Ionicons name="information-circle-outline" size={18} color="#0f172a" style={styles.itemIcon} />
+            <Text style={styles.itemText}>À propos</Text>
+            <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
+          </TouchableOpacity>
+
+          <TouchableOpacity style={[styles.item, styles.itemDanger]} onPress={() => { onClose(); onLogout(); }}>
+            <Ionicons name="log-out-outline" size={18} color="#dc2626" style={styles.itemIcon} />
+            <Text style={[styles.itemText, { color: "#dc2626" }]}>Déconnexion</Text>
+          </TouchableOpacity>
+        </Pressable>
+      </Pressable>
+    </Modal>
+  );
+}
+
+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 (file)
index 0000000..93ebefb
--- /dev/null
@@ -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 (file)
index 0000000..ab72035
--- /dev/null
@@ -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<AccountProfile | null>(null);
+  const [userId, setUserId] = useState<string>("");
+
+  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);
+
+  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 (
+      <View style={ui.centered}>
+        <Text>Chargement du compte…</Text>
+      </View>
+    );
+  }
+
+  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 (
+    <SafeAreaView style={styles.safeArea}>
+      <View style={ui.container}>
+        <Text style={styles.title}>Modification du compte</Text>
+
+        <View style={ui.card}>
+          <Text style={ui.title}>Informations</Text>
+
+          <Text style={ui.muted}>UserId (session)</Text>
+          <Text style={styles.mono}>{userId || "—"}</Text>
+
+          <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}
+          />
+        </View>
+
+        <View style={ui.card}>
+          <Text style={ui.title}>Mot de passe (UI prête)</Text>
+          <Text style={ui.muted}>
+            Le changement réel du mot de passe sera branché quand l’API auth sera disponible.
+          </Text>
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
+          <TextInput value={password} onChangeText={setPassword} secureTextEntry style={styles.input} />
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
+          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry style={styles.input} />
+        </View>
+
+        {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
+
+        <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
+          <Text style={ui.buttonText}>Sauvegarder</Text>
+        </TouchableOpacity>
+      </View>
+    </SafeAreaView>
+  );
+}
+
+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 (file)
index 0000000..ab72035
--- /dev/null
@@ -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<AccountProfile | null>(null);
+  const [userId, setUserId] = useState<string>("");
+
+  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);
+
+  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 (
+      <View style={ui.centered}>
+        <Text>Chargement du compte…</Text>
+      </View>
+    );
+  }
+
+  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 (
+    <SafeAreaView style={styles.safeArea}>
+      <View style={ui.container}>
+        <Text style={styles.title}>Modification du compte</Text>
+
+        <View style={ui.card}>
+          <Text style={ui.title}>Informations</Text>
+
+          <Text style={ui.muted}>UserId (session)</Text>
+          <Text style={styles.mono}>{userId || "—"}</Text>
+
+          <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}
+          />
+        </View>
+
+        <View style={ui.card}>
+          <Text style={ui.title}>Mot de passe (UI prête)</Text>
+          <Text style={ui.muted}>
+            Le changement réel du mot de passe sera branché quand l’API auth sera disponible.
+          </Text>
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
+          <TextInput value={password} onChangeText={setPassword} secureTextEntry style={styles.input} />
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
+          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry style={styles.input} />
+        </View>
+
+        {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
+
+        <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
+          <Text style={ui.buttonText}>Sauvegarder</Text>
+        </TouchableOpacity>
+      </View>
+    </SafeAreaView>
+  );
+}
+
+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
index dff674ae43ed2b66058894c8867269f95aa7180c..7668c219c6fc2720ce3795a4e0cf2b92a44866ed 100644 (file)
@@ -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<Alert[]>([]);
-  const [filter, setFilter] = useState<AlertFilter>("ALL");
+  const [items, setItems] = useState<Alert[]>([]);
+  const [filter, setFilter] = useState<Filter>("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 (
       <TouchableOpacity
-        style={[
-          styles.filterBtn,
-          active ? styles.filterBtnActive : styles.filterBtnInactive,
-        ]}
         onPress={() => setFilter(value)}
+        style={[styles.filterBtn, active && styles.filterBtnActive]}
       >
         <Text style={[styles.filterText, active && styles.filterTextActive]}>
           {label}
@@ -132,67 +128,57 @@ export default function AlertsScreen() {
     <SafeAreaView style={styles.safeArea}>
       <FlatList
         contentContainerStyle={ui.container}
-        data={filteredAndSorted}
-        keyExtractor={(_, idx) => `alert-${idx}`}
+        data={filteredSorted}
+        keyExtractor={(it, idx) =>
+          it.id ??
+          `${it.timestamp}-${it.pair}-${it.action}-${it.alertLevel}-${idx}`
+        }
         ListHeaderComponent={
-          <View style={ui.card}>
+          <View style={{ marginBottom: 12 }}>
             <View style={styles.headerRow}>
-              <Text style={ui.title}>Alertes</Text>
+              <Text style={styles.screenTitle}>Alertes</Text>
 
-              <TouchableOpacity
-                style={[styles.clearBtn, alerts.length === 0 && styles.clearBtnDisabled]}
-                onPress={confirmClear}
-                disabled={alerts.length === 0}
-              >
+              <TouchableOpacity onPress={handleClear} style={styles.clearBtn}>
                 <Text style={styles.clearBtnText}>Clear</Text>
               </TouchableOpacity>
             </View>
 
-            <Text style={ui.muted}>
-              Filtre : {filter === "ALL" ? "Toutes" : filter} — Total : {alerts.length}
-            </Text>
-
             <View style={styles.filtersRow}>
-              <FilterButton value="ALL" label="Toutes" />
-              <FilterButton value="CRITICAL" label="Critical" />
-              <FilterButton value="WARNING" label="Warning" />
+              <FilterButton value="CRITICAL" label="Critiques" />
+              <FilterButton value="WARNING" label="Warnings" />
               <FilterButton value="INFO" label="Info" />
+              <FilterButton value="ALL" label="Toutes" />
             </View>
 
-            <Text style={[ui.muted, { marginTop: 6 }]}>
-              Tri par défaut : CRITICAL &gt; WARNING &gt; INFO, puis plus récent.
+            <Text style={ui.muted}>
+              Tri : CRITICAL → WARNING → INFO, puis récent → ancien.
             </Text>
           </View>
         }
         ListEmptyComponent={
           <View style={ui.card}>
             <Text style={ui.title}>Aucune alerte</Text>
-            <Text style={ui.muted}>En attente d’alertes Socket.IO…</Text>
+            <Text style={ui.muted}>Aucune alerte reçue pour ce filtre.</Text>
           </View>
         }
         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 (
             <View style={ui.card}>
               <View style={ui.rowBetween}>
                 <Text style={ui.valueBold}>{item.pair}</Text>
-                <Text style={ui.muted}>
-                  {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"}
-                </Text>
+                <Text style={ui.muted}>{formatDate(item.timestamp)}</Text>
               </View>
 
-              <View style={{ flexDirection: "row", gap: 8, flexWrap: "wrap", marginTop: 10 }}>
-                <View style={[ui.badge, { backgroundColor: `${lvlColor}22`, marginTop: 0 }]}>
-                  <Text style={[ui.badgeText, { color: lvlColor }]}>{lvl}</Text>
+              <View style={styles.badgesRow}>
+                <View style={[ui.badge, { backgroundColor: `${aColor}22`, marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: aColor }]}>{item.action}</Text>
                 </View>
 
-                <View style={[ui.badge, { backgroundColor: `${actColor}22`, marginTop: 0 }]}>
-                  <Text style={[ui.badgeText, { color: actColor }]}>{act}</Text>
+                <View style={[ui.badge, { backgroundColor: `${lColor}22`, marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: lColor }]}>{item.alertLevel}</Text>
                 </View>
               </View>
 
@@ -201,14 +187,13 @@ export default function AlertsScreen() {
                 <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(0)}%</Text>
               </View>
 
-              {typeof item.price === "number" && (
-                <View style={[ui.rowBetween, { marginTop: 6 }]}>
-                  <Text style={ui.value}>Prix</Text>
-                  <Text style={ui.valueBold}>{item.price.toFixed(2)}</Text>
-                </View>
-              )}
-
               <Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
+
+              {!!item.price && (
+                <Text style={[ui.muted, { marginTop: 6 }]}>
+                  Prix (optionnel) : {item.price.toFixed(2)}
+                </Text>
+              )}
             </View>
           );
         }}
@@ -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
index d1e60f44ca681eaff5a1e11d383bb27e07d9e454..d97a3e621bb7a262f10cbf1cf4fa8cf2ccad61d8 100644 (file)
@@ -348,7 +348,7 @@ export default function DashboardScreen() {
         <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Alerts" as never)}>
           <View style={[ui.card, compact && styles.cardCompact]}>
             <View style={styles.headerRow}>
-              <Text style={[ui.title, compact && styles.titleCompact]}>Urgence</Text>
+              <Text style={[ui.title, compact && styles.titleCompact]}>Alertes</Text>
               <Chevron />
             </View>
 
index a59d4b469c6619c630d9f7fe99a0f569959de0c5..e747535a2b2800cc208761b98df0c71814887706 100644 (file)
@@ -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 (file)
index 0000000..cf15063
--- /dev/null
@@ -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<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