]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Mis a jour app - Fonctionnel
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Mon, 2 Mar 2026 08:18:28 +0000 (09:18 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Mon, 2 Mar 2026 08:18:28 +0000 (09:18 +0100)
13 files changed:
Wallette/mobile/App.tsx
Wallette/mobile/src/components/AccountMenu.tsx
Wallette/mobile/src/config/env.ts
Wallette/mobile/src/screens/AccountScreen.tsx [deleted file]
Wallette/mobile/src/screens/AuthScreen.tsx [deleted file]
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/services/api/authApi.ts [deleted file]
Wallette/mobile/src/services/api/dashboardApi.ts
Wallette/mobile/src/services/api/http.ts
Wallette/mobile/src/services/api/priceApi.ts
Wallette/mobile/src/services/notificationService.ts
Wallette/mobile/src/services/socketService.ts
Wallette/mobile/src/services/strategyService.ts

index 8645062fb9a1930821fca7f8efa363315c8a8336..0cb5c4a605df6a1c6ac7b1745c4ed45a24e41b28 100644 (file)
@@ -3,7 +3,7 @@ import {
   createNavigationContainerRef,
 } from "@react-navigation/native";
 import { createNativeStackNavigator } from "@react-navigation/native-stack";
-import { TouchableOpacity, View, Text, Alert as RNAlert } from "react-native";
+import { TouchableOpacity, View, Text } from "react-native";
 import { Ionicons } from "@expo/vector-icons";
 import { useEffect, useState } from "react";
 
@@ -14,17 +14,13 @@ 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 TutorialScreen from "./src/screens/TutorialScreen";
-
-import AccountScreen from "./src/screens/AccountScreen";
 import AboutScreen from "./src/screens/AboutScreen";
 
 import AccountMenu from "./src/components/AccountMenu";
 
-import { loadSession } from "./src/utils/sessionStorage";
+import { loadSession, saveSession, type Session } from "./src/utils/sessionStorage";
 import { hasSeenTutorial } from "./src/utils/tutorialStorage";
-import { logout as authLogout } from "./src/services/api/authApi";
 
 export type RootStackParamList = {
   Dashboard: undefined;
@@ -33,74 +29,60 @@ export type RootStackParamList = {
   Alerts: undefined;
   Strategy: undefined;
   Wallet: undefined;
-  Account: undefined;
   About: undefined;
 };
 
 const Stack = createNativeStackNavigator<RootStackParamList>();
-
-// ✅ navigationRef (navigate depuis le Modal AccountMenu)
 export const navigationRef = createNavigationContainerRef<RootStackParamList>();
 
+const SERVER_USER_ID = "user-123";
+
 export default function App() {
   const [ready, setReady] = useState(false);
 
-  // Auth state
-  const [isAuthed, setIsAuthed] = useState(false);
-  const [sessionEmail, setSessionEmail] = useState<string>("—");
+  // affichage simple dans le menu
+  const [sessionLabel, setSessionLabel] = useState<string>(SERVER_USER_ID);
 
-  // Tutorial gate
+  // Tutoriel
   const [needsTutorial, setNeedsTutorial] = useState(false);
 
-  // Account menu
+  // Menu
   const [menuVisible, setMenuVisible] = useState(false);
 
   useEffect(() => {
     let active = true;
 
     async function init() {
+      // ✅ Session fixe (pas d’auth)
       const s = await loadSession();
       if (!active) return;
 
-      const authed = !!s;
-      setIsAuthed(authed);
-      setSessionEmail(s?.email ?? "—");
-
-      if (authed) {
-        const seen = await hasSeenTutorial();
+      if (!s) {
+        const session: Session = {
+          userId: SERVER_USER_ID,
+          email: SERVER_USER_ID,
+          createdAtMs: Date.now(),
+        };
+        await saveSession(session);
         if (!active) return;
-        setNeedsTutorial(!seen);
+        setSessionLabel(session.userId);
       } else {
-        setNeedsTutorial(false);
+        setSessionLabel(s.userId ?? SERVER_USER_ID);
       }
 
+      const seen = await hasSeenTutorial();
+      if (!active) return;
+      setNeedsTutorial(!seen);
+
       setReady(true);
     }
 
     void 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 authLogout();
-          setIsAuthed(false);
-          setSessionEmail("—");
-          setNeedsTutorial(false);
-          setMenuVisible(false);
-        },
-      },
-    ]);
-  };
-
   const go = (route: keyof RootStackParamList) => {
     if (!navigationRef.isReady()) return;
     navigationRef.navigate(route);
@@ -114,24 +96,7 @@ export default function App() {
     );
   }
 
-  // 1) Pas connecté -> Auth (hors stack)
-  if (!isAuthed) {
-    return (
-      <AuthScreen
-        onAuthenticated={async () => {
-          const s = await loadSession();
-          setSessionEmail(s?.email ?? "—");
-          setIsAuthed(true);
-
-          // Après login, on vérifie le tutoriel
-          const seen = await hasSeenTutorial();
-          setNeedsTutorial(!seen);
-        }}
-      />
-    );
-  }
-
-  // 2) Connecté mais tuto pas vu -> Tutoriel (hors stack)
+  // Tutoriel au premier lancement
   if (needsTutorial) {
     return (
       <TutorialScreen
@@ -142,16 +107,13 @@ export default function App() {
     );
   }
 
-  // 3) Connecté + tuto vu -> App normale
   return (
     <NavigationContainer ref={navigationRef}>
       <AccountMenu
         visible={menuVisible}
-        email={sessionEmail}
+        label={sessionLabel}
         onClose={() => setMenuVisible(false)}
-        onGoAccount={() => go("Account")}
         onGoAbout={() => go("About")}
-        onLogout={doLogout}
       />
 
       <Stack.Navigator id="MainStack" initialRouteName="Dashboard">
@@ -162,72 +124,36 @@ export default function App() {
             title: "Dashboard",
             headerRight: () => (
               <View style={{ flexDirection: "row", gap: 12 }}>
-                {/* 👤 Menu Compte */}
+                {/* Menu */}
                 <TouchableOpacity onPress={() => setMenuVisible(true)}>
-                  <Ionicons
-                    name="person-circle-outline"
-                    size={24}
-                    color="#0f172a"
-                  />
+                  <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"
-                  />
+                {/* 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="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="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="Settings" options={{ title: "Paramètres" }}>
           {() => (
             <SettingsScreen
               onRequestTutorial={() => {
-                // Force le tuto à s'afficher hors stack
                 setNeedsTutorial(true);
               }}
             />
           )}
         </Stack.Screen>
 
-        <Stack.Screen
-          name="Account"
-          component={AccountScreen}
-          options={{ title: "Compte" }}
-        />
-        <Stack.Screen
-          name="About"
-          component={AboutScreen}
-          options={{ title: "À propos" }}
-        />
+        <Stack.Screen name="About" component={AboutScreen} options={{ title: "À propos" }} />
       </Stack.Navigator>
     </NavigationContainer>
   );
index 2be54cb3442ce166a07e2970e44455538b506f56..ce5841dc19dbbc90011794198d5b954517f7715b 100644 (file)
@@ -1,97 +1,80 @@
-import { Modal, View, Text, StyleSheet, TouchableOpacity, Pressable } from "react-native";
+import { Modal, View, Text, StyleSheet, TouchableOpacity } from "react-native";
 import { Ionicons } from "@expo/vector-icons";
-import { ui } from "./ui/uiStyles";
 
 type Props = {
   visible: boolean;
-  email: string;
+  label: string; // ex: user-123
   onClose: () => void;
-
-  onGoAccount: () => void;
   onGoAbout: () => void;
-  onLogout: () => void;
 };
 
-export default function AccountMenu({
-  visible,
-  email,
-  onClose,
-  onGoAccount,
-  onGoAbout,
-  onLogout,
-}: Props) {
+export default function AccountMenu({ visible, label, onClose, onGoAbout }: 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}>
+    <Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
+      <View style={styles.backdrop}>
+        <View style={styles.card}>
           <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 style={styles.userRow}>
+              <Ionicons name="person-circle-outline" size={28} color="#0f172a" style={{ opacity: 0.8 }} />
+              <Text style={styles.label}>{label}</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} />
+          <TouchableOpacity
+            style={styles.item}
+            onPress={() => {
+              onClose();
+              onGoAbout();
+            }}
+          >
+            <Ionicons name="information-circle-outline" size={18} color="#0f172a" style={{ opacity: 0.75 }} />
             <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 style={[styles.item, styles.itemSecondary]} onPress={onClose}>
+            <Ionicons name="arrow-back-outline" size={18} color="#0f172a" style={{ opacity: 0.75 }} />
+            <Text style={styles.itemText}>Fermer</Text>
           </TouchableOpacity>
-        </Pressable>
-      </Pressable>
+        </View>
+      </View>
     </Modal>
   );
 }
 
 const styles = StyleSheet.create({
-  overlay: {
+  backdrop: {
     flex: 1,
-    backgroundColor: "rgba(15, 23, 42, 0.35)",
-    justifyContent: "flex-start",
-    paddingTop: 70,
-    paddingHorizontal: 16,
+    backgroundColor: "rgba(0,0,0,0.35)",
+    justifyContent: "center",
+    padding: 16,
   },
-
   card: {
     backgroundColor: "#fff",
-    borderRadius: 14,
+    borderRadius: 16,
+    padding: 14,
     borderWidth: 1,
     borderColor: "#e5e7eb",
-    padding: 12,
   },
-
   headerRow: {
     flexDirection: "row",
+    justifyContent: "space-between",
     alignItems: "center",
-    gap: 10,
-    paddingBottom: 10,
-    borderBottomWidth: 1,
-    borderBottomColor: "#e5e7eb",
     marginBottom: 10,
   },
-
-  title: {
-    fontSize: 16,
+  userRow: {
+    flexDirection: "row",
+    alignItems: "center",
+    gap: 8,
+  },
+  label: {
     fontWeight: "900",
     color: "#0f172a",
+    opacity: 0.9,
   },
-
   item: {
     flexDirection: "row",
     alignItems: "center",
@@ -99,21 +82,17 @@ const styles = StyleSheet.create({
     paddingVertical: 12,
     paddingHorizontal: 10,
     borderRadius: 12,
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    backgroundColor: "#fff",
+    marginTop: 10,
   },
-
-  itemDanger: {
-    marginTop: 6,
-    backgroundColor: "#dc262611",
-  },
-
-  itemIcon: {
-    width: 22,
+  itemSecondary: {
     opacity: 0.9,
   },
-
   itemText: {
-    flex: 1,
     fontWeight: "900",
     color: "#0f172a",
+    opacity: 0.85,
   },
 });
\ No newline at end of file
index 859dbcf8dac2484eb820add24349b2a51d748733..a41f826a1f7f88e388ac8411e0b297c103e81b71 100644 (file)
@@ -13,7 +13,7 @@ const PROD_GATEWAY = "https://wallette.duckdns.org";
 export const GATEWAY_BASE_URL = PROD_GATEWAY;
 
 // REST (via gateway)
-export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
+export const API_BASE_URL = "https://wallette.duckdns.org/api";
 
 // Socket.IO (via gateway)
 export const SERVER_URL = GATEWAY_BASE_URL;
diff --git a/Wallette/mobile/src/screens/AccountScreen.tsx b/Wallette/mobile/src/screens/AccountScreen.tsx
deleted file mode 100644 (file)
index d7be829..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
-import { useEffect, useState } from "react";
-import { SafeAreaView } from "react-native-safe-area-context";
-
-import { ui } from "../components/ui/uiStyles";
-import { loadSession } from "../utils/sessionStorage";
-import { findUserById, updateCurrentUserProfile } from "../utils/authUsersStorage";
-
-/**
- * AccountScreen (Step 4 - sans API)
- * --------------------------------
- * - Email : affiché mais NON modifiable (important)
- * - Username : affiché mais NON modifiable (sinon userId local casse)
- * - DisplayName : modifiable
- * - Mot de passe : modifiable (hash local)
- *
- * Plus tard : remplacement par une route API (DB).
- */
-export default function AccountScreen() {
-  const [loading, setLoading] = useState(true);
-
-  const [userId, setUserId] = useState<string>("");
-
-  const [email, setEmail] = useState<string>(""); // affichage only
-  const [username, setUsername] = useState<string>(""); // affichage only
-  const [displayName, setDisplayName] = useState<string>("");
-
-  const [password, setPassword] = useState<string>("");
-  const [password2, setPassword2] = useState<string>("");
-
-  const [info, setInfo] = useState<string | null>(null);
-  const [error, setError] = useState<string | null>(null);
-
-  useEffect(() => {
-    let active = true;
-
-    async function init() {
-      const session = await loadSession();
-      if (!session) {
-        if (!active) return;
-        setError("Session absente.");
-        setLoading(false);
-        return;
-      }
-
-      const u = await findUserById(session.userId);
-
-      if (!active) return;
-
-      setUserId(session.userId);
-      setEmail(u?.email ?? session.email);
-      setUsername(u?.username ?? session.userId.replace("user_", ""));
-      setDisplayName(u?.displayName ?? "");
-
-      setLoading(false);
-    }
-
-    init();
-
-    return () => {
-      active = false;
-    };
-  }, []);
-
-  if (loading) {
-    return (
-      <View style={ui.centered}>
-        <Text>Chargement du compte…</Text>
-      </View>
-    );
-  }
-
-  const handleSave = async () => {
-    setInfo(null);
-    setError(null);
-
-    // Mot de passe : optionnel, mais si rempli => validations
-    const wantsPasswordChange = password.trim().length > 0 || password2.trim().length > 0;
-
-    if (wantsPasswordChange) {
-      if (password.trim().length < 6) {
-        setError("Mot de passe trop court (min 6).");
-        return;
-      }
-      if (password !== password2) {
-        setError("Les mots de passe ne correspondent pas.");
-        return;
-      }
-    }
-
-    const res = await updateCurrentUserProfile({
-      displayName,
-      newPassword: wantsPasswordChange ? password : undefined,
-    });
-
-    if (!res.ok) {
-      setError(res.message);
-      return;
-    }
-
-    // reset password fields
-    setPassword("");
-    setPassword2("");
-
-    setInfo("Profil sauvegardé ✅ (local, en attendant l’API).");
-  };
-
-  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</Text>
-          <Text style={styles.mono}>{userId || "—"}</Text>
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Email (non modifiable)</Text>
-          <TextInput value={email} editable={false} style={[styles.input, styles.inputDisabled]} />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur (non modifiable)</Text>
-          <TextInput value={username} editable={false} style={[styles.input, styles.inputDisabled]} />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Nom affiché</Text>
-          <TextInput value={displayName} onChangeText={setDisplayName} placeholder="ex: Thibaud M." style={styles.input} />
-
-          <Text style={[ui.muted, { marginTop: 12 }]}>
-            ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation).
-          </Text>
-        </View>
-
-        <View style={ui.card}>
-          <Text style={ui.title}>Mot de passe</Text>
-          <Text style={ui.muted}>
-            Change optionnel. (Local pour l’instant, sera relié à l’API plus tard.)
-          </Text>
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
-          <TextInput value={password} onChangeText={setPassword} secureTextEntry placeholder="min 6 caractères" style={styles.input} />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
-          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry placeholder="retaper le mot de passe" style={styles.input} />
-        </View>
-
-        {!!error && <Text style={styles.errorText}>{error}</Text>}
-        {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
-
-        <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
-          <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" },
-
-  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",
-  },
-
-  inputDisabled: {
-    backgroundColor: "#f3f4f6",
-    color: "#111827",
-    opacity: 0.8,
-  },
-
-  fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 },
-
-  errorText: { color: "#dc2626", fontWeight: "900", marginBottom: 10 },
-});
\ No newline at end of file
diff --git a/Wallette/mobile/src/screens/AuthScreen.tsx b/Wallette/mobile/src/screens/AuthScreen.tsx
deleted file mode 100644 (file)
index 30620ba..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
-import { useMemo, useState } from "react";
-
-import { ui } from "../components/ui/uiStyles";
-import { login as authLogin, register as authRegister } from "../services/api/authApi";
-
-/**
- * AuthScreen
- * ----------
- * Connexion + Création de compte.
- * - Identifiant : email OU nom d’utilisateur
- * - Mot de passe : minimum 6 caractères
- *
- * Note technique (code) :
- * - L’écran appelle authApi (façade).
- * - Le stockage de session est géré côté utils.
- */
-export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => void }) {
-  const [mode, setMode] = useState<"login" | "register">("login");
-  const [error, setError] = useState<string | null>(null);
-
-  // Connexion
-  const [login, setLogin] = useState("");
-  const [password, setPassword] = useState("");
-
-  // Création compte
-  const [email, setEmail] = useState("");
-  const [username, setUsername] = useState("");
-  const [displayName, setDisplayName] = useState("");
-  const [password2, setPassword2] = useState("");
-
-  const isValidEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val.trim().toLowerCase());
-  const isValidUsername = (val: string) => /^[a-zA-Z0-9_]{3,20}$/.test(val.trim());
-
-  const title = useMemo(() => (mode === "login" ? "Connexion" : "Créer un compte"), [mode]);
-
-  const handleLogin = async () => {
-    setError(null);
-
-    const l = login.trim();
-    const p = password;
-
-    if (!l) return setError("Veuillez entrer un email ou un nom d’utilisateur.");
-    if (!p || p.length < 6) return setError("Mot de passe invalide (minimum 6 caractères).");
-
-    const res = await authLogin({ login: l, password: p });
-    if (!res.ok) return setError(res.message);
-
-    onAuthenticated();
-  };
-
-  const handleRegister = async () => {
-    setError(null);
-
-    const e = email.trim().toLowerCase();
-    const u = username.trim();
-    const d = displayName.trim();
-    const p1 = password;
-    const p2 = password2;
-
-    if (!isValidEmail(e)) return setError("Email invalide.");
-    if (!isValidUsername(u)) return setError("Nom d’utilisateur invalide (3 à 20, lettres/chiffres/_).");
-    if (!p1 || p1.length < 6) return setError("Mot de passe trop court (minimum 6 caractères).");
-    if (p1 !== p2) return setError("Les mots de passe ne correspondent pas.");
-
-    const res = await authRegister({
-      email: e,
-      username: u,
-      displayName: d || undefined,
-      password: p1,
-    });
-
-    if (!res.ok) return setError(res.message);
-
-    onAuthenticated();
-  };
-
-  return (
-    <SafeAreaView style={styles.safeArea}>
-      <View style={ui.container}>
-        <Text style={styles.title}>{title}</Text>
-
-        {/* Onglets */}
-        <View style={styles.tabsRow}>
-          <TouchableOpacity
-            style={[styles.tab, mode === "login" && styles.tabActive]}
-            onPress={() => {
-              setMode("login");
-              setError(null);
-            }}
-          >
-            <Text style={[styles.tabText, mode === "login" && styles.tabTextActive]}>Connexion</Text>
-          </TouchableOpacity>
-
-          <TouchableOpacity
-            style={[styles.tab, mode === "register" && styles.tabActive]}
-            onPress={() => {
-              setMode("register");
-              setError(null);
-            }}
-          >
-            <Text style={[styles.tabText, mode === "register" && styles.tabTextActive]}>Créer un compte</Text>
-          </TouchableOpacity>
-        </View>
-
-        <View style={ui.card}>
-          {mode === "login" ? (
-            <>
-              <Text style={ui.muted}>Email ou nom d’utilisateur</Text>
-              <TextInput
-                value={login}
-                onChangeText={setLogin}
-                autoCapitalize="none"
-                placeholder="ex : user@example.com ou pseudo"
-                style={styles.input}
-              />
-
-              <Text style={[ui.muted, { marginTop: 10 }]}>Mot de passe</Text>
-              <TextInput
-                value={password}
-                onChangeText={setPassword}
-                secureTextEntry
-                placeholder="minimum 6 caractères"
-                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}>Email</Text>
-              <TextInput
-                value={email}
-                onChangeText={setEmail}
-                autoCapitalize="none"
-                keyboardType="email-address"
-                placeholder="ex : user@example.com"
-                style={styles.input}
-              />
-
-              <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur</Text>
-              <TextInput
-                value={username}
-                onChangeText={setUsername}
-                autoCapitalize="none"
-                placeholder="ex : pseudo_123"
-                style={styles.input}
-              />
-
-              <Text style={[ui.muted, { marginTop: 10 }]}>Nom / prénom (optionnel)</Text>
-              <TextInput
-                value={displayName}
-                onChangeText={setDisplayName}
-                placeholder="ex : Thibaud"
-                style={styles.input}
-              />
-
-              <Text style={[ui.muted, { marginTop: 10 }]}>Mot de passe</Text>
-              <TextInput
-                value={password}
-                onChangeText={setPassword}
-                secureTextEntry
-                placeholder="minimum 6 caractères"
-                style={styles.input}
-              />
-
-              <Text style={[ui.muted, { marginTop: 10 }]}>Confirmer le mot de passe</Text>
-              <TextInput
-                value={password2}
-                onChangeText={setPassword2}
-                secureTextEntry
-                placeholder="retapez le mot de passe"
-                style={styles.input}
-              />
-
-              {!!error && <Text style={styles.errorText}>{error}</Text>}
-
-              <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleRegister}>
-                <Text style={ui.buttonText}>Créer le compte</Text>
-              </TouchableOpacity>
-            </>
-          )}
-        </View>
-      </View>
-    </SafeAreaView>
-  );
-}
-
-const styles = StyleSheet.create({
-  safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
-
-  title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
-
-  tabsRow: {
-    flexDirection: "row",
-    gap: 10,
-    marginBottom: 12,
-  },
-  tab: {
-    flex: 1,
-    paddingVertical: 10,
-    borderRadius: 12,
-    borderWidth: 1,
-    borderColor: "#e5e7eb",
-    backgroundColor: "#fff",
-    alignItems: "center",
-  },
-  tabActive: {
-    borderColor: "#0f172a",
-  },
-  tabText: { fontWeight: "900", color: "#0f172a", opacity: 0.7 },
-  tabTextActive: { opacity: 1 },
-
-  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 bfb21ac1fde57420219f520a62eb85d0ec254b11..ef2a04a5859b0608bfe6ba3c973a7b20f91a86f9 100644 (file)
@@ -138,12 +138,20 @@ export default function DashboardScreen() {
 
     // Dashboard summary (prix + signal) => dépend du pair BTC/EUR ou BTC/USDT
     try {
-      const dash = await getDashboardSummary();
-      setSummary(dash);
-    } catch {
-      setSummary(null);
-      setSoftError("Données indisponibles pour le moment. Réessayez dans quelques secondes.");
-    }
+  const dash = await getDashboardSummary();
+  setSummary(dash);
+} catch (e: any) {
+  setSummary(null);
+
+  const msg = String(e?.message ?? "");
+  if (msg.includes("401")) {
+    setSoftError("Accès refusé. Vérifiez l’accès protégé du serveur.");
+  } else if (msg.toLowerCase().includes("prix")) {
+    setSoftError(msg);
+  } else {
+    setSoftError("Signal indisponible pour le moment.");
+  }
+}
 
     // Wallet API (source de vérité) -> cache local
     if (uid) {
@@ -389,7 +397,7 @@ export default function DashboardScreen() {
           <Text style={ui.muted}>{displayPair}</Text>
 
           <Text style={styles.bigValue}>
-            {(summary?.price ?? 0).toFixed(2)} {quote}
+            {summary ? `${summary.price.toFixed(2)} ${quote}` : "—"}
           </Text>
 
           {/* Info transparente : prix/signal serveur sont basés sur BTC (DB) */}
diff --git a/Wallette/mobile/src/services/api/authApi.ts b/Wallette/mobile/src/services/api/authApi.ts
deleted file mode 100644 (file)
index 7da2921..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-import type { AuthUser } from "../../models/AuthUser";
-import type { Session } from "../../utils/sessionStorage";
-
-import { createUser, verifyLogin, findUserById } from "../../utils/authUsersStorage";
-import { loadSession, saveSession, clearSession } from "../../utils/sessionStorage";
-
-/**
- * authApi.ts
- * ----------
- * Décision projet : PAS d'auth serveur.
- *
- * IMPORTANT :
- * - Les services (alerts/signal/wallets) utilisent un userId FIXE côté serveur : "user-123".
- * - Donc la session côté mobile doit utiliser ce userId pour toutes les routes API.
- *
- * L'auth locale sert uniquement à l'expérience utilisateur (écran de connexion),
- * mais le userId utilisé pour l'API = "user-123" (align serveur).
- */
-
-const SERVER_USER_ID = "user-123";
-
-export type AuthResult =
-  | { ok: true; user: AuthUser; session: Session }
-  | { ok: false; message: string };
-
-export async function register(params: {
-  email: string;
-  username: string;
-  displayName?: string;
-  password: string;
-}): Promise<AuthResult> {
-  const res = await createUser(params);
-  if (!res.ok) return { ok: false, message: res.message };
-
-  // ✅ userId aligné serveur
-  const session: Session = {
-    userId: SERVER_USER_ID,
-    email: res.user.email,
-    createdAtMs: Date.now(),
-  };
-
-  await saveSession(session);
-
-  return { ok: true, user: res.user, session };
-}
-
-export async function login(params: {
-  login: string; // email OU username
-  password: string;
-}): Promise<AuthResult> {
-  const res = await verifyLogin(params);
-  if (!res.ok) return { ok: false, message: res.message };
-
-  // ✅ userId aligné serveur
-  const session: Session = {
-    userId: SERVER_USER_ID,
-    email: res.user.email,
-    createdAtMs: Date.now(),
-  };
-
-  await saveSession(session);
-
-  return { ok: true, user: res.user, session };
-}
-
-export async function logout(): Promise<void> {
-  await clearSession();
-}
-
-/**
- * Retourne l'utilisateur courant (profil local).
- * (Le userId serveur étant fixe, on conserve le profil local via la recherche par session.email si besoin.)
- */
-export async function getCurrentUser(): Promise<AuthUser | null> {
-  const session = await loadSession();
-  if (!session) return null;
-
-  // On tente par userId local d'abord (si jamais), sinon on retombe sur findUserById.
-  // Note : si ton système local lie le profil à un autre userId, on peut améliorer plus tard.
-  return await findUserById((session as any).userId ?? SERVER_USER_ID);
-}
-
-export async function getSession(): Promise<Session | null> {
-  return await loadSession();
-}
\ No newline at end of file
index e7810f2315071adbf3f25fa237b4a754d1347f71..214d294f1516802a529a657314360d17ac5b9e4b 100644 (file)
@@ -10,37 +10,56 @@ function safeDecision(action: any): TradeDecision {
   return "HOLD";
 }
 
+function toNumber(x: any, fallback = 0): number {
+  const n = typeof x === "number" ? x : Number(x);
+  return Number.isFinite(n) ? n : fallback;
+}
+
 function confidenceToLevel(conf: number): AlertLevel {
   if (conf >= 0.85) return "CRITICAL";
   if (conf >= 0.65) return "WARNING";
   return "INFO";
 }
 
-function toNumber(x: any, fallback = 0): number {
-  const n = typeof x === "number" ? x : Number(x);
-  return Number.isFinite(n) ? n : fallback;
-}
-
 export async function getDashboardSummary(): Promise<DashboardSummary> {
   const session = await loadSession();
   const userId = session?.userId;
-  if (!userId) throw new Error("Session absente : impossible de charger le dashboard.");
+  if (!userId) throw new Error("Session absente.");
 
   const settings = await loadSettings();
   const quote: "EUR" | "USDT" = settings.currency === "USDT" ? "USDT" : "EUR";
   const pair = `BTC/${quote}`;
 
-  // 1) Prix (toujours OK)
-  const priceRes = await getCurrentPrice(pair);
+  // 1) Prix
+  let priceRes;
+  try {
+    priceRes = await getCurrentPrice(pair);
+  } catch (e: any) {
+    throw new Error(e?.message ?? `Prix indisponible pour ${pair}.`);
+  }
 
   // 2) Signal (peut être null)
-  const sig = await apiGet<any>(
-    `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
-  );
+  let sig: any = null;
+  try {
+    sig = await apiGet<any>(
+      `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
+    );
+  } catch (e: any) {
+    // Signal indisponible, mais prix OK → on renvoie un “dashboard minimal”
+    return {
+      pair,
+      price: priceRes.price,
+      strategy: settings.selectedStrategyKey,
+      decision: "HOLD",
+      confidence: 0,
+      reason: "Signal indisponible pour le moment.",
+      alertLevel: "INFO",
+      timestamp: priceRes.timestampMs,
+    };
+  }
 
-  // serveur renvoie { ok:true, data: ... } => http.ts unwrap => sig = data
+  // Pas de signal récent
   if (!sig) {
-    // pas de signal récent : on renvoie un dashboard “HOLD” propre
     return {
       pair,
       price: priceRes.price,
@@ -53,23 +72,21 @@ export async function getDashboardSummary(): Promise<DashboardSummary> {
     };
   }
 
-  // Champs réels observés :
-  // action, confidence (string), reason, timestamp_ms, pair_code
   const decision = safeDecision(sig.action ?? sig.decision);
   const confidence = toNumber(sig.confidence, 0);
   const reason = String(sig.reason ?? sig.message ?? "—");
-
   const timestamp =
     toNumber(sig.timestamp_ms, 0) ||
     toNumber(sig.timestamp, 0) ||
     priceRes.timestampMs;
 
-  const alertLevel =
-    (String(sig.alertLevel ?? sig.criticality ?? "").toUpperCase() as AlertLevel) ||
-    confidenceToLevel(confidence);
-
-  // pair_code si présent, sinon pair demandé
   const pairOut = String(sig.pair_code ?? sig.pair ?? pair);
+  const levelFromServer = String(sig.alertLevel ?? sig.criticality ?? "").toUpperCase();
+
+  const alertLevel: AlertLevel =
+    levelFromServer === "CRITICAL" || levelFromServer === "WARNING" || levelFromServer === "INFO"
+      ? (levelFromServer as AlertLevel)
+      : confidenceToLevel(confidence);
 
   return {
     pair: pairOut,
@@ -78,9 +95,7 @@ export async function getDashboardSummary(): Promise<DashboardSummary> {
     decision,
     confidence,
     reason,
-    alertLevel: alertLevel === "CRITICAL" || alertLevel === "WARNING" || alertLevel === "INFO"
-      ? alertLevel
-      : confidenceToLevel(confidence),
+    alertLevel,
     timestamp,
   };
 }
\ No newline at end of file
index a7f51063262e5c931ea35a26a87bfcad05c9a2f2..7123d8b1c0654fb56f19c8269df19b37d6f24439 100644 (file)
@@ -1,16 +1,6 @@
-import { API_BASE_URL } from "../../config/env";
-import { BASIC_AUTH_USER, BASIC_AUTH_PASS } from "../../config/env";
+import { API_BASE_URL, BASIC_AUTH_USER, BASIC_AUTH_PASS } from "../../config/env";
 import { Buffer } from "buffer";
 
-/**
- * HTTP helper (fetch)
- * -------------------
- * - Ajoute Basic Auth si configuré (pour passer la barrière 401)
- * - Compatible avec 2 formats :
- *   A) wrap : { ok:true, data: ... } / { ok:false, error:{message} }
- *   B) raw  : { ... } / [...]
- */
-
 function getBasicAuthHeader(): string | null {
   const u = (BASIC_AUTH_USER ?? "").trim();
   const p = (BASIC_AUTH_PASS ?? "").trim();
index c91a3780d4f205409f95cb7e4f3b6b0b6eace44b..da599598136e2d11963efd31fdc1b845f6626365 100644 (file)
@@ -7,26 +7,35 @@ export type PriceCurrent = {
   source?: string;
 };
 
+function toNumber(x: any): number | null {
+  if (typeof x === "number" && Number.isFinite(x)) return x;
+  if (typeof x === "string") {
+    const n = Number(x);
+    return Number.isFinite(n) ? n : null;
+  }
+  return null;
+}
+
 export async function getCurrentPrice(pair: string): Promise<PriceCurrent> {
   const data = await apiGet<any>(`/price/current?pair=${encodeURIComponent(pair)}`);
 
   const price =
-    (typeof data?.current_price === "number" ? data.current_price : null) ??
-    (typeof data?.price === "number" ? data.price : null);
+    toNumber(data?.current_price) ??
+    toNumber(data?.price);
 
-  if (typeof price !== "number" || !Number.isFinite(price)) {
-    throw new Error("Prix invalide (API).");
+  if (price === null) {
+    throw new Error(`Prix indisponible pour ${pair}.`);
   }
 
   const ts =
-    (typeof data?.timestamp_ms === "number" ? data.timestamp_ms : null) ??
-    (typeof data?.timestampMs === "number" ? data.timestampMs : null) ??
+    toNumber(data?.timestamp_ms) ??
+    toNumber(data?.timestampMs) ??
     Date.now();
 
   return {
     pair: String(data?.pair ?? pair),
     timestampMs: Number(ts),
-    price: Number(price),
+    price,
     source: typeof data?.source === "string" ? data.source : undefined,
   };
 }
\ No newline at end of file
index 5e6cf1647742cdd335f180152248970f3f55f4d0..2bb80c2ebc97ca9e51b40a7f695144b01318d56e 100644 (file)
@@ -2,29 +2,15 @@ import * as Notifications from "expo-notifications";
 import { Platform } from "react-native";
 import type { Alert } from "../types/Alert";
 
-/**
- * Notification handler (Foreground)
- * ---------------------------------
- * Sur certaines versions, Expo demande aussi shouldShowBanner/shouldShowList.
- * On les met explicitement pour éviter les erreurs TypeScript.
- */
 Notifications.setNotificationHandler({
   handleNotification: async () => ({
-    shouldShowAlert: true,
-    shouldPlaySound: true,
-    shouldSetBadge: false,
-
-    // ✅ champs demandés par les versions récentes
     shouldShowBanner: true,
     shouldShowList: true,
+    shouldPlaySound: false,
+    shouldSetBadge: false,
   }),
 });
 
-/**
- * Initialisation Android (channel)
- * --------------------------------
- * Sur Android, un channel "alerts" permet d'avoir une importance HIGH.
- */
 export async function initNotificationChannel(): Promise<void> {
   if (Platform.OS !== "android") return;
 
@@ -37,20 +23,33 @@ export async function initNotificationChannel(): Promise<void> {
   });
 }
 
+function safeLevel(alert: Alert): string {
+  return String(alert.alertLevel ?? alert.criticality ?? "INFO");
+}
+
+function safeAction(alert: Alert): string {
+  return String(alert.action ?? "HOLD");
+}
+
+function safePair(alert: Alert): string {
+  return String(alert.pair ?? "—");
+}
+
+function safeConfidence(alert: Alert): number {
+  const c = alert.confidence;
+  return typeof c === "number" && Number.isFinite(c) ? c : 0;
+}
+
 function formatTitle(alert: Alert): string {
-  return `${alert.alertLevel} — ${alert.action} ${alert.pair}`;
+  return `${safeLevel(alert)} — ${safeAction(alert)} ${safePair(alert)}`;
 }
 
 function formatBody(alert: Alert): string {
-  const conf = Math.round(alert.confidence * 100);
-  return `${conf}% — ${alert.reason}`;
+  const confPct = Math.round(safeConfidence(alert) * 100);
+  const reason = String(alert.reason ?? alert.message ?? "—");
+  return `${confPct}% — ${reason}`;
 }
 
-/**
- * Demande de permission
- * ---------------------
- * Retourne true si l'utilisateur accepte.
- */
 export async function requestNotificationPermission(): Promise<boolean> {
   const current = await Notifications.getPermissionsAsync();
   if (current.status === "granted") return true;
@@ -59,22 +58,17 @@ export async function requestNotificationPermission(): Promise<boolean> {
   return req.status === "granted";
 }
 
-/**
- * Afficher une notification locale immédiate
- * ------------------------------------------
- * data doit être un Record<string, unknown> (pas un objet typé direct).
- */
 export async function showAlertNotification(alert: Alert): Promise<void> {
   await initNotificationChannel();
 
   const data: Record<string, unknown> = {
-    pair: alert.pair,
-    action: alert.action,
-    alertLevel: alert.alertLevel,
-    confidence: alert.confidence,
-    reason: alert.reason,
-    price: alert.price ?? null,
-    timestamp: alert.timestamp ?? Date.now(),
+    pair: alert.pair ?? null,
+    action: alert.action ?? null,
+    alertLevel: alert.alertLevel ?? alert.criticality ?? null,
+    confidence: safeConfidence(alert),
+    reason: alert.reason ?? alert.message ?? null,
+    price: typeof alert.price === "number" ? alert.price : null,
+    timestamp: typeof alert.timestamp === "number" ? alert.timestamp : Date.now(),
   };
 
   await Notifications.scheduleNotificationAsync({
@@ -84,6 +78,6 @@ export async function showAlertNotification(alert: Alert): Promise<void> {
       data,
       sound: "default",
     },
-    trigger: null, // immédiat
+    trigger: null,
   });
 }
\ No newline at end of file
index 76d9fd78a4ddb44515e80b3f9fa72a01b81c9dd7..c622621248ac1df6bc37b5e7eeb224afc09b62d8 100644 (file)
@@ -25,68 +25,42 @@ class SocketService {
     this.currentServerUrl = serverUrl;
     this.currentUserId = userId;
 
-    const attachHandlers = (sock: Socket) => {
-      const emitAuth = () => sock.emit("auth", userId);
-
-      sock.on("connect", () => {
-        console.log("✅ Socket connecté:", sock.id);
-        emitAuth();
-      });
-
-      sock.on("auth_success", (data: any) => {
-        console.log("✅ Auth success:", data?.message ?? data);
-      });
-
-      sock.on("alert", (alert: Alert) => {
-        for (const cb of this.listeners) cb(alert);
-      });
-
-      sock.on("disconnect", (reason: string) => {
-        console.log("⚠️ Socket disconnect:", reason);
-      });
-
-      sock.on("error", (err: any) => {
-        console.log("❌ Socket error:", err?.message ?? err);
-      });
-    };
-
-    // tentative websocket + polling
     const sock = io(serverUrl, {
       path: "/socket.io",
-      transports: ["websocket", "polling"],
+      transports: ["websocket"],      // ✅ on force websocket (plus stable en prod)
+      upgrade: false,                 // ✅ évite switch polling->ws
       reconnection: true,
-      reconnectionAttempts: 10,
+      reconnectionAttempts: Infinity, // ✅ mais...
+      reconnectionDelay: 1500,
       timeout: 10000,
     });
 
     this.socket = sock;
-    attachHandlers(sock);
+
+    sock.on("connect", () => {
+      console.log("✅ Socket connecté:", sock.id);
+      sock.emit("auth", userId);
+    });
+
+    sock.on("auth_success", (data: any) => {
+      console.log("✅ Auth success:", data?.message ?? data);
+    });
+
+    sock.on("alert", (alert: Alert) => {
+      for (const cb of this.listeners) cb(alert);
+    });
+
+    sock.on("disconnect", (reason: string) => {
+      console.log("⚠️ Socket disconnect:", reason);
+    });
 
     sock.on("connect_error", (err: any) => {
-      const msg = String(err?.message ?? err);
-      console.log("❌ Socket connect_error:", msg);
-
-      // fallback polling-only si websocket échoue
-      if (msg.toLowerCase().includes("websocket")) {
-        console.log("↩️ Fallback: polling-only");
-        this.disconnect();
-
-        const sockPolling = io(serverUrl, {
-          path: "/socket.io",
-          transports: ["polling"],
-          upgrade: false,
-          reconnection: true,
-          reconnectionAttempts: 10,
-          timeout: 10000,
-        });
-
-        this.socket = sockPolling;
-        attachHandlers(sockPolling);
-
-        sockPolling.on("connect_error", (e: any) => {
-          console.log("❌ Socket polling connect_error:", e?.message ?? e);
-        });
-      }
+      // ✅ log simple, pas de fallback spam
+      console.log("❌ Socket connect_error:", err?.message ?? err);
+    });
+
+    sock.on("error", (err: any) => {
+      console.log("❌ Socket error:", err?.message ?? err);
     });
   }
 
index 2d7a55829e3ff1b1fd6788f0c30a3538d86acdff..f0f7812ad8e4e57962c2b1675a38857f383810bf 100644 (file)
@@ -1,23 +1,73 @@
 import type { StrategyOption, StrategyKey } from "../types/Strategy";
-import { getStrategies } from "../mocks/strategies.mock";
-
-/**
- * strategyService
- * ---------------
- * Aujourd'hui : mock
- * Demain : REST
- *
- * REST attendu (contrat groupe) :
- * - POST /api/strategy/select
- */
+import { apiGet } from "./api/http";
+
+type Risk = "SAFE" | "NORMAL" | "AGGRESSIVE";
+
+function asArray(x: any): any[] {
+  if (Array.isArray(x)) return x;
+  if (Array.isArray(x?.strategies)) return x.strategies;
+  if (Array.isArray(x?.items)) return x.items;
+  if (Array.isArray(x?.data)) return x.data;
+  return [];
+}
+
+function normalizeRisk(raw: any): Risk {
+  const v = String(raw ?? "").toUpperCase();
+  if (v === "SAFE") return "SAFE";
+  if (v === "AGGRESSIVE") return "AGGRESSIVE";
+  if (v === "NORMAL") return "NORMAL";
+  return "NORMAL";
+}
+
+function normalizeStrategy(raw: any): StrategyOption | null {
+  const key = String(raw?.key ?? raw?.mode ?? raw?.strategy_key ?? raw?.strategyKey ?? "").trim();
+  if (!key) return null;
+
+  return {
+    key: key as StrategyKey,
+    label: String(raw?.label ?? raw?.name ?? key),
+    description: String(raw?.description ?? "—"),
+    risk: normalizeRisk(raw?.risk ?? raw?.level ?? raw?.modeRisk),
+  };
+}
+
 export async function fetchStrategies(): Promise<StrategyOption[]> {
-  return await getStrategies();
+  const candidates = ["/strategy/list", "/strategy/available", "/strategy/modes"];
+
+  for (const path of candidates) {
+    try {
+      const data = await apiGet<any>(path);
+      const arr = asArray(data);
+      const normalized = arr.map(normalizeStrategy).filter(Boolean) as StrategyOption[];
+      if (normalized.length > 0) return normalized;
+    } catch {
+      // continue
+    }
+  }
+
+  // Fallback “propre” (sans mocks)
+  return [
+    {
+      key: "RSI_SIMPLE" as StrategyKey,
+      label: "RSI Simple",
+      description: "Signal basé sur RSI (surachat / survente).",
+      risk: "NORMAL",
+    },
+    {
+      key: "EMA_CROSS" as StrategyKey,
+      label: "EMA Cross",
+      description: "Croisement de moyennes mobiles exponentielles.",
+      risk: "NORMAL",
+    },
+    {
+      key: "ALWAYS_HOLD" as StrategyKey,
+      label: "Always HOLD",
+      description: "Ne déclenche jamais d’achat/vente.",
+      risk: "SAFE",
+    },
+  ];
 }
 
-/**
- * Placeholder : plus tard on fera le POST ici.
- * Pour le moment, la sélection est gérée via AsyncStorage (settings).
- */
 export async function selectStrategy(_strategyKey: StrategyKey): Promise<void> {
   return;
 }
\ No newline at end of file