]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Mise en place des emplacement futur clée API + modification mineur
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Sat, 28 Feb 2026 01:36:37 +0000 (02:36 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Sat, 28 Feb 2026 01:36:37 +0000 (02:36 +0100)
14 files changed:
Wallette/mobile/src/config/env.ts
Wallette/mobile/src/screens/AlertsScreen.tsx
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/screens/HistoryScreen.tsx
Wallette/mobile/src/screens/SettingsScreen.tsx
Wallette/mobile/src/screens/StrategyScreen.tsx
Wallette/mobile/src/screens/WalletScreen.tsx
Wallette/mobile/src/services/api/alertsApi.ts
Wallette/mobile/src/services/api/dashboardApi.ts [new file with mode: 0644]
Wallette/mobile/src/services/api/http.ts [new file with mode: 0644]
Wallette/mobile/src/services/api/priceApi.ts
Wallette/mobile/src/services/api/signalApi.ts
Wallette/mobile/src/services/api/strategyApi.ts
Wallette/mobile/src/services/api/walletApi.ts [new file with mode: 0644]

index e394a9ba3e90d26318efd6c22ef74920439c9a77..4ffc47514416713d7b6e65636d843a878700720d 100644 (file)
@@ -3,35 +3,42 @@ import { Platform } from "react-native";
 /**
  * env.ts
  * ------
- * Objectif : centraliser les URLs réseau pour le mobile.
+ * Objectif : 1 seul point de config réseau.
  *
- * IMPORTANT (microservices + gateway) :
- * - Le mobile parle UNIQUEMENT au gateway : http://<HOST>:3000
- * - REST = http://<HOST>:3000/api/...
- * - Socket.IO = http://<HOST>:3000  (proxy /socket.io/* via gateway)
+ * DEV  : IP LAN (PC qui fait tourner gateway en local)
+ * PROD : URL publique (serveur prof / déploiement)
  *
- * Téléphone physique :
- * - <HOST> = IP du PC sur le Wi-Fi (ex: 192.168.x.x)
- *
- * Émulateurs (si un jour) :
- * - Android: 10.0.2.2
- * - iOS: localhost
+ * Le mobile parle UNIQUEMENT au Gateway :
+ * - REST   : <GATEWAY>/api/...
+ * - Socket : <GATEWAY>   (socket.io proxy via gateway)
  */
 
+// ✅ DEV (chez toi / en classe quand tu lances sur ton PC)
 const DEV_LAN_IP = "192.168.129.121";
+const DEV_GATEWAY = `http://${DEV_LAN_IP}:3000`;
+
+// ✅ PROD (quand le chef vous donne l’URL du serveur prof)
+// Mets un placeholder clair pour ne pas oublier
+const PROD_GATEWAY = "https://CHANGE_ME_GATEWAY_URL";
 
-function resolveHost(): string {
-  // On part sur téléphone physique (ton cas).
-  // Si un jour tu testes sur émulateur Android, tu peux mettre une condition:
-  // if (Platform.OS === "android" && __DEV__ && isEmulator) return "10.0.2.2";
-  return DEV_LAN_IP;
-}
+// Si tu veux autoriser un PROD en http (pas recommandé iOS), tu peux.
+// const PROD_GATEWAY = "http://CHANGE_ME_GATEWAY_URL:3000";
 
-// Base gateway (HTTP)
-export const GATEWAY_BASE_URL = `http://${resolveHost()}:3000`;
+/**
+ * Pour l'instant :
+ * - en Expo / dev => DEV_GATEWAY
+ * - en build (APK/IPA) => PROD_GATEWAY
+ */
+export const GATEWAY_BASE_URL = __DEV__ ? DEV_GATEWAY : PROD_GATEWAY;
 
 // REST (via gateway)
 export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
 
 // Socket.IO (via gateway)
-export const SERVER_URL = GATEWAY_BASE_URL;
\ No newline at end of file
+export const SERVER_URL = GATEWAY_BASE_URL;
+
+/**
+ * Helpers (debug)
+ */
+export const ENV_MODE = __DEV__ ? "DEV" : "PROD";
+export const PLATFORM = Platform.OS;
\ No newline at end of file
index 7668c219c6fc2720ce3795a4e0cf2b92a44866ed..e4ef5b0fc094076025cb7abaee080a50816d1373 100644 (file)
@@ -6,6 +6,13 @@ import { ui } from "../components/ui/uiStyles";
 import type { Alert } from "../types/Alert";
 import { alertStore } from "../services/alertStore";
 
+/**
+ * ✅ API-ready (façade)
+ * Aujourd'hui : clear local store
+ * Demain : REST /api/alerts/events ... etc.
+ */
+import { clearAlertsLocal } from "../services/api/alertsApi";
+
 /**
  * AlertsScreen
  * -----------
@@ -101,8 +108,8 @@ export default function AlertsScreen() {
         {
           text: "Supprimer",
           style: "destructive",
-          onPress: () => {
-            alertStore.clear?.();
+          onPress: async () => {
+            await clearAlertsLocal();
             setItems(alertStore.getAll?.() ?? []);
           },
         },
@@ -130,7 +137,8 @@ export default function AlertsScreen() {
         contentContainerStyle={ui.container}
         data={filteredSorted}
         keyExtractor={(it, idx) =>
-          it.id ??
+          // Alert n'a pas forcément d'id => fallback stable
+          (it as any).id ??
           `${it.timestamp}-${it.pair}-${it.action}-${it.alertLevel}-${idx}`
         }
         ListHeaderComponent={
index d97a3e621bb7a262f10cbf1cf4fa8cf2ccad61d8..2aeeca9df0a7444a42f80c48f7090b6444010685 100644 (file)
@@ -12,7 +12,13 @@ import { useFocusEffect, useNavigation } from "@react-navigation/native";
 import { Ionicons } from "@expo/vector-icons";
 
 import type { DashboardSummary } from "../types/DashboardSummary";
-import { fetchDashboardSummary } from "../services/dashboardService";
+
+/**
+ * ✅ Façade Dashboard (API-ready)
+ * - aujourd'hui : ce fichier peut encore être mock si ton dashboardApi l'est
+ * - demain : REST via gateway (/api/...)
+ */
+import { getDashboardSummary } from "../services/api/dashboardApi";
 
 import { loadSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
@@ -27,16 +33,24 @@ import { showAlertNotification } from "../services/notificationService";
 
 import { loadPortfolio } from "../utils/portfolioStorage";
 import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
-import { getMockPrice } from "../mocks/prices.mock";
 
 import { loadSession } from "../utils/sessionStorage";
 
+/**
+ * ✅ API-only (price)
+ */
+import { getCurrentPrice } from "../services/api/priceApi";
+
 /**
  * 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
+ *
+ * IMPORTANT :
+ * - Les prix (portfolio) sont chargés via API-only (pas de mock).
+ * - Pour performance : on charge les prix du TOP 3 (affichage "valeur partielle").
  */
 export default function DashboardScreen() {
   const { height } = useWindowDimensions();
@@ -57,8 +71,65 @@ export default function DashboardScreen() {
   const [lastRefreshMs, setLastRefreshMs] = useState<number | null>(null);
   const [refreshing, setRefreshing] = useState<boolean>(false);
 
+  // ✅ Prix (API-only) pour TOP 3 assets
+  const [assetPrices, setAssetPrices] = useState<Record<string, number>>({});
+  const [assetPricesError, setAssetPricesError] = useState<string | null>(null);
+
   const navigation = useNavigation();
 
+  /**
+   * Top 3 assets (par quantité, puis symbole — simple et défendable)
+   * NOTE: si tu veux une vraie logique "top valeur", il faut les prix de tous les assets.
+   */
+  const topAssets: PortfolioAsset[] = useMemo(() => {
+    if (!portfolio) return [];
+
+    const copy = [...portfolio.assets];
+    copy.sort((a, b) => b.quantity - a.quantity || a.symbol.localeCompare(b.symbol));
+    return copy.slice(0, 3);
+  }, [portfolio]);
+
+  const remainingCount = useMemo(() => {
+    if (!portfolio) return 0;
+    return Math.max(0, portfolio.assets.length - topAssets.length);
+  }, [portfolio, topAssets]);
+
+  /**
+   * Charge prix API-only pour les TOP 3 assets
+   */
+  const loadTopAssetPrices = useCallback(async (assets: PortfolioAsset[], currency: string) => {
+    setAssetPricesError(null);
+
+    if (assets.length === 0) {
+      setAssetPrices({});
+      return;
+    }
+
+    try {
+      const entries = await Promise.all(
+        assets.map(async (a) => {
+          const pair = `${a.symbol}/${currency}`;
+          const cur = await getCurrentPrice(pair);
+          return [a.symbol, cur.price] as const;
+        })
+      );
+
+      const map: Record<string, number> = {};
+      for (const [sym, pr] of entries) map[sym] = pr;
+      setAssetPrices(map);
+    } catch (e: any) {
+      setAssetPrices({});
+      setAssetPricesError(e?.message ?? "Impossible de charger les prix (API).");
+    }
+  }, []);
+
+  /**
+   * Chargement initial (focus)
+   * - dashboard
+   * - settings
+   * - portfolio
+   * + prix (API-only) sur top 3
+   */
   useFocusEffect(
     useCallback(() => {
       let isActive = true;
@@ -69,7 +140,7 @@ export default function DashboardScreen() {
           setLoading(true);
 
           const [dashboardData, userSettings, portfolioData] = await Promise.all([
-            fetchDashboardSummary(),
+            getDashboardSummary(),
             loadSettings(),
             loadPortfolio(),
           ]);
@@ -80,6 +151,12 @@ export default function DashboardScreen() {
           setSettings(userSettings);
           setPortfolio(portfolioData);
           setLastRefreshMs(Date.now());
+
+          // prix API-only sur top 3
+          const copy = [...portfolioData.assets];
+          copy.sort((a, b) => b.quantity - a.quantity || a.symbol.localeCompare(b.symbol));
+          const top = copy.slice(0, 3);
+          await loadTopAssetPrices(top, userSettings.currency);
         } catch {
           if (isActive) setError("Impossible de charger le dashboard.");
         } finally {
@@ -87,14 +164,17 @@ export default function DashboardScreen() {
         }
       }
 
-      loadData();
+      void loadData();
 
       return () => {
         isActive = false;
       };
-    }, [])
+    }, [loadTopAssetPrices])
   );
 
+  /**
+   * Refresh auto (si activé)
+   */
   useEffect(() => {
     if (!settings) return;
     if (settings.refreshMode !== "auto") return;
@@ -103,7 +183,7 @@ export default function DashboardScreen() {
 
     const intervalId = setInterval(async () => {
       try {
-        const data = await fetchDashboardSummary();
+        const data = await getDashboardSummary();
         if (!cancelled) {
           setSummary(data);
           setLastRefreshMs(Date.now());
@@ -119,13 +199,21 @@ export default function DashboardScreen() {
     };
   }, [settings]);
 
+  /**
+   * Refresh manuel
+   */
   const handleManualRefresh = async () => {
     try {
       setRefreshing(true);
-      const data = await fetchDashboardSummary();
+      const data = await getDashboardSummary();
       setSummary(data);
       setLastRefreshMs(Date.now());
       setError(null);
+
+      // refresh prix top assets aussi (API-only)
+      if (settings) {
+        await loadTopAssetPrices(topAssets, settings.currency);
+      }
     } catch {
       setError("Erreur lors de l'actualisation.");
     } finally {
@@ -194,6 +282,10 @@ export default function DashboardScreen() {
     };
   }, [settings]);
 
+  /**
+   * Alerte "urgente" (priorité : CRITICAL > WARNING > INFO)
+   * Le dashboard n'affiche qu'une seule alerte (la plus importante).
+   */
   const urgentAlert: Alert | null = useMemo(() => {
     if (liveAlerts.length === 0) return null;
 
@@ -206,32 +298,24 @@ export default function DashboardScreen() {
     return liveAlerts[0];
   }, [liveAlerts]);
 
-  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 [];
+  /**
+   * Valeur portefeuille (partielle = top3), API-only
+   */
+  const portfolioPartialValue = useMemo(() => {
+    if (!portfolio) return null;
 
-    const withValue = portfolio.assets.map((a) => {
-      const price = getMockPrice(a.symbol) ?? 0;
-      return { ...a, _value: a.quantity * price };
-    });
+    let sum = 0;
+    let hasAny = false;
 
-    withValue.sort((a, b) => (b as any)._value - (a as any)._value);
-    return withValue.slice(0, 3).map(({ symbol, quantity }) => ({ symbol, quantity }));
-  }, [portfolio]);
+    for (const a of topAssets) {
+      const price = assetPrices[a.symbol];
+      if (typeof price !== "number") continue;
+      hasAny = true;
+      sum += a.quantity * price;
+    }
 
-  const remainingCount = useMemo(() => {
-    if (!portfolio) return 0;
-    return Math.max(0, portfolio.assets.length - topAssets.length);
-  }, [portfolio, topAssets]);
+    return hasAny ? sum : null;
+  }, [portfolio, topAssets, assetPrices]);
 
   if (loading) {
     return (
@@ -311,12 +395,18 @@ export default function DashboardScreen() {
             </View>
 
             <View style={ui.rowBetween}>
-              <Text style={ui.value}>Valeur globale :</Text>
+              <Text style={ui.value}>Valeur (top 3) :</Text>
               <Text style={ui.valueBold}>
-                {portfolioTotalValue.toFixed(2)} {settings.currency}
+                {portfolioPartialValue !== null ? `${portfolioPartialValue.toFixed(2)} ${settings.currency}` : "—"}
               </Text>
             </View>
 
+            {!!assetPricesError && (
+              <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={2}>
+                ⚠️ {assetPricesError}
+              </Text>
+            )}
+
             {portfolio.assets.length === 0 ? (
               <Text style={[ui.muted, { marginTop: 8 }]}>
                 Aucun asset (ajoute BTC/ETH/SOL dans Portefeuille)
@@ -337,10 +427,6 @@ export default function DashboardScreen() {
                 )}
               </View>
             )}
-
-            <Text style={[ui.muted, { marginTop: 8 }]} numberOfLines={1}>
-              BTC @ {summary.price.toFixed(2)} {settings.currency} (mock)
-            </Text>
           </View>
         </TouchableOpacity>
 
index ef568e90ccf8e49fa8dda365a31b29ca9fb7e01a..8c29d507c8e17dc03068f7ae84ca4e0720e41c07 100644 (file)
@@ -3,7 +3,12 @@ import { useEffect, useState } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
 
 import type { Signal, SignalAction, SignalCriticality } from "../types/Signal";
-import { fetchRecentSignals } from "../services/signalService";
+
+/**
+ * ✅ API-ready (façade)
+ */
+import { getRecentSignals } from "../services/api/signalApi";
+
 import { ui } from "../components/ui/uiStyles";
 
 function getActionColor(action: SignalAction): string {
@@ -48,7 +53,8 @@ export default function HistoryScreen() {
       try {
         setError(null);
         setLoading(true);
-        const data = await fetchRecentSignals(20);
+
+        const data = await getRecentSignals(20);
         if (active) setItems(data);
       } catch {
         if (active) setError("Impossible de charger l'historique.");
@@ -112,45 +118,22 @@ export default function HistoryScreen() {
                   flexWrap: "wrap",
                 }}
               >
-                <View
-                  style={[
-                    ui.badge,
-                    { backgroundColor: `${actionColor}22`, marginTop: 0 },
-                  ]}
-                >
-                  <Text style={[ui.badgeText, { color: actionColor }]}>
-                    {item.action}
-                  </Text>
+                <View style={[ui.badge, { backgroundColor: `${actionColor}22`, marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: actionColor }]}>{item.action}</Text>
                 </View>
 
-                <View
-                  style={[
-                    ui.badge,
-                    { backgroundColor: `${critColor}22`, marginTop: 0 },
-                  ]}
-                >
-                  <Text style={[ui.badgeText, { color: critColor }]}>
-                    {item.criticality}
-                  </Text>
+                <View style={[ui.badge, { backgroundColor: `${critColor}22`, marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: critColor }]}>{item.criticality}</Text>
                 </View>
 
-                <View
-                  style={[
-                    ui.badge,
-                    { backgroundColor: "#11182722", marginTop: 0 },
-                  ]}
-                >
-                  <Text style={[ui.badgeText, { color: "#111827" }]}>
-                    {item.status}
-                  </Text>
+                <View style={[ui.badge, { backgroundColor: "#11182722", marginTop: 0 }]}>
+                  <Text style={[ui.badgeText, { color: "#111827" }]}>{item.status}</Text>
                 </View>
               </View>
 
               <View style={[ui.rowBetween, { marginTop: 10 }]}>
                 <Text style={ui.value}>Confiance</Text>
-                <Text style={ui.valueBold}>
-                  {(item.confidence * 100).toFixed(1)}%
-                </Text>
+                <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(1)}%</Text>
               </View>
 
               <View style={[ui.rowBetween, { marginTop: 6 }]}>
index aa1540a601aaa3edc5fea563cadb1d2e8c3c9f59..b8790e71e79a008820cfa8a6dd8a197b92cb2da9 100644 (file)
@@ -16,6 +16,10 @@ import { setSeenTutorial } from "../utils/tutorialStorage";
  * - Settings par userId (via settingsStorage)
  * - Notifications (local)
  * - Revoir le tutoriel (remet hasSeenTutorial à false)
+ *
+ * Note "API-ready" :
+ * - Aujourd'hui : AsyncStorage (local)
+ * - Demain : les mêmes actions pourront appeler une API (sans changer l'UI)
  */
 export default function SettingsScreen({
   onRequestTutorial,
@@ -24,13 +28,22 @@ export default function SettingsScreen({
 }) {
   const [settings, setSettings] = useState<UserSettings | null>(null);
   const [infoMessage, setInfoMessage] = useState<string | null>(null);
+  const [saving, setSaving] = useState<boolean>(false);
 
   useEffect(() => {
+    let active = true;
+
     async function init() {
       const data = await loadSettings();
+      if (!active) return;
       setSettings(data);
     }
+
     init();
+
+    return () => {
+      active = false;
+    };
   }, []);
 
   if (!settings) {
@@ -41,39 +54,69 @@ export default function SettingsScreen({
     );
   }
 
-  const toggleCurrency = () => {
+  /**
+   * Sauvegarde "safe"
+   * - évite les doubles clics
+   * - affiche un message simple
+   */
+  const persist = async (next: UserSettings, msg?: string) => {
+    try {
+      setSaving(true);
+      await saveSettings(next);
+      setSettings(next);
+      if (msg) setInfoMessage(msg);
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const toggleCurrency = async () => {
+    setInfoMessage(null);
     const newCurrency = settings.currency === "EUR" ? "USD" : "EUR";
-    setSettings({ ...settings, currency: newCurrency });
+    await persist({ ...settings, currency: newCurrency }, `Devise : ${newCurrency} ✅`);
   };
 
-  const toggleRefreshMode = () => {
+  const toggleRefreshMode = async () => {
+    setInfoMessage(null);
     const newMode = settings.refreshMode === "manual" ? "auto" : "manual";
-    setSettings({ ...settings, refreshMode: newMode });
+    await persist({ ...settings, refreshMode: newMode }, `Rafraîchissement : ${newMode} ✅`);
   };
 
   const toggleNotifications = async () => {
     setInfoMessage(null);
 
-    if (settings.notificationsEnabled) {
-      setSettings({ ...settings, notificationsEnabled: false });
-      setInfoMessage("Notifications désactivées (pense à sauvegarder).");
-      return;
-    }
-
-    const granted = await requestNotificationPermission();
-    if (!granted) {
-      setSettings({ ...settings, notificationsEnabled: false });
-      setInfoMessage("Permission refusée : notifications désactivées.");
+    // OFF -> ON : on demande la permission
+    if (!settings.notificationsEnabled) {
+      const granted = await requestNotificationPermission();
+      if (!granted) {
+        await persist(
+          { ...settings, notificationsEnabled: false },
+          "Permission refusée : notifications désactivées."
+        );
+        return;
+      }
+
+      await persist(
+        { ...settings, notificationsEnabled: true },
+        "Notifications activées ✅"
+      );
       return;
     }
 
-    setSettings({ ...settings, notificationsEnabled: true });
-    setInfoMessage("Notifications activées ✅ (pense à sauvegarder).");
+    // ON -> OFF
+    await persist(
+      { ...settings, notificationsEnabled: false },
+      "Notifications désactivées ✅"
+    );
   };
 
+  /**
+   * Bouton "Sauvegarder" : garde-fou
+   * (utile si on ajoute d'autres réglages plus tard)
+   */
   const handleSave = async () => {
-    await saveSettings(settings);
-    setInfoMessage("Paramètres sauvegardés ✅");
+    setInfoMessage(null);
+    await persist(settings, "Paramètres sauvegardés ✅");
   };
 
   const handleReplayTutorial = () => {
@@ -103,7 +146,11 @@ export default function SettingsScreen({
           <Text style={ui.title}>Devise</Text>
           <Text style={ui.value}>Actuelle : {settings.currency}</Text>
 
-          <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleCurrency}>
+          <TouchableOpacity
+            style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
+            onPress={toggleCurrency}
+            disabled={saving}
+          >
             <Text style={ui.buttonText}>Changer devise</Text>
           </TouchableOpacity>
         </View>
@@ -113,7 +160,11 @@ export default function SettingsScreen({
           <Text style={ui.title}>Rafraîchissement</Text>
           <Text style={ui.value}>Mode : {settings.refreshMode}</Text>
 
-          <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleRefreshMode}>
+          <TouchableOpacity
+            style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
+            onPress={toggleRefreshMode}
+            disabled={saving}
+          >
             <Text style={ui.buttonText}>Changer mode</Text>
           </TouchableOpacity>
         </View>
@@ -123,7 +174,11 @@ export default function SettingsScreen({
           <Text style={ui.title}>Notifications</Text>
           <Text style={ui.value}>Statut : {settings.notificationsEnabled ? "ON" : "OFF"}</Text>
 
-          <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleNotifications}>
+          <TouchableOpacity
+            style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
+            onPress={toggleNotifications}
+            disabled={saving}
+          >
             <Text style={ui.buttonText}>
               {settings.notificationsEnabled ? "Désactiver" : "Activer"} les notifications
             </Text>
@@ -144,9 +199,13 @@ export default function SettingsScreen({
           </TouchableOpacity>
         </View>
 
-        {/* Bouton Save */}
-        <TouchableOpacity style={[ui.button, styles.fullButton, styles.saveButton]} onPress={handleSave}>
-          <Text style={ui.buttonText}>Sauvegarder</Text>
+        {/* Bouton Save (garde-fou) */}
+        <TouchableOpacity
+          style={[ui.button, styles.fullButton, styles.saveButton, saving && styles.btnDisabled]}
+          onPress={handleSave}
+          disabled={saving}
+        >
+          <Text style={ui.buttonText}>{saving ? "Sauvegarde…" : "Sauvegarder"}</Text>
         </TouchableOpacity>
       </View>
     </SafeAreaView>
@@ -188,4 +247,7 @@ const styles = StyleSheet.create({
     color: "#0f172a",
     opacity: 0.85,
   },
+  btnDisabled: {
+    opacity: 0.7,
+  },
 });
\ No newline at end of file
index f3ebfdcb331145064af01bae6cbf2630bdd80e25..17e0af904074433676c23eae2480962c89ce3083 100644 (file)
@@ -3,21 +3,19 @@ import { useEffect, useState } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
 
 import { ui } from "../components/ui/uiStyles";
-import { loadSettings, saveSettings } from "../utils/settingsStorage";
+import { loadSettings } from "../utils/settingsStorage";
 
 import type { UserSettings } from "../models/UserSettings";
 import type { StrategyKey, StrategyOption } from "../types/Strategy";
 import { fetchStrategies } from "../services/strategyService";
 
 /**
- * StrategyScreen
- * --------------
- * L'utilisateur sélectionne une stratégie.
- * La stratégie choisie est persistée dans UserSettings (AsyncStorage).
- *
- * Plus tard : on pourra aussi appeler POST /api/strategy/select,
- * sans changer l'écran.
+ * ✅ API-ready (façade)
+ * Aujourd'hui : update local settings
+ * Demain : POST /api/strategy/select
  */
+import { selectStrategy } from "../services/api/strategyApi";
+
 export default function StrategyScreen() {
   const [settings, setSettings] = useState<UserSettings | null>(null);
   const [strategies, setStrategies] = useState<StrategyOption[]>([]);
@@ -58,12 +56,10 @@ export default function StrategyScreen() {
   const isSelected = (key: StrategyKey) => settings.selectedStrategyKey === key;
 
   const handleSelect = async (key: StrategyKey) => {
-    const updated: UserSettings = { ...settings, selectedStrategyKey: key };
-    setSettings(updated);
-
-    // Sauvegarde immédiate (simple et défendable)
-    await saveSettings(updated);
+    // ✅ Façade : aujourd'hui local, demain API
+    const updated = await selectStrategy(key);
 
+    setSettings(updated);
     setInfo(`Stratégie sélectionnée : ${key}`);
   };
 
@@ -97,7 +93,11 @@ export default function StrategyScreen() {
             <Text style={[ui.muted, { marginTop: 8 }]}>{item.description}</Text>
 
             <TouchableOpacity
-              style={[ui.button, styles.fullButton, isSelected(item.key) && styles.btnSelected]}
+              style={[
+                ui.button,
+                styles.fullButton,
+                isSelected(item.key) && styles.btnSelected,
+              ]}
               onPress={() => handleSelect(item.key)}
             >
               <Text style={ui.buttonText}>
index 0368f014c47e9255afed9e6304c68acc0d030e28..9ade6c1ba1d4608501a2ec59c0c6f4a4a82aa3d0 100644 (file)
@@ -13,26 +13,36 @@ import { useFocusEffect } from "@react-navigation/native";
 
 import { ui } from "../components/ui/uiStyles";
 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 (Step 3)
- * ---------------------
- * Mono-user / multi-cryptos
- * - liste d'assets (BTC, ETH, SOL...)
+ * ✅ API-only (wallet)
+ */
+import { getPortfolio, upsertPortfolio, resetPortfolio } from "../services/api/walletApi";
+
+/**
+ * ✅ API-only (price)
+ */
+import { getCurrentPrice } from "../services/api/priceApi";
+
+/**
+ * WalletScreen (Step 3/4)
+ * -----------------------
+ * Multi-cryptos
  * - quantité par asset
- * - valeur globale du portefeuille
+ * - valeur globale
  *
- * Aujourd'hui : prix mock
- * Demain : prix via GET /api/price/current?pair=XXX/EUR
+ * Prix : API-only (pas de mock)
  */
 export default function WalletScreen() {
   const [portfolio, setPortfolio] = useState<PortfolioState | null>(null);
   const [settings, setSettings] = useState<UserSettings | null>(null);
 
+  // Prix chargés depuis l'API (par symbole)
+  const [prices, setPrices] = useState<Record<string, number>>({});
+  const [pricesError, setPricesError] = useState<string | null>(null);
+
   // Ajout asset
   const [symbolInput, setSymbolInput] = useState<string>("BTC");
   const [qtyInput, setQtyInput] = useState<string>("0");
@@ -45,14 +55,36 @@ export default function WalletScreen() {
 
       async function init() {
         setInfo(null);
-        const [p, s] = await Promise.all([loadPortfolio(), loadSettings()]);
+        setPricesError(null);
+
+        const [p, s] = await Promise.all([getPortfolio(), loadSettings()]);
         if (!active) return;
 
         setPortfolio(p);
         setSettings(s);
+
+        // Charger les prix pour les assets actuels
+        try {
+          const entries = await Promise.all(
+            p.assets.map(async (a) => {
+              const pair = `${a.symbol}/${s.currency}`;
+              const cur = await getCurrentPrice(pair);
+              return [a.symbol, cur.price] as const;
+            })
+          );
+
+          if (!active) return;
+
+          const map: Record<string, number> = {};
+          for (const [sym, pr] of entries) map[sym] = pr;
+          setPrices(map);
+        } catch (e: any) {
+          if (!active) return;
+          setPricesError(e?.message ?? "Impossible de charger les prix (API).");
+        }
       }
 
-      init();
+      void init();
 
       return () => {
         active = false;
@@ -79,14 +111,14 @@ export default function WalletScreen() {
     if (!portfolio) return 0;
 
     return portfolio.assets.reduce((sum, a) => {
-      const price = getMockPrice(a.symbol);
-      if (price === null) return sum;
+      const price = prices[a.symbol];
+      if (typeof price !== "number") return sum; // prix absent => on ignore
       return sum + a.quantity * price;
     }, 0);
-  }, [portfolio]);
+  }, [portfolio, prices]);
 
   const handleAddOrUpdate = async () => {
-    if (!portfolio) return;
+    if (!portfolio || !settings) return;
 
     setInfo(null);
 
@@ -100,9 +132,17 @@ export default function WalletScreen() {
       return;
     }
 
-    const price = getMockPrice(normalizedSymbol);
-    if (price === null) {
-      setInfo("Prix inconnu (mock). Essayez: BTC, ETH, SOL, ADA.");
+    // ✅ API-only : on vérifie que la paire est récupérable
+    try {
+      const pair = `${normalizedSymbol}/${settings.currency}`;
+      const cur = await getCurrentPrice(pair);
+
+      // on mémorise le prix reçu (utile pour affichage)
+      setPrices((prev) => ({ ...prev, [normalizedSymbol]: cur.price }));
+      setPricesError(null);
+    } catch (e: any) {
+      setInfo(`Prix indisponible (API) pour ${normalizedSymbol}/${settings.currency}.`);
+      setPricesError(e?.message ?? "Erreur API prix.");
       return;
     }
 
@@ -110,12 +150,10 @@ export default function WalletScreen() {
 
     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 }];
     }
 
@@ -124,7 +162,7 @@ export default function WalletScreen() {
       updatedAtMs: Date.now(),
     };
 
-    await savePortfolio(updated);
+    await upsertPortfolio(updated);
     setPortfolio(updated);
 
     setInfo(existingIndex >= 0 ? "Asset mis à jour ✅" : "Asset ajouté ✅");
@@ -135,7 +173,7 @@ export default function WalletScreen() {
 
     RNAlert.alert(
       `Supprimer ${symbol} ?`,
-      "Cette action retire l’asset du portefeuille local.",
+      "Cette action retire l’asset du portefeuille.",
       [
         { text: "Annuler", style: "cancel" },
         {
@@ -146,8 +184,17 @@ export default function WalletScreen() {
               assets: portfolio.assets.filter((a) => a.symbol !== symbol),
               updatedAtMs: Date.now(),
             };
-            await savePortfolio(updated);
+
+            await upsertPortfolio(updated);
             setPortfolio(updated);
+
+            // nettoyage prix local
+            setPrices((prev) => {
+              const next = { ...prev };
+              delete next[symbol];
+              return next;
+            });
+
             setInfo(`${symbol} supprimé ✅`);
           },
         },
@@ -158,16 +205,16 @@ export default function WalletScreen() {
   const handleClear = () => {
     RNAlert.alert(
       "Réinitialiser le portefeuille ?",
-      "Cela supprime tous les assets du stockage local.",
+      "Cela supprime tous les assets du portefeuille.",
       [
         { text: "Annuler", style: "cancel" },
         {
           text: "Réinitialiser",
           style: "destructive",
           onPress: async () => {
-            await clearPortfolio();
-            const fresh = await loadPortfolio();
+            const fresh = await resetPortfolio();
             setPortfolio(fresh);
+            setPrices({});
             setInfo("Portefeuille réinitialisé ✅");
           },
         },
@@ -205,10 +252,15 @@ export default function WalletScreen() {
               </View>
 
               <Text style={[ui.muted, { marginTop: 6 }]}>
-                Dernière mise à jour :{" "}
-                <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
+                Dernière mise à jour : <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
               </Text>
 
+              {!!pricesError && (
+                <Text style={[ui.muted, { marginTop: 6 }]}>
+                  ⚠️ {pricesError}
+                </Text>
+              )}
+
               <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
                 <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
               </TouchableOpacity>
@@ -218,7 +270,7 @@ export default function WalletScreen() {
             <View style={ui.card}>
               <Text style={ui.title}>Ajouter / Modifier un asset</Text>
 
-              <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL, ADA)</Text>
+              <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL)</Text>
               <TextInput
                 value={symbolInput}
                 onChangeText={setSymbolInput}
@@ -241,14 +293,9 @@ export default function WalletScreen() {
               </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 style={[ui.muted, { marginBottom: 10 }]}>Liste des assets :</Text>
           </View>
         }
         ListEmptyComponent={
@@ -258,8 +305,8 @@ export default function WalletScreen() {
           </View>
         }
         renderItem={({ item }) => {
-          const price = getMockPrice(item.symbol);
-          const value = price !== null ? item.quantity * price : null;
+          const price = prices[item.symbol];
+          const value = typeof price === "number" ? item.quantity * price : null;
 
           return (
             <View style={ui.card}>
@@ -276,9 +323,9 @@ export default function WalletScreen() {
               </View>
 
               <View style={[ui.rowBetween, { marginTop: 6 }]}>
-                <Text style={ui.value}>Prix (mock)</Text>
+                <Text style={ui.value}>Prix</Text>
                 <Text style={ui.valueBold}>
-                  {price !== null ? `${price.toFixed(2)} ${settings.currency}` : "—"}
+                  {typeof price === "number" ? `${price.toFixed(2)} ${settings.currency}` : "—"}
                 </Text>
               </View>
 
index 3c9e7d20067f8863502bd6773c2e5b2eaae66f5b..0e51f280b6dacec3dd2cdf5fa533c94dc1d563b1 100644 (file)
@@ -1,21 +1,30 @@
 import type { Alert } from "../../types/Alert";
+import { apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
 import { alertStore } from "../alertStore";
 
 /**
- * alertsApi
- * ---------
- * Contrat futur (gateway):
- * - GET /api/alerts/events?userId=...&limit=10
- * - POST /api/alerts
- * - POST /api/alerts/:id/toggle
- *
- * Pour l'instant : on lit ce qu'on a en local (alertStore alimenté par Socket).
+ * alertsApi (API-first)
+ * --------------------
+ * - Lecture events serveur (optionnel) : GET /api/alerts/events
+ * - Clear : local (car pas d'endpoint "clear" dans le contrat)
  */
-export async function getRecentAlerts(limit = 10): Promise<Alert[]> {
-  const all = alertStore.getAll?.() ?? [];
-  return all.slice(0, limit);
+export async function getAlertEvents(limit = 10): Promise<Alert[]> {
+  const session = await loadSession();
+  const userId = session?.userId;
+  if (!userId) throw new Error("Session absente : impossible de charger les alertes.");
+
+  const data = await apiGet<{ events: Alert[] }>(
+    `/alerts/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
+  );
+
+  return data.events ?? [];
 }
 
-export async function clearLocalAlerts(): Promise<void> {
+/**
+ * Clear local : vide la liste affichée (alertStore).
+ * (Le contrat API ne prévoit pas de suppression globale côté serveur.)
+ */
+export async function clearAlertsLocal(): Promise<void> {
   alertStore.clear?.();
 }
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/dashboardApi.ts b/Wallette/mobile/src/services/api/dashboardApi.ts
new file mode 100644 (file)
index 0000000..6edc2f2
--- /dev/null
@@ -0,0 +1,47 @@
+import type { DashboardSummary } from "../../types/DashboardSummary";
+import type { Alert } from "../../types/Alert";
+import { apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
+import { loadSettings } from "../../utils/settingsStorage";
+
+/**
+ * dashboardApi (API-only)
+ * -----------------------
+ * Construit un DashboardSummary en appelant les endpoints du contrat.
+ * Aucun fallback mock.
+ */
+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.");
+
+  const settings = await loadSettings();
+  const pair = "BTC/EUR"; // TODO: quand tu gères multi-paires sur dashboard, tu changes ici.
+
+  // 1) Prix courant
+  const priceData = await apiGet<{
+    pair: string;
+    timestamp_ms: number;
+    current_price: number;
+  }>(`/price/current?pair=${encodeURIComponent(pair)}`);
+
+  // 2) Signal courant
+  const signalData = await apiGet<{
+    action: Alert["action"];              // BUY/SELL/HOLD/STOP_LOSS
+    criticality: Alert["alertLevel"];     // CRITICAL/WARNING/INFO
+    confidence: number;                   // 0..1
+    reason: string;
+    timestamp_ms: number;
+  }>(`/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`);
+
+  return {
+    pair,
+    price: Number(priceData.current_price),
+    strategy: settings.selectedStrategyKey, // affichage “mobile” (en attendant un champ API)
+    decision: signalData.action,
+    alertLevel: signalData.criticality,
+    confidence: Number(signalData.confidence),
+    reason: String(signalData.reason),
+    timestamp: Number(signalData.timestamp_ms),
+  };
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/http.ts b/Wallette/mobile/src/services/api/http.ts
new file mode 100644 (file)
index 0000000..8956e3b
--- /dev/null
@@ -0,0 +1,43 @@
+import { API_BASE_URL } from "../../config/env";
+
+/**
+ * Petit helper HTTP (fetch) :
+ * - force JSON
+ * - vérifie le format { ok: true, data } / { ok:false, error }
+ * - jette une erreur claire si l’API répond mal ou si HTTP != 2xx
+ */
+export async function apiGet<T>(path: string): Promise<T> {
+  const res = await fetch(`${API_BASE_URL}${path}`, { method: "GET" });
+
+  const json = await res.json().catch(() => null);
+
+  if (!res.ok) {
+    const msg = json?.error?.message ?? `HTTP ${res.status}`;
+    throw new Error(msg);
+  }
+  if (!json || json.ok !== true) {
+    const msg = json?.error?.message ?? "Réponse API invalide (ok=false)";
+    throw new Error(msg);
+  }
+  return json.data as T;
+}
+
+export async function apiPost<T>(path: string, body: unknown): Promise<T> {
+  const res = await fetch(`${API_BASE_URL}${path}`, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify(body),
+  });
+
+  const json = await res.json().catch(() => null);
+
+  if (!res.ok) {
+    const msg = json?.error?.message ?? `HTTP ${res.status}`;
+    throw new Error(msg);
+  }
+  if (!json || json.ok !== true) {
+    const msg = json?.error?.message ?? "Réponse API invalide (ok=false)";
+    throw new Error(msg);
+  }
+  return json.data as T;
+}
\ No newline at end of file
index 224660d3d3ed51e271cb197c38e98ea21672b9e0..30eb15754f451e6e198cac59fb69c8012cc49a9b 100644 (file)
@@ -1,15 +1,31 @@
-import type { DashboardSummary } from "../../types/DashboardSummary";
-import { fetchDashboardSummary } from "../dashboardService";
+import { apiGet } from "./http";
 
 /**
- * priceApi
- * --------
- * Contrat futur (gateway):
+ * priceApi (API-only)
+ * -------------------
+ * Contrat :
  * - GET /api/price/current?pair=BTC/EUR
  *
- * Pour l'instant : on renvoie la donnée mock déjà utilisée par le Dashboard.
+ * Retour attendu (à ajuster si le backend diffère) :
+ * { ok:true, data:{ pair, timestamp_ms, current_price } }
  */
-export async function getCurrentPriceForDashboard(): Promise<Pick<DashboardSummary, "pair" | "price" | "timestamp">> {
-  const d = await fetchDashboardSummary();
-  return { pair: d.pair, price: d.price, timestamp: d.timestamp };
+
+export type PriceCurrent = {
+  pair: string;
+  timestampMs: number;
+  price: number;
+};
+
+export async function getCurrentPrice(pair: string): Promise<PriceCurrent> {
+  const data = await apiGet<{
+    pair: string;
+    timestamp_ms: number;
+    current_price: number;
+  }>(`/price/current?pair=${encodeURIComponent(pair)}`);
+
+  return {
+    pair: data.pair,
+    timestampMs: Number(data.timestamp_ms),
+    price: Number(data.current_price),
+  };
 }
\ No newline at end of file
index 8a2d0756f8820835d0a527367900eb2209950fe9..404429a4bb39edb2187ff1b2b0ca37eafeba2380 100644 (file)
@@ -1,15 +1,22 @@
 import type { Signal } from "../../types/Signal";
-import { fetchRecentSignals } from "../signalService";
+import { apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
 
 /**
- * signalApi
- * ---------
- * Contrat futur (gateway):
- * - GET /api/signal/current?userId=...&pair=BTC/EUR
- * - (option) GET /api/signal/recent?userId=...&limit=20
- *
- * Pour l'instant : on utilise les mocks existants.
+ * signalApi (API-only)
+ * --------------------
+ * NOTE: endpoint à confirmer/aligner avec le chef.
+ * Proposition :
+ * - GET /api/signal/recent?userId=...&limit=20
  */
 export async function getRecentSignals(limit = 20): Promise<Signal[]> {
-  return await fetchRecentSignals(limit);
+  const session = await loadSession();
+  const userId = session?.userId;
+  if (!userId) throw new Error("Session absente : impossible de charger l'historique.");
+
+  const data = await apiGet<{ signals: Signal[] }>(
+    `/signal/recent?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
+  );
+
+  return data.signals ?? [];
 }
\ No newline at end of file
index 5f5b366b31f524554109aa92468211fd79f508d4..dccd3ff993d71710349b24b5949053643bda5fc6 100644 (file)
@@ -1,17 +1,24 @@
-import { loadSettings, saveSettings } from "../../utils/settingsStorage";
+import { apiPost } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
 import type { UserSettings } from "../../models/UserSettings";
 
 /**
- * strategyApi
- * -----------
- * Contrat futur (gateway):
- * - POST /api/strategy/select   { userId, pair, strategyKey, params... }
- *
- * Pour l'instant : on sauvegarde localement dans settingsStorage.
+ * strategyApi (API-only)
+ * ----------------------
+ * POST /api/strategy/select
  */
 export async function selectStrategy(strategyKey: string): Promise<UserSettings> {
-  const s = await loadSettings();
-  const next: UserSettings = { ...s, selectedStrategyKey: strategyKey };
-  await saveSettings(next);
-  return next;
+  const session = await loadSession();
+  const userId = session?.userId;
+  if (!userId) throw new Error("Session absente : impossible de sélectionner une stratégie.");
+
+  // Le backend décidera quoi renvoyer : settings mis à jour ou juste ok.
+  // On part sur "settings" pour éviter de recharger partout.
+  const data = await apiPost<{ settings: UserSettings }>(`/strategy/select`, {
+    userId,
+    strategyKey,
+  });
+
+  if (!data.settings) throw new Error("Réponse API invalide : settings manquant.");
+  return data.settings;
 }
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/walletApi.ts b/Wallette/mobile/src/services/api/walletApi.ts
new file mode 100644 (file)
index 0000000..b6986bc
--- /dev/null
@@ -0,0 +1,52 @@
+import { apiPost, apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
+import type { PortfolioState } from "../../models/Portfolio";
+
+/**
+ * walletApi (API-only)
+ * --------------------
+ * - GET /api/wallets?userId=...   (à confirmer)
+ * - POST /api/wallets/upsert
+ *
+ * NOTE: reset = upsert d'un portefeuille vide (pas besoin d'un endpoint dédié)
+ */
+
+function nowMs() {
+  return Date.now();
+}
+
+export async function getPortfolio(): Promise<PortfolioState> {
+  const session = await loadSession();
+  const userId = session?.userId;
+  if (!userId) throw new Error("Session absente : impossible de charger le portefeuille.");
+
+  const data = await apiGet<{ portfolio: PortfolioState }>(
+    `/wallets?userId=${encodeURIComponent(userId)}`
+  );
+
+  if (!data.portfolio) throw new Error("Réponse API invalide : portfolio manquant.");
+  return data.portfolio;
+}
+
+export async function upsertPortfolio(next: PortfolioState): Promise<PortfolioState> {
+  const session = await loadSession();
+  const userId = session?.userId;
+  if (!userId) throw new Error("Session absente : impossible de sauvegarder le portefeuille.");
+
+  const data = await apiPost<{ portfolio: PortfolioState }>(`/wallets/upsert`, {
+    userId,
+    portfolio: next,
+  });
+
+  if (!data.portfolio) throw new Error("Réponse API invalide : portfolio manquant.");
+  return data.portfolio;
+}
+
+/**
+ * Reset portefeuille (API-only)
+ * => on sauvegarde un portefeuille vide via /wallets/upsert
+ */
+export async function resetPortfolio(): Promise<PortfolioState> {
+  const empty: PortfolioState = { assets: [], updatedAtMs: nowMs() };
+  return await upsertPortfolio(empty);
+}
\ No newline at end of file