]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Modification 'mobile' - Mise en place des différente fenetre + modification UI
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Thu, 26 Feb 2026 14:43:57 +0000 (15:43 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Thu, 26 Feb 2026 14:43:57 +0000 (15:43 +0100)
19 files changed:
Wallette/mobile/.expo/README.md [new file with mode: 0644]
Wallette/mobile/.expo/devices.json [new file with mode: 0644]
Wallette/mobile/App.tsx
Wallette/mobile/src/components/AccountMenu.tsx [new file with mode: 0644]
Wallette/mobile/src/mocks/prices.mock.ts [new file with mode: 0644]
Wallette/mobile/src/models/AccountProfile.ts [new file with mode: 0644]
Wallette/mobile/src/models/Portfolio.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/AuthScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/screens/SettingsScreen.tsx
Wallette/mobile/src/screens/WalletScreen.tsx
Wallette/mobile/src/types/Alert.ts
Wallette/mobile/src/utils/accountStorage.ts [new file with mode: 0644]
Wallette/mobile/src/utils/portfolioStorage.ts [new file with mode: 0644]
Wallette/mobile/src/utils/sessionStorage.ts [new file with mode: 0644]
Wallette/mobile/src/utils/settingsStorage.ts

diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md
new file mode 100644 (file)
index 0000000..ce8c4b6
--- /dev/null
@@ -0,0 +1,13 @@
+> Why do I have a folder named ".expo" in my project?
+
+The ".expo" folder is created when an Expo project is started using "expo start" command.
+
+> What do the files contain?
+
+- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
+- "settings.json": contains the server configuration that is used to serve the application manifest.
+
+> Should I commit the ".expo" folder?
+
+No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
+Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
diff --git a/Wallette/mobile/.expo/devices.json b/Wallette/mobile/.expo/devices.json
new file mode 100644 (file)
index 0000000..5efff6c
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "devices": []
+}
index 8f9403ff4f5030d083c2464201f265c112703819..d33c4c3850c8c3c8a2aeaf1733701fca46f32d82 100644 (file)
@@ -1,7 +1,8 @@
-import { NavigationContainer } from "@react-navigation/native";
+import { NavigationContainer, createNavigationContainerRef } from "@react-navigation/native";
 import { createNativeStackNavigator } from "@react-navigation/native-stack";
-import { TouchableOpacity } from "react-native";
+import { TouchableOpacity, View, Text, Alert as RNAlert } from "react-native";
 import { Ionicons } from "@expo/vector-icons";
+import { useEffect, useState } from "react";
 
 import DashboardScreen from "./src/screens/DashboardScreen";
 import SettingsScreen from "./src/screens/SettingsScreen";
@@ -9,8 +10,13 @@ import HistoryScreen from "./src/screens/HistoryScreen";
 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";
 
-// Types des routes (pour éviter les erreurs de navigation)
 export type RootStackParamList = {
   Dashboard: undefined;
   Settings: undefined;
@@ -18,13 +24,95 @@ 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);
+    }
+
+    init();
+
+    return () => {
+      active = false;
+    };
+  }, []);
+
+  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" }}>
+        <Text>Initialisation…</Text>
+      </View>
+    );
+  }
+
+  // Pas connecté -> écran Auth
+  if (!isAuthed) {
+    return (
+      <AuthScreen
+        onAuthenticated={async () => {
+          const s = await loadSession();
+          setSessionEmail(s?.email ?? "—");
+          setIsAuthed(true);
+        }}
+      />
+    );
+  }
+
   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"
@@ -32,42 +120,30 @@ export default function App() {
           options={({ navigation }) => ({
             title: "Dashboard",
             headerRight: () => (
-              <TouchableOpacity onPress={() => navigation.navigate("Settings")}>
-                <Ionicons name="settings-outline" size={22} color="#0f172a" />
-              </TouchableOpacity>
+              <View style={{ flexDirection: "row", gap: 12 }}>
+                {/* 👤 Menu Compte */}
+                <TouchableOpacity onPress={() => setMenuVisible(true)}>
+                  <Ionicons name="person-circle-outline" size={24} color="#0f172a" />
+                </TouchableOpacity>
+
+                {/* ⚙️ Paramètres */}
+                <TouchableOpacity onPress={() => navigation.navigate("Settings")}>
+                  <Ionicons name="settings-outline" size={22} color="#0f172a" />
+                </TouchableOpacity>
+              </View>
             ),
           })}
         />
 
-        <Stack.Screen
-          name="Wallet"
-          component={WalletScreen}
-          options={{ title: "Portefeuille" }}
-        />
+        <Stack.Screen name="Wallet" component={WalletScreen} options={{ title: "Portefeuille" }} />
+        <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="Strategy"
-          component={StrategyScreen}
-          options={{ title: "Stratégie" }}
-        />
+        <Stack.Screen name="Settings" component={SettingsScreen} options={{ title: "Paramètres" }} />
 
-        <Stack.Screen
-          name="Alerts"
-          component={AlertsScreen}
-          options={{ title: "Alertes" }}
-        />
-
-        <Stack.Screen
-          name="History"
-          component={HistoryScreen}
-          options={{ title: "Historique" }}
-        />
-
-        <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/mocks/prices.mock.ts b/Wallette/mobile/src/mocks/prices.mock.ts
new file mode 100644 (file)
index 0000000..d93941c
--- /dev/null
@@ -0,0 +1,16 @@
+/**
+ * Mock prices (Step 3)
+ * --------------------
+ * En attendant l'API REST /api/price/current.
+ */
+export const mockPrices: Record<string, number> = {
+  BTC: 42150.23,
+  ETH: 2300.55,
+  SOL: 105.12,
+  ADA: 0.48,
+};
+
+export function getMockPrice(symbol: string): number | null {
+  const key = symbol.toUpperCase().trim();
+  return typeof mockPrices[key] === "number" ? mockPrices[key] : null;
+}
\ 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/models/Portfolio.ts b/Wallette/mobile/src/models/Portfolio.ts
new file mode 100644 (file)
index 0000000..52f0f34
--- /dev/null
@@ -0,0 +1,15 @@
+/**
+ * Portfolio (Step 3)
+ * ------------------
+ * Mono-user / multi-cryptos.
+ * Une ligne = un asset (BTC, ETH, etc.) + quantité.
+ */
+export interface PortfolioAsset {
+  symbol: string;   // ex: "BTC"
+  quantity: number; // ex: 0.25
+}
+
+export interface PortfolioState {
+  assets: PortfolioAsset[];
+  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
diff --git a/Wallette/mobile/src/screens/AuthScreen.tsx b/Wallette/mobile/src/screens/AuthScreen.tsx
new file mode 100644 (file)
index 0000000..8e15dac
--- /dev/null
@@ -0,0 +1,112 @@
+import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
+import { useMemo, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+import { ui } from "../components/ui/uiStyles";
+import { saveSession } from "../utils/sessionStorage";
+
+/**
+ * AuthScreen (Step 4 - sans API)
+ * ------------------------------
+ * Simule un login local :
+ * - Email -> userId stable
+ * - Session stockée en AsyncStorage
+ *
+ * Plus tard : on remplace par une auth REST réelle.
+ */
+export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => void }) {
+  const [email, setEmail] = useState<string>("demo@example.com");
+  const [error, setError] = useState<string | null>(null);
+
+  const normalizedEmail = useMemo(() => email.trim().toLowerCase(), [email]);
+
+  const isValidEmail = useMemo(() => {
+    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail);
+  }, [normalizedEmail]);
+
+  const makeUserId = (mail: string) => `user_${mail.replace(/[^a-z0-9]/g, "_")}`;
+
+  const handleLogin = async () => {
+    setError(null);
+
+    if (!isValidEmail) {
+      setError("Email invalide.");
+      return;
+    }
+
+    const userId = makeUserId(normalizedEmail);
+
+    await saveSession({
+      userId,
+      email: normalizedEmail,
+      createdAtMs: Date.now(),
+    });
+
+    onAuthenticated();
+  };
+
+  return (
+    <SafeAreaView style={styles.safeArea}>
+      <View style={ui.container}>
+        <Text style={styles.title}>Connexion</Text>
+
+        <View style={ui.card}>
+          <Text style={ui.title}>Email</Text>
+
+          <TextInput
+            value={email}
+            onChangeText={setEmail}
+            autoCapitalize="none"
+            keyboardType="email-address"
+            placeholder="ex: demo@example.com"
+            style={styles.input}
+          />
+
+          {!!error && <Text style={styles.errorText}>{error}</Text>}
+
+          <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleLogin}>
+            <Text style={ui.buttonText}>Se connecter</Text>
+          </TouchableOpacity>
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>
+            Step 4 (sans API) : session locale. Plus tard, auth réelle via serveur.
+          </Text>
+        </View>
+      </View>
+    </SafeAreaView>
+  );
+}
+
+const styles = StyleSheet.create({
+  safeArea: {
+    flex: 1,
+    backgroundColor: ui.screen.backgroundColor,
+  },
+  title: {
+    fontSize: 22,
+    fontWeight: "900",
+    marginBottom: 12,
+    color: "#0f172a",
+  },
+  input: {
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    borderRadius: 10,
+    paddingHorizontal: 12,
+    paddingVertical: 10,
+    marginTop: 8,
+    backgroundColor: "#fff",
+    color: "#0f172a",
+  },
+  fullButton: {
+    flexGrow: 0,
+    flexBasis: "auto",
+    width: "100%",
+    marginTop: 12,
+  },
+  errorText: {
+    marginTop: 10,
+    color: "#dc2626",
+    fontWeight: "900",
+  },
+});
\ No newline at end of file
index 8bd123a3a4b64874a67011bbff7e0e3a5ad77192..d97a3e621bb7a262f10cbf1cf4fa8cf2ccad61d8 100644 (file)
@@ -13,6 +13,7 @@ import { Ionicons } from "@expo/vector-icons";
 
 import type { DashboardSummary } from "../types/DashboardSummary";
 import { fetchDashboardSummary } from "../services/dashboardService";
+
 import { loadSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
 
@@ -24,18 +25,18 @@ import type { Alert } from "../types/Alert";
 import { alertStore } from "../services/alertStore";
 import { showAlertNotification } from "../services/notificationService";
 
-import { loadWallet } from "../utils/walletStorage";
-import type { WalletState } from "../models/Wallet";
+import { loadPortfolio } from "../utils/portfolioStorage";
+import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
+import { getMockPrice } from "../mocks/prices.mock";
+
+import { loadSession } from "../utils/sessionStorage";
 
 /**
- * DashboardScreen (WF-01) — Responsive + No-scroll goal
- * ----------------------------------------------------
- * - Cartes cliquables (chevron subtil) :
- *   * Portefeuille -> Wallet
- *   * Urgence -> Alertes
- *   * Prix BTC -> Historique
- * - Conseiller : bouton -> Stratégie
- * - Socket.IO non bloquant + notifications locales
+ * DashboardScreen — Step 4
+ * ------------------------
+ * - Multi-users : userId vient de la session (AsyncStorage)
+ * - Settings/Portfolio sont déjà "scopés" par userId (via storages)
+ * - Socket.IO auth utilise userId de session
  */
 export default function DashboardScreen() {
   const { height } = useWindowDimensions();
@@ -43,7 +44,7 @@ export default function DashboardScreen() {
 
   const [summary, setSummary] = useState<DashboardSummary | null>(null);
   const [settings, setSettings] = useState<UserSettings | null>(null);
-  const [wallet, setWallet] = useState<WalletState | null>(null);
+  const [portfolio, setPortfolio] = useState<PortfolioState | null>(null);
 
   const [loading, setLoading] = useState<boolean>(true);
   const [error, setError] = useState<string | null>(null);
@@ -67,17 +68,17 @@ export default function DashboardScreen() {
           setError(null);
           setLoading(true);
 
-          const [dashboardData, userSettings, walletData] = await Promise.all([
+          const [dashboardData, userSettings, portfolioData] = await Promise.all([
             fetchDashboardSummary(),
             loadSettings(),
-            loadWallet(),
+            loadPortfolio(),
           ]);
 
           if (!isActive) return;
 
           setSummary(dashboardData);
           setSettings(userSettings);
-          setWallet(walletData);
+          setPortfolio(portfolioData);
           setLastRefreshMs(Date.now());
         } catch {
           if (isActive) setError("Impossible de charger le dashboard.");
@@ -132,41 +133,62 @@ export default function DashboardScreen() {
     }
   };
 
+  /**
+   * Socket.IO (non bloquant)
+   * - userId = session.userId
+   */
   useEffect(() => {
-    if (!settings) return;
+    let unsub: null | (() => void) = null;
+    let active = true;
 
-    setSocketError(null);
+    async function initSocket() {
+      if (!settings) return;
 
-    const userId = "test-user";
+      setSocketError(null);
 
-    try {
-      socketService.connect(SERVER_URL, userId);
-      setSocketConnected(true);
-    } catch {
-      setSocketConnected(false);
-      setSocketError("Socket : paramètres invalides (URL ou userId).");
-      return;
-    }
+      const session = await loadSession();
+      const userId = session?.userId;
 
-    const unsubscribeAlert = socketService.onAlert((alert) => {
-      alertStore.add(alert);
-      setLiveAlerts((prev) => [alert, ...prev].slice(0, 100));
-
-      if (settings.notificationsEnabled) {
-        void (async () => {
-          try {
-            await showAlertNotification(alert);
-          } catch (e) {
-            console.log("⚠️ Notification error:", e);
-          }
-        })();
+      if (!userId) {
+        setSocketConnected(false);
+        setSocketError("Socket désactivé : session absente.");
+        return;
       }
-    });
 
-    socketService.ping();
+      try {
+        socketService.connect(SERVER_URL, userId);
+        if (!active) return;
+        setSocketConnected(true);
+      } catch {
+        if (!active) return;
+        setSocketConnected(false);
+        setSocketError("Socket : paramètres invalides (URL ou userId).");
+        return;
+      }
+
+      unsub = socketService.onAlert((alert) => {
+        alertStore.add(alert);
+        setLiveAlerts((prev) => [alert, ...prev].slice(0, 100));
+
+        if (settings.notificationsEnabled) {
+          void (async () => {
+            try {
+              await showAlertNotification(alert);
+            } catch (e) {
+              console.log("⚠️ Notification error:", e);
+            }
+          })();
+        }
+      });
+
+      socketService.ping();
+    }
+
+    void initSocket();
 
     return () => {
-      unsubscribeAlert();
+      active = false;
+      if (unsub) unsub();
       socketService.disconnect();
       setSocketConnected(false);
     };
@@ -184,10 +206,32 @@ export default function DashboardScreen() {
     return liveAlerts[0];
   }, [liveAlerts]);
 
-  const walletTotalValue = useMemo(() => {
-    if (!wallet || !summary) return null;
-    return wallet.quantity * summary.price;
-  }, [wallet, summary]);
+  const portfolioTotalValue = useMemo(() => {
+    if (!portfolio) return 0;
+
+    return portfolio.assets.reduce((sum, a) => {
+      const price = getMockPrice(a.symbol);
+      if (price === null) return sum;
+      return sum + a.quantity * price;
+    }, 0);
+  }, [portfolio]);
+
+  const topAssets: PortfolioAsset[] = useMemo(() => {
+    if (!portfolio) return [];
+
+    const withValue = portfolio.assets.map((a) => {
+      const price = getMockPrice(a.symbol) ?? 0;
+      return { ...a, _value: a.quantity * price };
+    });
+
+    withValue.sort((a, b) => (b as any)._value - (a as any)._value);
+    return withValue.slice(0, 3).map(({ symbol, quantity }) => ({ symbol, quantity }));
+  }, [portfolio]);
+
+  const remainingCount = useMemo(() => {
+    if (!portfolio) return 0;
+    return Math.max(0, portfolio.assets.length - topAssets.length);
+  }, [portfolio, topAssets]);
 
   if (loading) {
     return (
@@ -205,7 +249,7 @@ export default function DashboardScreen() {
     );
   }
 
-  if (!summary || !settings || !wallet) {
+  if (!summary || !settings || !portfolio) {
     return (
       <View style={ui.centered}>
         <Text>Initialisation…</Text>
@@ -214,7 +258,12 @@ export default function DashboardScreen() {
   }
 
   const Chevron = () => (
-    <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
+    <Ionicons
+      name="chevron-forward-outline"
+      size={18}
+      color="#0f172a"
+      style={{ opacity: 0.35 }}
+    />
   );
 
   return (
@@ -253,7 +302,7 @@ export default function DashboardScreen() {
           </TouchableOpacity>
         </View>
 
-        {/* 2) PORTEFEUILLE — cliquable => Wallet */}
+        {/* 2) PORTEFEUILLE */}
         <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Wallet" as never)}>
           <View style={[ui.card, compact && styles.cardCompact]}>
             <View style={styles.headerRow}>
@@ -262,28 +311,44 @@ export default function DashboardScreen() {
             </View>
 
             <View style={ui.rowBetween}>
-              <Text style={ui.value}>Quantité BTC :</Text>
-              <Text style={ui.valueBold}>{wallet.quantity.toFixed(6)} BTC</Text>
-            </View>
-
-            <View style={[ui.rowBetween, { marginTop: 6 }]}>
-              <Text style={ui.value}>Valeur Totale :</Text>
+              <Text style={ui.value}>Valeur globale :</Text>
               <Text style={ui.valueBold}>
-                {walletTotalValue !== null ? `${walletTotalValue.toFixed(2)} ${settings.currency}` : "—"}
+                {portfolioTotalValue.toFixed(2)} {settings.currency}
               </Text>
             </View>
 
-            <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
-              BTC @ {summary.price.toFixed(2)} {settings.currency}
+            {portfolio.assets.length === 0 ? (
+              <Text style={[ui.muted, { marginTop: 8 }]}>
+                Aucun asset (ajoute BTC/ETH/SOL dans Portefeuille)
+              </Text>
+            ) : (
+              <View style={{ marginTop: 8 }}>
+                {topAssets.map((a) => (
+                  <View key={a.symbol} style={[ui.rowBetween, { marginTop: 4 }]}>
+                    <Text style={ui.muted}>{a.symbol}</Text>
+                    <Text style={ui.valueBold}>{a.quantity.toFixed(6)}</Text>
+                  </View>
+                ))}
+
+                {remainingCount > 0 && (
+                  <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
+                    +{remainingCount} autre(s) asset(s)
+                  </Text>
+                )}
+              </View>
+            )}
+
+            <Text style={[ui.muted, { marginTop: 8 }]} numberOfLines={1}>
+              BTC @ {summary.price.toFixed(2)} {settings.currency} (mock)
             </Text>
           </View>
         </TouchableOpacity>
 
-        {/* 3) URGENCE — cliquable => Alertes */}
+        {/* 3) URGENCE */}
         <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>
 
@@ -307,7 +372,7 @@ export default function DashboardScreen() {
           </View>
         </TouchableOpacity>
 
-        {/* 4) PRIX BTC — cliquable => Historique */}
+        {/* 4) PRIX BTC */}
         <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("History" as never)}>
           <View style={[ui.card, compact && styles.cardCompact]}>
             <View style={styles.priceHeaderRow}>
@@ -317,7 +382,11 @@ export default function DashboardScreen() {
               </View>
 
               <TouchableOpacity
-                style={[styles.refreshBtn, refreshing && styles.refreshBtnDisabled, compact && styles.refreshBtnCompact]}
+                style={[
+                  styles.refreshBtn,
+                  refreshing && styles.refreshBtnDisabled,
+                  compact && styles.refreshBtnCompact,
+                ]}
                 onPress={handleManualRefresh}
                 disabled={refreshing}
               >
@@ -404,7 +473,6 @@ const styles = StyleSheet.create({
     marginTop: 10,
   },
 
-  // ✅ header row + chevron icon
   headerRow: {
     flexDirection: "row",
     justifyContent: "space-between",
index 24c9e776caa7cf3c5794facd8a55ce05395fcf5d..4f753fb66eb8640676ba003bbe1cafad8b316cbc 100644 (file)
@@ -6,16 +6,26 @@ import { loadSettings, saveSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
 
 import { requestNotificationPermission } from "../services/notificationService";
+import { clearSession, loadSession } from "../utils/sessionStorage";
+
 import { ui } from "../components/ui/uiStyles";
 
-export default function SettingsScreen() {
+/**
+ * SettingsScreen (Step 4)
+ * ----------------------
+ * Paramètres stockés par userId (via settingsStorage).
+ * Ajout : bouton Déconnexion.
+ */
+export default function SettingsScreen({ onLogout }: { onLogout?: () => void }) {
   const [settings, setSettings] = useState<UserSettings | null>(null);
   const [infoMessage, setInfoMessage] = useState<string | null>(null);
+  const [sessionLabel, setSessionLabel] = useState<string>("");
 
   useEffect(() => {
     async function init() {
-      const data = await loadSettings();
+      const [data, session] = await Promise.all([loadSettings(), loadSession()]);
       setSettings(data);
+      setSessionLabel(session?.email ?? "—");
     }
     init();
   }, []);
@@ -63,11 +73,26 @@ export default function SettingsScreen() {
     setInfoMessage("Paramètres sauvegardés ✅");
   };
 
+  const handleLogout = async () => {
+    await clearSession();
+    onLogout?.(); // App.tsx repasse sur Auth
+  };
+
   return (
     <SafeAreaView style={styles.safeArea}>
       <View style={ui.container}>
         <Text style={styles.screenTitle}>Paramètres</Text>
 
+        {/* Carte session */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Compte</Text>
+          <Text style={ui.muted}>Connecté : <Text style={styles.boldInline}>{sessionLabel}</Text></Text>
+
+          <TouchableOpacity style={styles.secondaryButton} onPress={handleLogout}>
+            <Text style={styles.secondaryButtonText}>Se déconnecter</Text>
+          </TouchableOpacity>
+        </View>
+
         {/* Carte : Devise */}
         <View style={ui.card}>
           <Text style={ui.title}>Devise</Text>
@@ -91,9 +116,7 @@ export default function SettingsScreen() {
         {/* Carte : Notifications */}
         <View style={ui.card}>
           <Text style={ui.title}>Notifications</Text>
-          <Text style={ui.value}>
-            Statut : {settings.notificationsEnabled ? "ON" : "OFF"}
-          </Text>
+          <Text style={ui.value}>Statut : {settings.notificationsEnabled ? "ON" : "OFF"}</Text>
 
           <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleNotifications}>
             <Text style={ui.buttonText}>
@@ -101,7 +124,7 @@ export default function SettingsScreen() {
             </Text>
           </TouchableOpacity>
 
-          {!!infoMessage && <Text style={styles.infoText}>{infoMessage}</Text>}
+          {!!infoMessage && <Text style={[ui.muted, { marginTop: 10 }]}>{infoMessage}</Text>}
         </View>
 
         {/* Bouton Save */}
@@ -124,25 +147,33 @@ const styles = StyleSheet.create({
     marginBottom: 12,
     color: "#0f172a",
   },
-  infoText: {
-    marginTop: 10,
-    opacity: 0.8,
+  boldInline: {
+    fontWeight: "900",
+    color: "#0f172a",
   },
 
-  /**
-   * fullButton
-   * ----------
-   * On neutralise les propriétés "grille" de ui.button (flexGrow/flexBasis),
-   * car elles sont utiles dans ActionsCard, mais pas dans Settings.
-   */
   fullButton: {
     flexGrow: 0,
     flexBasis: "auto",
     width: "100%",
     marginTop: 10,
   },
-
   saveButton: {
+    marginTop: 4,
     marginBottom: 10,
   },
+
+  secondaryButton: {
+    marginTop: 10,
+    paddingVertical: 10,
+    borderRadius: 10,
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    alignItems: "center",
+    backgroundColor: "#fff",
+  },
+  secondaryButtonText: {
+    fontWeight: "900",
+    color: "#dc2626",
+  },
 });
\ No newline at end of file
index 07683790020acd95d17a4ee75b76f767eeb04217..0368f014c47e9255afed9e6304c68acc0d030e28 100644 (file)
@@ -1,57 +1,55 @@
-import { View, Text, StyleSheet, TouchableOpacity, TextInput, Alert as RNAlert } from "react-native";
-import { useMemo, useState } from "react";
+import {
+  View,
+  Text,
+  StyleSheet,
+  TouchableOpacity,
+  TextInput,
+  Alert as RNAlert,
+  FlatList,
+} from "react-native";
+import { useCallback, useMemo, useState } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
 import { useFocusEffect } from "@react-navigation/native";
-import { useCallback } from "react";
 
 import { ui } from "../components/ui/uiStyles";
-import { loadWallet, saveWallet, clearWallet } from "../utils/walletStorage";
-import type { WalletState } from "../models/Wallet";
-import { fetchDashboardSummary } from "../services/dashboardService";
+import type { PortfolioAsset, PortfolioState } from "../models/Portfolio";
+import { loadPortfolio, savePortfolio, clearPortfolio } from "../utils/portfolioStorage";
+import { getMockPrice } from "../mocks/prices.mock";
 import { loadSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
 
 /**
- * WalletScreen (WF-03 Step 1)
- * ---------------------------
- * Mono-utilisateur / mono-crypto : BTC uniquement.
- * - L'utilisateur encode la quantité de BTC qu'il possède
- * - On calcule la valeur estimée via le prix BTC du dashboard
- * - Stockage local (AsyncStorage) pour ne pas dépendre de l'API
+ * WalletScreen (Step 3)
+ * ---------------------
+ * Mono-user / multi-cryptos
+ * - liste d'assets (BTC, ETH, SOL...)
+ * - quantité par asset
+ * - valeur globale du portefeuille
+ *
+ * Aujourd'hui : prix mock
+ * Demain : prix via GET /api/price/current?pair=XXX/EUR
  */
 export default function WalletScreen() {
-  const [wallet, setWallet] = useState<WalletState | null>(null);
+  const [portfolio, setPortfolio] = useState<PortfolioState | null>(null);
   const [settings, setSettings] = useState<UserSettings | null>(null);
 
-  // Prix BTC actuel (mock aujourd'hui, API demain)
-  const [btcPrice, setBtcPrice] = useState<number | null>(null);
-
-  // input texte (évite les bugs de virgule/points)
+  // Ajout asset
+  const [symbolInput, setSymbolInput] = useState<string>("BTC");
   const [qtyInput, setQtyInput] = useState<string>("0");
 
   const [info, setInfo] = useState<string | null>(null);
 
-  // Recharge quand l’écran reprend le focus (retour depuis autre page)
   useFocusEffect(
     useCallback(() => {
       let active = true;
 
       async function init() {
         setInfo(null);
-
-        const [w, s, dash] = await Promise.all([
-          loadWallet(),
-          loadSettings(),
-          fetchDashboardSummary(),
-        ]);
-
+        const [p, s] = await Promise.all([loadPortfolio(), loadSettings()]);
         if (!active) return;
 
-        setWallet(w);
+        setPortfolio(p);
         setSettings(s);
-        setBtcPrice(dash.price);
-
-        setQtyInput(String(w.quantity));
       }
 
       init();
@@ -62,6 +60,11 @@ export default function WalletScreen() {
     }, [])
   );
 
+  const lastUpdatedLabel = useMemo(() => {
+    if (!portfolio) return "—";
+    return new Date(portfolio.updatedAtMs).toLocaleString();
+  }, [portfolio]);
+
   const parsedQty = useMemo(() => {
     const normalized = qtyInput.replace(",", ".").trim();
     const val = Number(normalized);
@@ -70,49 +73,101 @@ export default function WalletScreen() {
     return val;
   }, [qtyInput]);
 
+  const normalizedSymbol = useMemo(() => symbolInput.toUpperCase().trim(), [symbolInput]);
+
   const totalValue = useMemo(() => {
-    if (parsedQty === null || btcPrice === null) return null;
-    return parsedQty * btcPrice;
-  }, [parsedQty, btcPrice]);
+    if (!portfolio) return 0;
 
-  const lastUpdatedLabel = useMemo(() => {
-    if (!wallet) return "—";
-    return new Date(wallet.updatedAtMs).toLocaleString();
-  }, [wallet]);
+    return portfolio.assets.reduce((sum, a) => {
+      const price = getMockPrice(a.symbol);
+      if (price === null) return sum;
+      return sum + a.quantity * price;
+    }, 0);
+  }, [portfolio]);
 
-  const handleSave = async () => {
-    if (!wallet) return;
+  const handleAddOrUpdate = async () => {
+    if (!portfolio) return;
+
+    setInfo(null);
+
+    if (!normalizedSymbol || normalizedSymbol.length < 2) {
+      setInfo("Symbole invalide (ex: BTC).");
+      return;
+    }
 
     if (parsedQty === null) {
       setInfo("Quantité invalide. Exemple : 0.25");
       return;
     }
 
-    const updated: WalletState = {
-      ...wallet,
-      quantity: parsedQty,
+    const price = getMockPrice(normalizedSymbol);
+    if (price === null) {
+      setInfo("Prix inconnu (mock). Essayez: BTC, ETH, SOL, ADA.");
+      return;
+    }
+
+    const existingIndex = portfolio.assets.findIndex((a) => a.symbol === normalizedSymbol);
+
+    let updatedAssets: PortfolioAsset[];
+    if (existingIndex >= 0) {
+      // update
+      updatedAssets = portfolio.assets.map((a) =>
+        a.symbol === normalizedSymbol ? { ...a, quantity: parsedQty } : a
+      );
+    } else {
+      // add
+      updatedAssets = [...portfolio.assets, { symbol: normalizedSymbol, quantity: parsedQty }];
+    }
+
+    const updated: PortfolioState = {
+      assets: updatedAssets,
       updatedAtMs: Date.now(),
     };
 
-    await saveWallet(updated);
-    setWallet(updated);
-    setInfo("Portefeuille sauvegardé ✅");
+    await savePortfolio(updated);
+    setPortfolio(updated);
+
+    setInfo(existingIndex >= 0 ? "Asset mis à jour ✅" : "Asset ajouté ✅");
+  };
+
+  const handleDelete = (symbol: string) => {
+    if (!portfolio) return;
+
+    RNAlert.alert(
+      `Supprimer ${symbol} ?`,
+      "Cette action retire l’asset du portefeuille local.",
+      [
+        { text: "Annuler", style: "cancel" },
+        {
+          text: "Supprimer",
+          style: "destructive",
+          onPress: async () => {
+            const updated: PortfolioState = {
+              assets: portfolio.assets.filter((a) => a.symbol !== symbol),
+              updatedAtMs: Date.now(),
+            };
+            await savePortfolio(updated);
+            setPortfolio(updated);
+            setInfo(`${symbol} supprimé ✅`);
+          },
+        },
+      ]
+    );
   };
 
   const handleClear = () => {
     RNAlert.alert(
       "Réinitialiser le portefeuille ?",
-      "Cela remet la quantité BTC à 0 (stockage local).",
+      "Cela supprime tous les assets du stockage local.",
       [
         { text: "Annuler", style: "cancel" },
         {
           text: "Réinitialiser",
           style: "destructive",
           onPress: async () => {
-            await clearWallet();
-            const fresh = await loadWallet();
-            setWallet(fresh);
-            setQtyInput("0");
+            await clearPortfolio();
+            const fresh = await loadPortfolio();
+            setPortfolio(fresh);
             setInfo("Portefeuille réinitialisé ✅");
           },
         },
@@ -120,7 +175,7 @@ export default function WalletScreen() {
     );
   };
 
-  if (!wallet || !settings) {
+  if (!portfolio || !settings) {
     return (
       <View style={ui.centered}>
         <Text>Chargement du portefeuille…</Text>
@@ -130,62 +185,113 @@ export default function WalletScreen() {
 
   return (
     <SafeAreaView style={styles.safeArea}>
-      <View style={ui.container}>
-        <Text style={styles.screenTitle}>Portefeuille</Text>
-
-        {/* Carte BTC */}
-        <View style={ui.card}>
-          <Text style={ui.title}>BTC</Text>
-
-          <Text style={ui.muted}>Quantité détenue</Text>
-
-          <TextInput
-            value={qtyInput}
-            onChangeText={setQtyInput}
-            keyboardType="decimal-pad"
-            placeholder="ex: 0.25"
-            style={styles.input}
-          />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>
-            Prix BTC actuel :{" "}
-            <Text style={styles.boldInline}>
-              {btcPrice !== null ? `${btcPrice.toFixed(2)} ${settings.currency}` : "—"}
+      <FlatList
+        contentContainerStyle={ui.container}
+        data={portfolio.assets}
+        keyExtractor={(it) => it.symbol}
+        ListHeaderComponent={
+          <View>
+            <Text style={styles.screenTitle}>Portefeuille</Text>
+
+            {/* Résumé global */}
+            <View style={ui.card}>
+              <Text style={ui.title}>Résumé</Text>
+
+              <View style={ui.rowBetween}>
+                <Text style={ui.value}>Valeur globale :</Text>
+                <Text style={ui.valueBold}>
+                  {totalValue.toFixed(2)} {settings.currency}
+                </Text>
+              </View>
+
+              <Text style={[ui.muted, { marginTop: 6 }]}>
+                Dernière mise à jour :{" "}
+                <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
+              </Text>
+
+              <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
+                <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
+              </TouchableOpacity>
+            </View>
+
+            {/* Ajouter / modifier */}
+            <View style={ui.card}>
+              <Text style={ui.title}>Ajouter / Modifier un asset</Text>
+
+              <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL, ADA)</Text>
+              <TextInput
+                value={symbolInput}
+                onChangeText={setSymbolInput}
+                autoCapitalize="characters"
+                placeholder="BTC"
+                style={styles.input}
+              />
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>Quantité</Text>
+              <TextInput
+                value={qtyInput}
+                onChangeText={setQtyInput}
+                keyboardType="decimal-pad"
+                placeholder="0.25"
+                style={styles.input}
+              />
+
+              <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleAddOrUpdate}>
+                <Text style={ui.buttonText}>Enregistrer</Text>
+              </TouchableOpacity>
+
+              {!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
+              <Text style={[ui.muted, { marginTop: 10 }]}>
+                Prix utilisés = mock pour Step 3 (API à brancher plus tard).
+              </Text>
+            </View>
+
+            <Text style={[ui.muted, { marginBottom: 10 }]}>
+              Liste des assets :
             </Text>
-          </Text>
-
-          <Text style={[ui.muted, { marginTop: 6 }]}>
-            Valeur estimée :{" "}
-            <Text style={styles.boldInline}>
-              {totalValue !== null ? `${totalValue.toFixed(2)} ${settings.currency}` : "—"}
-            </Text>
-          </Text>
-
-          {/* ✅ Dernière mise à jour */}
-          <Text style={[ui.muted, { marginTop: 6 }]}>
-            Dernière mise à jour :{" "}
-            <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
-          </Text>
-
-          <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
-            <Text style={ui.buttonText}>Enregistrer</Text>
-          </TouchableOpacity>
-
-          <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
-            <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
-          </TouchableOpacity>
-
-          {!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
-        </View>
-
-        {/* Carte info Step */}
-        <View style={ui.card}>
-          <Text style={ui.title}>Step 1</Text>
-          <Text style={ui.muted}>
-            Mono-utilisateur / mono-crypto (BTC). Step 3 : portefeuille multi-cryptos + valeur globale.
-          </Text>
-        </View>
-      </View>
+          </View>
+        }
+        ListEmptyComponent={
+          <View style={ui.card}>
+            <Text style={ui.title}>Aucun asset</Text>
+            <Text style={ui.muted}>Ajoutez BTC/ETH/SOL… pour commencer.</Text>
+          </View>
+        }
+        renderItem={({ item }) => {
+          const price = getMockPrice(item.symbol);
+          const value = price !== null ? item.quantity * price : null;
+
+          return (
+            <View style={ui.card}>
+              <View style={ui.rowBetween}>
+                <Text style={ui.valueBold}>{item.symbol}</Text>
+                <TouchableOpacity onPress={() => handleDelete(item.symbol)}>
+                  <Text style={styles.deleteText}>Supprimer</Text>
+                </TouchableOpacity>
+              </View>
+
+              <View style={[ui.rowBetween, { marginTop: 8 }]}>
+                <Text style={ui.value}>Quantité</Text>
+                <Text style={ui.valueBold}>{item.quantity.toFixed(6)}</Text>
+              </View>
+
+              <View style={[ui.rowBetween, { marginTop: 6 }]}>
+                <Text style={ui.value}>Prix (mock)</Text>
+                <Text style={ui.valueBold}>
+                  {price !== null ? `${price.toFixed(2)} ${settings.currency}` : "—"}
+                </Text>
+              </View>
+
+              <View style={[ui.rowBetween, { marginTop: 6 }]}>
+                <Text style={ui.value}>Valeur</Text>
+                <Text style={ui.valueBold}>
+                  {value !== null ? `${value.toFixed(2)} ${settings.currency}` : "—"}
+                </Text>
+              </View>
+            </View>
+          );
+        }}
+      />
     </SafeAreaView>
   );
 }
@@ -237,4 +343,9 @@ const styles = StyleSheet.create({
     fontWeight: "900",
     color: "#dc2626",
   },
+
+  deleteText: {
+    fontWeight: "900",
+    color: "#dc2626",
+  },
 });
\ No newline at end of file
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
diff --git a/Wallette/mobile/src/utils/portfolioStorage.ts b/Wallette/mobile/src/utils/portfolioStorage.ts
new file mode 100644 (file)
index 0000000..8774d15
--- /dev/null
@@ -0,0 +1,53 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import type { PortfolioState } from "../models/Portfolio";
+import { loadSession } from "./sessionStorage";
+
+/**
+ * KEY devient "portfolio:<userId>"
+ */
+function keyFor(userId: string) {
+  return `portfolio:${userId}`;
+}
+
+const DEFAULT_PORTFOLIO: PortfolioState = {
+  assets: [],
+  updatedAtMs: Date.now(),
+};
+
+export async function loadPortfolio(): Promise<PortfolioState> {
+  const session = await loadSession();
+  if (!session) return DEFAULT_PORTFOLIO;
+
+  const raw = await AsyncStorage.getItem(keyFor(session.userId));
+  if (!raw) return DEFAULT_PORTFOLIO;
+
+  try {
+    const parsed = JSON.parse(raw) as Partial<PortfolioState>;
+    return {
+      ...DEFAULT_PORTFOLIO,
+      ...parsed,
+      assets: Array.isArray(parsed.assets) ? parsed.assets : [],
+    };
+  } catch {
+    return DEFAULT_PORTFOLIO;
+  }
+}
+
+export async function savePortfolio(portfolio: PortfolioState): Promise<void> {
+  const session = await loadSession();
+  if (!session) return;
+
+  const safe: PortfolioState = {
+    assets: Array.isArray(portfolio.assets) ? portfolio.assets : [],
+    updatedAtMs: Date.now(),
+  };
+
+  await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(safe));
+}
+
+export async function clearPortfolio(): Promise<void> {
+  const session = await loadSession();
+  if (!session) return;
+
+  await AsyncStorage.removeItem(keyFor(session.userId));
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/utils/sessionStorage.ts b/Wallette/mobile/src/utils/sessionStorage.ts
new file mode 100644 (file)
index 0000000..0c756c8
--- /dev/null
@@ -0,0 +1,45 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+
+export type Session = {
+  userId: string;
+  email: string;
+  createdAtMs: number;
+};
+
+const KEY = "session";
+
+/**
+ * Charge la session courante (si l'utilisateur est "connecté").
+ */
+export async function loadSession(): Promise<Session | null> {
+  const raw = await AsyncStorage.getItem(KEY);
+  if (!raw) return null;
+
+  try {
+    const parsed = JSON.parse(raw) as Partial<Session>;
+    if (!parsed.userId || !parsed.email) return null;
+
+    return {
+      userId: String(parsed.userId),
+      email: String(parsed.email),
+      createdAtMs: Number(parsed.createdAtMs ?? Date.now()),
+    };
+  } catch {
+    return null;
+  }
+}
+
+/**
+ * Sauvegarde une session.
+ * Ici c'est un login "local" (sans API).
+ */
+export async function saveSession(session: Session): Promise<void> {
+  await AsyncStorage.setItem(KEY, JSON.stringify(session));
+}
+
+/**
+ * Déconnexion.
+ */
+export async function clearSession(): Promise<void> {
+  await AsyncStorage.removeItem(KEY);
+}
\ No newline at end of file
index 07570d727a7eee2c342e08efb94dd3760f92a518..1fe6b39e951ce4d66c910b8048a25dac6452cd10 100644 (file)
@@ -1,26 +1,28 @@
 import AsyncStorage from "@react-native-async-storage/async-storage";
 import type { UserSettings } from "../models/UserSettings";
-
-const KEY = "userSettings";
+import { loadSession } from "./sessionStorage";
 
 /**
- * Settings par défaut
- * -------------------
- * On les fusionne avec ce qui est en storage.
+ * KEY devient "userSettings:<userId>"
  */
+function keyFor(userId: string) {
+  return `userSettings:${userId}`;
+}
+
 const DEFAULT_SETTINGS: UserSettings = {
   currency: "EUR",
   favoriteSymbol: "BTC",
   alertPreference: "critical",
   refreshMode: "manual",
   notificationsEnabled: false,
-
-  // ✅ stratégie par défaut
   selectedStrategyKey: "RSI_SIMPLE",
 };
 
 export async function loadSettings(): Promise<UserSettings> {
-  const raw = await AsyncStorage.getItem(KEY);
+  const session = await loadSession();
+  if (!session) return DEFAULT_SETTINGS; // sécurité (normalement, App bloque sans session)
+
+  const raw = await AsyncStorage.getItem(keyFor(session.userId));
   if (!raw) return DEFAULT_SETTINGS;
 
   try {
@@ -32,5 +34,8 @@ export async function loadSettings(): Promise<UserSettings> {
 }
 
 export async function saveSettings(settings: UserSettings): Promise<void> {
-  await AsyncStorage.setItem(KEY, JSON.stringify(settings));
+  const session = await loadSession();
+  if (!session) return;
+
+  await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(settings));
 }
\ No newline at end of file