]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Modification API (demander par steph)
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Sat, 28 Feb 2026 18:37:32 +0000 (19:37 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Sat, 28 Feb 2026 18:37:32 +0000 (19:37 +0100)
Wallette/mobile/.expo/README.md [new file with mode: 0644]
Wallette/mobile/.expo/devices.json [new file with mode: 0644]
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/services/api/alertsApi.ts
Wallette/mobile/src/services/api/dashboardApi.ts
Wallette/mobile/src/services/api/priceApi.ts
Wallette/mobile/src/services/api/walletApi.ts

diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md
new file mode 100644 (file)
index 0000000..ce8c4b6
--- /dev/null
@@ -0,0 +1,13 @@
+> Why do I have a folder named ".expo" in my project?
+
+The ".expo" folder is created when an Expo project is started using "expo start" command.
+
+> What do the files contain?
+
+- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
+- "settings.json": contains the server configuration that is used to serve the application manifest.
+
+> Should I commit the ".expo" folder?
+
+No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
+Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
diff --git a/Wallette/mobile/.expo/devices.json b/Wallette/mobile/.expo/devices.json
new file mode 100644 (file)
index 0000000..5efff6c
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "devices": []
+}
index 6c8c0b06a3fdf2f5e12843ce859ca6256f2f888f..6dde816b63693620b9b7c8f02bc84404b9d870b8 100644 (file)
@@ -16,36 +16,26 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
 import { SafeAreaView } from "react-native-safe-area-context";
 import { useFocusEffect, useNavigation } from "@react-navigation/native";
 
+import { ui } from "../components/ui/uiStyles";
+
 import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
 import type { UserSettings } from "../models/UserSettings";
+import type { DashboardSummary } from "../types/DashboardSummary";
 import type { Alert as AlertType } from "../types/Alert";
 
 import { loadSession } from "../utils/sessionStorage";
 import { loadSettings } from "../utils/settingsStorage";
 import { loadPortfolio, savePortfolio } from "../utils/portfolioStorage";
 
-import { SERVER_URL } from "../config/env";
+import { SERVER_URL, API_BASE_URL, ENV_MODE } from "../config/env";
 import { socketService } from "../services/socketService";
 import { alertStore } from "../services/alertStore";
 import { showAlertNotification } from "../services/notificationService";
 
-import { getCurrentPrice } from "../services/api/priceApi";
+import { getDashboardSummary } from "../services/api/dashboardApi";
 import { getAlertHistory } from "../services/api/alertsApi";
 import { getPortfolio as getPortfolioFromApi } from "../services/api/walletApi";
 
-/**
- * DashboardScreen (aligné Web)
- * ---------------------------
- * Web consomme :
- * - GET /api/prices/current?pair=BTC/EUR  -> { price }
- * - GET /api/wallet/:userId              -> { balance }
- * - GET /api/alerts/history?userId=...   -> Alert[]
- *
- * Mobile :
- * - UI identique dans l’esprit (crypto + adresse + buy/sell)
- * - "Acheter/Vendre" = simulation (local) -> pas de trading réel
- */
-
 type CryptoSymbol = "BTC" | "ETH" | "LTC";
 type TradeSide = "BUY" | "SELL";
 const CRYPTOS: CryptoSymbol[] = ["BTC", "ETH", "LTC"];
@@ -59,9 +49,7 @@ function normalizeSymbol(s: string) {
 }
 
 function mergePortfolios(base: PortfolioState, patch: PortfolioState): PortfolioState {
-  // patch écrase base sur les symboles présents dans patch
   const map = new Map<string, number>();
-
   for (const a of base.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
   for (const a of patch.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
 
@@ -76,34 +64,31 @@ function mergePortfolios(base: PortfolioState, patch: PortfolioState): Portfolio
 export default function DashboardScreen() {
   const navigation = useNavigation();
 
-  // session / settings
+  const [loading, setLoading] = useState(true);
+  const [softError, setSoftError] = useState<string | null>(null);
+
   const [userId, setUserId] = useState<string | null>(null);
   const [settings, setSettings] = useState<UserSettings | null>(null);
+  const [portfolio, setPortfolio] = useState<PortfolioState>({ assets: [], updatedAtMs: Date.now() });
 
-  // dashboard data
   const [selectedCrypto, setSelectedCrypto] = useState<CryptoSymbol>("BTC");
-  const [currentPrice, setCurrentPrice] = useState<number | null>(null);
-  const [lastPriceUpdateMs, setLastPriceUpdateMs] = useState<number | null>(null);
 
-  const [portfolio, setPortfolio] = useState<PortfolioState>({ assets: [], updatedAtMs: Date.now() });
+  // dashboard summary = signal + prix
+  const [summary, setSummary] = useState<DashboardSummary | null>(null);
 
   // wallet address
-  const [walletAddress, setWalletAddress] = useState<string>("");
+  const [walletAddress, setWalletAddress] = useState("");
   const [walletAddressInfo, setWalletAddressInfo] = useState<string | null>(null);
 
-  // alerts
+  // alerts (live)
   const [liveAlerts, setLiveAlerts] = useState<AlertType[]>([]);
-  const [socketConnected, setSocketConnected] = useState<boolean>(false);
+  const [socketConnected, setSocketConnected] = useState(false);
   const [socketInfo, setSocketInfo] = useState<string | null>(null);
 
-  // loading / errors
-  const [loading, setLoading] = useState<boolean>(true);
-  const [softError, setSoftError] = useState<string | null>(null);
-
   // modal trade
-  const [tradeOpen, setTradeOpen] = useState<boolean>(false);
+  const [tradeOpen, setTradeOpen] = useState(false);
   const [tradeSide, setTradeSide] = useState<TradeSide>("BUY");
-  const [tradeQty, setTradeQty] = useState<string>("0.01");
+  const [tradeQty, setTradeQty] = useState("0.01");
   const [tradeInfo, setTradeInfo] = useState<string | null>(null);
 
   const currency: "EUR" | "USD" = settings?.currency === "USD" ? "USD" : "EUR";
@@ -116,25 +101,13 @@ export default function DashboardScreen() {
 
   const urgentAlert = useMemo(() => {
     if (liveAlerts.length === 0) return null;
-
     const critical = liveAlerts.find((a) => String(a.alertLevel).toUpperCase() === "CRITICAL");
     if (critical) return critical;
-
     const warning = liveAlerts.find((a) => String(a.alertLevel).toUpperCase() === "WARNING");
     if (warning) return warning;
-
     return liveAlerts[0];
   }, [liveAlerts]);
 
-  const fetchPrice = useCallback(
-    async (p: string) => {
-      const res = await getCurrentPrice(p);
-      setCurrentPrice(res.price);
-      setLastPriceUpdateMs(res.timestampMs ?? Date.now());
-    },
-    []
-  );
-
   const refreshAll = useCallback(async () => {
     setSoftError(null);
 
@@ -145,11 +118,11 @@ export default function DashboardScreen() {
     const s = await loadSettings();
     setSettings(s);
 
-    // Local portfolio (fallback + base)
+    // local portfolio
     const localPortfolio = await loadPortfolio();
     setPortfolio(localPortfolio);
 
-    // Adresse wallet (local, par userId)
+    // wallet address
     if (uid) {
       const addr = (await AsyncStorage.getItem(walletAddressKey(uid))) ?? "";
       setWalletAddress(addr);
@@ -157,14 +130,18 @@ export default function DashboardScreen() {
       setWalletAddress("");
     }
 
-    // Prix (API)
+    // summary (signal+prix) -> API
     try {
-      await fetchPrice(`${selectedCrypto}/${s.currency === "USD" ? "USD" : "EUR"}`);
+      const dash = await getDashboardSummary();
+      setSummary(dash);
     } catch {
-      setSoftError("Prix indisponible pour le moment (API).");
+      setSummary(null);
+      setSoftError(
+        `Signal/Prix indisponibles (API). Vérifie si tu es en DEV: ${ENV_MODE}.`
+      );
     }
 
-    // Wallet (API) -> fusion pour ne pas écraser ETH/LTC locaux
+    // wallet API (si dispo) -> fusion
     if (uid) {
       try {
         const apiPortfolio = await getPortfolioFromApi();
@@ -173,43 +150,39 @@ export default function DashboardScreen() {
         await savePortfolio(merged);
       } catch {
         // pas bloquant
-        setSoftError((prev) => prev ?? "Wallet indisponible pour le moment (API).");
       }
     }
 
-    // Alert history (API) + socket live ensuite
+    // alert history REST (si dispo)
     if (uid) {
       try {
-        const history = await getAlertHistory();
-        // On met en store + state
+        const history = await getAlertHistory(10);
         for (const a of history) alertStore.add(a);
         setLiveAlerts(history.slice(0, 50));
       } catch {
         // pas bloquant
       }
     }
-  }, [fetchPrice, selectedCrypto]);
+  }, []);
 
   useFocusEffect(
     useCallback(() => {
-      let active = true;
-
+      let alive = true;
       (async () => {
         try {
           setLoading(true);
           await refreshAll();
         } finally {
-          if (active) setLoading(false);
+          if (alive) setLoading(false);
         }
       })();
-
       return () => {
-        active = false;
+        alive = false;
       };
     }, [refreshAll])
   );
 
-  // Socket : alert live (non bloquant)
+  // Socket live (non bloquant)
   useEffect(() => {
     let unsub: null | (() => void) = null;
     let alive = true;
@@ -232,7 +205,7 @@ export default function DashboardScreen() {
         setSocketConnected(true);
       } catch {
         if (!alive) return;
-        setSocketInfo("Socket indisponible (URL ou serveur).");
+        setSocketInfo("Socket indisponible (URL / serveur).");
         return;
       }
 
@@ -240,7 +213,6 @@ export default function DashboardScreen() {
         alertStore.add(a);
         setLiveAlerts((prev) => [a, ...prev].slice(0, 100));
 
-        // notifications si activées
         if (settings?.notificationsEnabled) {
           void (async () => {
             try {
@@ -252,7 +224,6 @@ export default function DashboardScreen() {
         }
       });
 
-      // Optionnel
       socketService.ping();
     })();
 
@@ -324,27 +295,16 @@ export default function DashboardScreen() {
       updatedAtMs: Date.now(),
     };
 
-    // Aujourd’hui : local (simulation)
     await savePortfolio(nextPortfolio);
     setPortfolio(nextPortfolio);
-
     setTradeOpen(false);
 
     RNAlert.alert("Transaction enregistrée ✅", `${tradeSide} ${qty} ${symbol}`);
   }, [tradeQty, tradeSide, selectedCrypto, portfolio]);
 
-  const handleManualRefreshPrice = useCallback(async () => {
-    try {
-      setSoftError(null);
-      await fetchPrice(pair);
-    } catch {
-      setSoftError("Prix indisponible pour le moment (API).");
-    }
-  }, [fetchPrice, pair]);
-
   if (loading) {
     return (
-      <View style={styles.centered}>
+      <View style={ui.centered}>
         <ActivityIndicator />
         <Text style={{ marginTop: 10 }}>Chargement…</Text>
       </View>
@@ -353,30 +313,29 @@ export default function DashboardScreen() {
 
   return (
     <SafeAreaView style={styles.safeArea}>
-      <ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
+      <ScrollView contentContainerStyle={ui.container} showsVerticalScrollIndicator={false}>
         {!!softError && (
-          <View style={styles.banner}>
+          <View style={[ui.banner, styles.bannerWarn]}>
             <Text style={styles.bannerText}>{softError}</Text>
+            <Text style={styles.bannerSub}>
+              Base REST : {API_BASE_URL}
+            </Text>
           </View>
         )}
 
-        {/* COMPTE UTILISATEUR */}
-        <View style={styles.card}>
-          <View style={styles.rowBetween}>
-            <Text style={styles.title}>Compte utilisateur</Text>
-            <Text style={styles.muted}>{socketConnected ? "Socket: ON ✅" : "Socket: OFF ⚠️"}</Text>
+        {/* Compte */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Compte utilisateur</Text>
+          <View style={ui.rowBetween}>
+            <Text style={ui.muted}>UserId : <Text style={styles.bold}>{userId ?? "—"}</Text></Text>
+            <Text style={ui.muted}>Socket : {socketConnected ? "ON ✅" : "OFF ⚠️"}</Text>
           </View>
-
-          <Text style={styles.muted}>
-            UserId : <Text style={styles.bold}>{userId ?? "—"}</Text>
-          </Text>
-
-          {!!socketInfo && <Text style={[styles.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
+          {!!socketInfo && <Text style={[ui.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
         </View>
 
-        {/* CHOIX CRYPTO + ADRESSE */}
-        <View style={styles.card}>
-          <Text style={styles.title}>Choisir une cryptomonnaie</Text>
+        {/* Crypto + adresse */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Choisir une cryptomonnaie</Text>
 
           <View style={styles.cryptoRow}>
             {CRYPTOS.map((c) => {
@@ -385,14 +344,7 @@ export default function DashboardScreen() {
                 <TouchableOpacity
                   key={c}
                   style={[styles.cryptoBtn, active && styles.cryptoBtnActive]}
-                  onPress={async () => {
-                    setSelectedCrypto(c);
-                    try {
-                      await fetchPrice(`${c}/${currency}`);
-                    } catch {
-                      setSoftError("Prix indisponible pour le moment (API).");
-                    }
-                  }}
+                  onPress={() => setSelectedCrypto(c)}
                 >
                   <Text style={[styles.cryptoText, active && styles.cryptoTextActive]}>{c}</Text>
                 </TouchableOpacity>
@@ -400,7 +352,7 @@ export default function DashboardScreen() {
             })}
           </View>
 
-          <Text style={[styles.muted, { marginTop: 12 }]}>Adresse du portefeuille</Text>
+          <Text style={[ui.muted, { marginTop: 12 }]}>Adresse du portefeuille</Text>
           <TextInput
             value={walletAddress}
             onChangeText={(t) => {
@@ -417,75 +369,86 @@ export default function DashboardScreen() {
             <Text style={styles.secondaryButtonText}>Enregistrer l’adresse</Text>
           </TouchableOpacity>
 
-          {!!walletAddressInfo && <Text style={[styles.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
+          {!!walletAddressInfo && <Text style={[ui.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
         </View>
 
-        {/* SOLDE + PRIX (comme le Web) */}
-        <View style={styles.rowGap}>
-          <View style={[styles.card, styles.half]}>
-            <Text style={styles.title}>Solde</Text>
-            <Text style={styles.bigValue}>
-              {selectedQty.toFixed(6)} {selectedCrypto}
-            </Text>
-            <Text style={styles.muted}>Source : wallet local (+ API BTC fusionné)</Text>
-          </View>
-
-          <View style={[styles.card, styles.half]}>
-            <View style={styles.rowBetween}>
-              <Text style={styles.title}>Prix</Text>
-              <TouchableOpacity onPress={handleManualRefreshPrice} style={styles.refreshBtn}>
-                <Text style={styles.refreshText}>Actualiser</Text>
-              </TouchableOpacity>
-            </View>
-
-            <Text style={styles.bigValue}>
-              {(currentPrice ?? 0).toFixed(2)} {currency}
-            </Text>
+        {/* Solde + Prix */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Solde</Text>
+          <Text style={styles.bigValue}>
+            {selectedQty.toFixed(6)} {selectedCrypto}
+          </Text>
+          <Text style={ui.muted}>Portefeuille local (+ sync serveur si dispo)</Text>
+        </View>
 
-            <Text style={styles.muted}>
-              Pair : {pair} — Maj : {lastPriceUpdateMs ? new Date(lastPriceUpdateMs).toLocaleTimeString() : "—"}
-            </Text>
+        <View style={ui.card}>
+          <View style={ui.rowBetween}>
+            <Text style={ui.title}>Prix</Text>
+            <Text style={ui.muted}>{pair}</Text>
           </View>
+          <Text style={styles.bigValue}>
+            {(summary?.price ?? 0).toFixed(2)} {currency}
+          </Text>
+          <Text style={ui.muted}>
+            Maj : {summary?.timestamp ? new Date(summary.timestamp).toLocaleTimeString() : "—"}
+          </Text>
+
+          <TouchableOpacity style={[ui.button, { marginTop: 10 }]} onPress={() => void refreshAll()}>
+            <Text style={ui.buttonText}>Actualiser</Text>
+          </TouchableOpacity>
         </View>
 
-        {/* SIGNAL (minimal) */}
-        <TouchableOpacity style={styles.card} onPress={() => navigation.navigate("Alerts" as never)} activeOpacity={0.85}>
-          <View style={styles.rowBetween}>
-            <Text style={styles.title}>Signal du marché</Text>
-            <Text style={styles.muted}>Ouvrir alertes</Text>
-          </View>
+        {/* Signal */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Signal du marché</Text>
 
-          {urgentAlert ? (
+          {summary ? (
             <>
-              <Text style={styles.signalAction}>{String(urgentAlert.action ?? "HOLD")}</Text>
-              <Text style={styles.muted} numberOfLines={2}>
-                {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.reason ?? urgentAlert.message ?? "—")}
+              <Text style={styles.signalDecision}>{summary.decision}</Text>
+              <Text style={ui.muted}>
+                {summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}%
               </Text>
-              <Text style={styles.muted}>
-                {String(urgentAlert.pair ?? "")} — Confiance{" "}
-                {typeof urgentAlert.confidence === "number" ? `${Math.round(urgentAlert.confidence * 100)}%` : "—"}
+              <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={3}>
+                {summary.reason}
               </Text>
             </>
           ) : (
-            <Text style={styles.muted}>Aucune alerte pour le moment.</Text>
+            <Text style={ui.muted}>Aucune donnée pour le moment.</Text>
           )}
-        </TouchableOpacity>
 
-        {/* STRATÉGIE (bouton) */}
-        <TouchableOpacity style={styles.card} onPress={() => navigation.navigate("Strategy" as never)} activeOpacity={0.85}>
-          <View style={styles.rowBetween}>
-            <Text style={styles.title}>Stratégie</Text>
-            <Text style={styles.muted}>Configurer</Text>
-          </View>
+          {/* Bonus : urgence live si dispo */}
+          {urgentAlert && (
+            <View style={styles.urgentBox}>
+              <Text style={styles.urgentTitle}>
+                {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.action ?? "HOLD")}
+              </Text>
+              <Text style={ui.muted} numberOfLines={2}>
+                {urgentAlert.reason ?? urgentAlert.message ?? "—"}
+              </Text>
+            </View>
+          )}
+        </View>
 
-          <Text style={styles.muted} numberOfLines={2}>
-            (L’écran Stratégie reste inchangé — ici on ne fait que naviguer.)
-          </Text>
+        {/* Stratégie */}
+        <TouchableOpacity
+          activeOpacity={0.85}
+          onPress={() => navigation.navigate("Strategy" as never)}
+        >
+          <View style={ui.card}>
+            <View style={ui.rowBetween}>
+              <Text style={ui.title}>Stratégie</Text>
+              <Text style={ui.muted}>Configurer</Text>
+            </View>
+
+            <Text style={[ui.muted, { marginTop: 6 }]}>
+              Sélection : <Text style={styles.bold}>{settings?.selectedStrategyKey ?? "—"}</Text>
+            </Text>
+          </View>
         </TouchableOpacity>
 
-        {/* ACTIONS (Acheter / Vendre) */}
-        <View style={styles.card}>
-          <Text style={styles.title}>Actions</Text>
+        {/* Actions */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Actions</Text>
 
           <View style={styles.buySellRow}>
             <TouchableOpacity style={styles.buyBtn} onPress={() => openTrade("BUY")}>
@@ -497,12 +460,12 @@ export default function DashboardScreen() {
             </TouchableOpacity>
           </View>
 
-          <Text style={[styles.muted, { marginTop: 10 }]} numberOfLines={2}>
+          <Text style={[ui.muted, { marginTop: 10 }]} numberOfLines={2}>
             Note : Acheter/Vendre = simulation (registre local). Pas de trading réel.
           </Text>
         </View>
 
-        {/* MODAL BUY/SELL */}
+        {/* Modal BUY/SELL */}
         <Modal visible={tradeOpen} transparent animationType="fade" onRequestClose={() => setTradeOpen(false)}>
           <View style={styles.modalBackdrop}>
             <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined} style={styles.modalWrap}>
@@ -511,11 +474,11 @@ export default function DashboardScreen() {
                   {tradeSide === "BUY" ? "Acheter" : "Vendre"} {selectedCrypto}
                 </Text>
 
-                <Text style={styles.muted}>
-                  Prix : {(currentPrice ?? 0).toFixed(2)} {currency}
+                <Text style={ui.muted}>
+                  Prix : {(summary?.price ?? 0).toFixed(2)} {currency}
                 </Text>
 
-                <Text style={[styles.muted, { marginTop: 12 }]}>Quantité</Text>
+                <Text style={[ui.muted, { marginTop: 12 }]}>Quantité</Text>
                 <TextInput
                   value={tradeQty}
                   onChangeText={(t) => {
@@ -542,51 +505,22 @@ export default function DashboardScreen() {
             </KeyboardAvoidingView>
           </View>
         </Modal>
-
-        {/* Bouton refresh global */}
-        <TouchableOpacity style={[styles.secondaryButton, { marginTop: 4 }]} onPress={() => void refreshAll()}>
-          <Text style={styles.secondaryButtonText}>Tout actualiser</Text>
-        </TouchableOpacity>
       </ScrollView>
     </SafeAreaView>
   );
 }
 
 const styles = StyleSheet.create({
-  safeArea: { flex: 1, backgroundColor: "#0b1220" },
-  container: { padding: 16, paddingBottom: 28 },
+  safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
 
-  centered: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#0b1220" },
+  bannerWarn: { borderColor: "#ca8a04" },
+  bannerText: { color: "#ca8a04", fontWeight: "900" },
+  bannerSub: { marginTop: 6, color: "#475569", fontWeight: "700" },
 
-  banner: {
-    borderWidth: 1,
-    borderColor: "#f59e0b",
-    backgroundColor: "rgba(245,158,11,0.12)",
-    padding: 12,
-    borderRadius: 12,
-    marginBottom: 12,
-  },
-  bannerText: { color: "#f59e0b", fontWeight: "800" },
-
-  card: {
-    backgroundColor: "#ffffff",
-    borderRadius: 16,
-    padding: 14,
-    marginBottom: 12,
-    borderWidth: 1,
-    borderColor: "#e5e7eb",
-  },
-
-  rowBetween: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
-  rowGap: { flexDirection: "row", gap: 12, marginBottom: 12 },
-  half: { flex: 1, marginBottom: 0 },
-
-  title: { fontSize: 16, fontWeight: "900", color: "#0f172a" },
-  muted: { marginTop: 6, color: "#475569", fontWeight: "600" },
   bold: { fontWeight: "900", color: "#0f172a" },
 
   bigValue: { marginTop: 10, fontSize: 22, fontWeight: "900", color: "#0f172a" },
-  signalAction: { marginTop: 10, fontSize: 24, fontWeight: "900", color: "#0f172a" },
+  signalDecision: { marginTop: 10, fontSize: 26, fontWeight: "900", color: "#0f172a" },
 
   cryptoRow: { flexDirection: "row", gap: 10, marginTop: 10 },
   cryptoBtn: {
@@ -624,15 +558,15 @@ const styles = StyleSheet.create({
   },
   secondaryButtonText: { fontWeight: "900", color: "#0f172a", opacity: 0.9 },
 
-  refreshBtn: {
-    paddingHorizontal: 12,
-    paddingVertical: 8,
-    borderRadius: 12,
+  urgentBox: {
+    marginTop: 12,
     borderWidth: 1,
     borderColor: "#e5e7eb",
+    borderRadius: 12,
+    padding: 10,
     backgroundColor: "#fff",
   },
-  refreshText: { fontWeight: "900", color: "#0f172a" },
+  urgentTitle: { fontWeight: "900", color: "#0f172a" },
 
   buySellRow: { flexDirection: "row", gap: 10, marginTop: 10 },
   buyBtn: { flex: 1, backgroundColor: "#16a34a", paddingVertical: 12, borderRadius: 12, alignItems: "center" },
@@ -644,7 +578,6 @@ const styles = StyleSheet.create({
   modalCard: { backgroundColor: "#fff", borderRadius: 16, padding: 16, borderWidth: 1, borderColor: "#e5e7eb" },
   modalTitle: { fontSize: 18, fontWeight: "900", color: "#0f172a" },
   modalError: { marginTop: 10, fontWeight: "800", color: "#dc2626" },
-
   modalButtonsRow: { flexDirection: "row", gap: 10, marginTop: 14 },
   modalBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: "center" },
   modalBtnSecondary: { backgroundColor: "#fff", borderWidth: 1, borderColor: "#e5e7eb" },
index b0f282be580b8e79bc45e1901153f06cd7018462..8c1cf87dd1884ba11e501f52ba14002638ad4d9a 100644 (file)
@@ -4,21 +4,35 @@ import { loadSession } from "../../utils/sessionStorage";
 import { alertStore } from "../alertStore";
 
 /**
- * alertsApi
- * ---------
- * Aligné Web :
- * - GET /api/alerts/history?userId=...
- * Réponse : Alert[]
+ * alertsApi (aligné serveur déployé)
+ * ---------------------------------
+ * Route dispo :
+ * - GET /api/alerts/events?userId=...&limit=10
+ *
+ * Réponse :
+ * {
+ *   "ok": true,
+ *   "data": { "events": [...], "count": 10, "limit": 10 }
+ * }
  */
 
-export async function getAlertHistory(): Promise<Alert[]> {
+type AlertsEventsResponse = {
+  events?: Alert[];
+  count?: number;
+  limit?: number;
+};
+
+export async function getAlertHistory(limit = 10): Promise<Alert[]> {
   const session = await loadSession();
   const userId = session?.userId;
-  if (!userId) throw new Error("Session absente : impossible de charger l'historique des alertes.");
+  if (!userId) throw new Error("Session absente : impossible de charger les alertes.");
 
-  const data = await apiGet<any>(`/alerts/history?userId=${encodeURIComponent(userId)}`);
+  const data = await apiGet<AlertsEventsResponse>(
+    `/alerts/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
+  );
 
-  return Array.isArray(data) ? (data as Alert[]) : [];
+  const events = Array.isArray(data?.events) ? data.events : [];
+  return events;
 }
 
 export async function clearAlertsLocal(): Promise<void> {
index 6edc2f2f1178261256632272949d68283b4949c2..4f4e64c84b977acbc53157098070b7c06cc5697f 100644 (file)
@@ -1,5 +1,4 @@
-import type { DashboardSummary } from "../../types/DashboardSummary";
-import type { Alert } from "../../types/Alert";
+import type { DashboardSummary, TradeDecision, AlertLevel } from "../../types/DashboardSummary";
 import { apiGet } from "./http";
 import { loadSession } from "../../utils/sessionStorage";
 import { loadSettings } from "../../utils/settingsStorage";
@@ -7,8 +6,13 @@ import { loadSettings } from "../../utils/settingsStorage";
 /**
  * dashboardApi (API-only)
  * -----------------------
- * Construit un DashboardSummary en appelant les endpoints du contrat.
- * Aucun fallback mock.
+ * Construit un DashboardSummary via les endpoints gateway.
+ *
+ * Aligné Web pour le prix :
+ * - GET /api/prices/current?pair=BTC/EUR -> { price }
+ *
+ * Signal :
+ * - GET /api/signal/current?userId=...&pair=BTC/EUR
  */
 export async function getDashboardSummary(): Promise<DashboardSummary> {
   const session = await loadSession();
@@ -16,32 +20,88 @@ export async function getDashboardSummary(): Promise<DashboardSummary> {
   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)}`);
+  const currency = settings.currency === "USD" ? "USD" : "EUR";
+  const pair = `BTC/${currency}`;
+
+  // 1) Prix courant (aligné Web)
+  const priceRaw = await apiGet<any>(`/prices/current?pair=${encodeURIComponent(pair)}`);
+
+  const price =
+    (typeof priceRaw?.price === "number" ? priceRaw.price : null) ??
+    (typeof priceRaw?.current_price === "number" ? priceRaw.current_price : null) ??
+    (typeof priceRaw?.data?.price === "number" ? priceRaw.data.price : null) ??
+    (typeof priceRaw?.data?.current_price === "number" ? priceRaw.data.current_price : null);
+
+  if (typeof price !== "number" || !Number.isFinite(price)) {
+    throw new Error("Prix invalide (API).");
+  }
+
+  const tsPrice =
+    (typeof priceRaw?.timestampMs === "number" ? priceRaw.timestampMs : null) ??
+    (typeof priceRaw?.timestamp_ms === "number" ? priceRaw.timestamp_ms : null) ??
+    Date.now();
+
+  // 2) Signal courant (API)
+  const signalRaw = await apiGet<any>(
+    `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
+  );
+
+  // action (défaut HOLD)
+  const actionStr = String(
+    signalRaw?.action ??
+      signalRaw?.data?.action ??
+      "HOLD"
+  ).toUpperCase();
+
+  const decision: TradeDecision =
+    actionStr === "BUY" || actionStr === "SELL" || actionStr === "STOP_LOSS"
+      ? (actionStr as TradeDecision)
+      : "HOLD";
+
+  // alertLevel (défaut INFO)
+  const lvlStr = String(
+    signalRaw?.alertLevel ??
+      signalRaw?.criticality ??
+      signalRaw?.data?.alertLevel ??
+      signalRaw?.data?.criticality ??
+      "INFO"
+  ).toUpperCase();
+
+  const alertLevel: AlertLevel =
+    lvlStr === "CRITICAL" || lvlStr === "WARNING"
+      ? (lvlStr as AlertLevel)
+      : "INFO";
+
+  const confidence =
+    typeof signalRaw?.confidence === "number"
+      ? signalRaw.confidence
+      : typeof signalRaw?.data?.confidence === "number"
+      ? signalRaw.data.confidence
+      : 0;
+
+  const reason = String(
+    signalRaw?.reason ??
+      signalRaw?.message ??
+      signalRaw?.data?.reason ??
+      signalRaw?.data?.message ??
+      "—"
+  );
+
+  const tsSignal =
+    (typeof signalRaw?.timestamp === "number" ? signalRaw.timestamp : null) ??
+    (typeof signalRaw?.timestamp_ms === "number" ? signalRaw.timestamp_ms : null) ??
+    (typeof signalRaw?.data?.timestamp === "number" ? signalRaw.data.timestamp : null) ??
+    (typeof signalRaw?.data?.timestamp_ms === "number" ? signalRaw.data.timestamp_ms : null) ??
+    Number(tsPrice);
 
   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),
+    price: Number(price),
+    strategy: settings.selectedStrategyKey,
+    decision,
+    confidence: Number(confidence),
+    reason,
+    alertLevel,
+    timestamp: Number(tsSignal),
   };
 }
\ No newline at end of file
index 09bba8d3fc17029cd0184153c60a21bdca94fc11..9fbf20b50dc384f4b4233bc39501a4bd9a5c0209 100644 (file)
@@ -1,44 +1,50 @@
 import { apiGet } from "./http";
 
 /**
- * priceApi
- * --------
- * Web utilise :
- * - GET /api/prices/current?pair=BTC/EUR
- * Réponse attendue côté web : { price: number, ... }
+ * priceApi (aligné serveur déployé)
+ * ---------------------------------
+ * Route dispo :
+ * - GET /api/price/current?pair=BTC/EUR
  *
- * On reste robuste :
- * - accepte aussi d'anciens formats si jamais.
+ * Réponse :
+ * {
+ *   "ok": true,
+ *   "data": {
+ *     "pair": "BTC/EUR",
+ *     "current_price": 42150.23,
+ *     "timestamp_ms": 1700000000000,
+ *     "source": "..."
+ *   }
+ * }
  */
 
 export type PriceCurrent = {
   pair: string;
   timestampMs: number;
   price: number;
+  source?: string;
 };
 
 export async function getCurrentPrice(pair: string): Promise<PriceCurrent> {
-  const data = await apiGet<any>(`/prices/current?pair=${encodeURIComponent(pair)}`);
+  const data = await apiGet<any>(`/price/current?pair=${encodeURIComponent(pair)}`);
 
-  // Format web : { price: number }
   const price =
-    (typeof data?.price === "number" ? data.price : null) ??
     (typeof data?.current_price === "number" ? data.current_price : null) ??
-    (typeof data?.data?.price === "number" ? data.data.price : null) ??
-    (typeof data?.data?.current_price === "number" ? data.data.current_price : null);
+    (typeof data?.price === "number" ? data.price : null);
 
   if (typeof price !== "number" || !Number.isFinite(price)) {
     throw new Error("Prix invalide (API).");
   }
 
   const ts =
-    (typeof data?.timestampMs === "number" ? data.timestampMs : null) ??
     (typeof data?.timestamp_ms === "number" ? data.timestamp_ms : null) ??
+    (typeof data?.timestampMs === "number" ? data.timestampMs : null) ??
     Date.now();
 
   return {
-    pair,
+    pair: String(data?.pair ?? pair),
     timestampMs: Number(ts),
     price: Number(price),
+    source: typeof data?.source === "string" ? data.source : undefined,
   };
 }
\ No newline at end of file
index 89170c11e86875e33620e736ab2fa1e497c2a562..dbb11e22b8388d3b464027ef9fb59bd1a7468808 100644 (file)
@@ -3,48 +3,99 @@ import { loadSession } from "../../utils/sessionStorage";
 import type { PortfolioState } from "../../models/Portfolio";
 
 /**
- * walletApi
- * ---------
- * Aligné Web :
- * - GET /api/wallet/:userId
- * Réponse attendue : { balance: number }
+ * walletApi (aligné serveur déployé)
+ * ---------------------------------
+ * Routes dispo :
+ * - GET    /api/wallets?userId=...
+ * - GET    /api/wallets/:walletId
+ * - GET    /api/wallets/:walletId/events
  *
- * Le web affiche le solde en BTC.
- * Côté mobile, on convertit en PortfolioState (assets).
+ * Objectif côté mobile (pour l’instant) :
+ * - Récupérer un portefeuille (assets + quantités) si possible.
+ *
+ * Si le backend ne fournit pas (encore) un format multi-assets clair,
+ * on renvoie un portefeuille vide plutôt que casser l’app.
  */
 
-type WalletResponse = {
-  balance?: number;
-  // si un jour ils passent multi-assets, on ne casse pas :
-  balances?: Record<string, number>;
+type WalletListItem = {
+  id?: string;
+  walletId?: string;
+  _id?: string;
 };
 
-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.");
+type WalletListResponse =
+  | { wallets?: WalletListItem[] }
+  | { items?: WalletListItem[] }
+  | WalletListItem[]
+  | any;
 
-  const data = await apiGet<WalletResponse>(`/wallet/${encodeURIComponent(userId)}`);
+type WalletDetailsResponse = any;
 
-  // Format actuel web : balance BTC
-  if (typeof data?.balance === "number" && Number.isFinite(data.balance)) {
-    return {
-      assets: [{ symbol: "BTC", quantity: data.balance }],
-      updatedAtMs: Date.now(),
-    };
+function pickWalletId(list: any): string | null {
+  // cas 1: data = array
+  const arr = Array.isArray(list) ? list : Array.isArray(list?.wallets) ? list.wallets : Array.isArray(list?.items) ? list.items : null;
+  if (!arr || arr.length === 0) return null;
+
+  const first = arr[0];
+  return (
+    (typeof first?.walletId === "string" && first.walletId) ||
+    (typeof first?.id === "string" && first.id) ||
+    (typeof first?._id === "string" && first._id) ||
+    null
+  );
+}
+
+function extractAssetsFromWalletDetails(details: any): { symbol: string; quantity: number }[] {
+  // On tente plusieurs formats possibles :
+  // - details.portfolio.assets
+  // - details.assets
+  // - details.balances { BTC: 0.1, ETH: 2 }
+  const assetsArr =
+    (Array.isArray(details?.portfolio?.assets) && details.portfolio.assets) ||
+    (Array.isArray(details?.assets) && details.assets) ||
+    null;
+
+  if (assetsArr) {
+    return assetsArr
+      .filter((a: any) => a && typeof a.symbol === "string" && typeof a.quantity === "number")
+      .map((a: any) => ({ symbol: String(a.symbol).toUpperCase(), quantity: Number(a.quantity) }))
+      .filter((a: any) => Number.isFinite(a.quantity) && a.quantity > 0)
+      .sort((a: any, b: any) => a.symbol.localeCompare(b.symbol));
   }
 
-  // Format futur possible : balances { BTC:..., ETH:..., LTC:... }
-  const balances = data?.balances;
+  const balances = details?.balances;
   if (balances && typeof balances === "object") {
-    const assets = Object.entries(balances)
+    return Object.entries(balances)
       .filter(([, qty]) => typeof qty === "number" && Number.isFinite(qty) && qty > 0)
-      .map(([symbol, qty]) => ({ symbol: symbol.toUpperCase(), quantity: qty }))
+      .map(([symbol, qty]) => ({ symbol: String(symbol).toUpperCase(), quantity: Number(qty) }))
       .sort((a, b) => a.symbol.localeCompare(b.symbol));
+  }
 
-    return { assets, updatedAtMs: Date.now() };
+  // fallback : rien exploitable
+  return [];
+}
+
+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.");
+
+  // 1) récupérer la liste des wallets de l’utilisateur
+  const list = await apiGet<WalletListResponse>(`/wallets?userId=${encodeURIComponent(userId)}`);
+  const walletId = pickWalletId(list);
+
+  if (!walletId) {
+    // Aucun wallet côté serveur : on renvoie vide (l’app reste stable)
+    return { assets: [], updatedAtMs: Date.now() };
   }
 
-  // fallback safe
-  return { assets: [], updatedAtMs: Date.now() };
+  // 2) récupérer le wallet détail
+  const details = await apiGet<WalletDetailsResponse>(`/wallets/${encodeURIComponent(walletId)}`);
+
+  const assets = extractAssetsFromWalletDetails(details);
+
+  return {
+    assets,
+    updatedAtMs: Date.now(),
+  };
 }
\ No newline at end of file