From: Thibaud Moustier Date: Sat, 28 Feb 2026 18:37:32 +0000 (+0100) Subject: Mobile : Modification API (demander par steph) X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=274913337a103bab270352acdaa22b913c06d8c9;p=pdw25-26 Mobile : Modification API (demander par steph) --- diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md new file mode 100644 index 0000000..ce8c4b6 --- /dev/null +++ b/Wallette/mobile/.expo/README.md @@ -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 index 0000000..5efff6c --- /dev/null +++ b/Wallette/mobile/.expo/devices.json @@ -0,0 +1,3 @@ +{ + "devices": [] +} diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index 6c8c0b0..6dde816 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -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(); - 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(null); + const [userId, setUserId] = useState(null); const [settings, setSettings] = useState(null); + const [portfolio, setPortfolio] = useState({ assets: [], updatedAtMs: Date.now() }); - // dashboard data const [selectedCrypto, setSelectedCrypto] = useState("BTC"); - const [currentPrice, setCurrentPrice] = useState(null); - const [lastPriceUpdateMs, setLastPriceUpdateMs] = useState(null); - const [portfolio, setPortfolio] = useState({ assets: [], updatedAtMs: Date.now() }); + // dashboard summary = signal + prix + const [summary, setSummary] = useState(null); // wallet address - const [walletAddress, setWalletAddress] = useState(""); + const [walletAddress, setWalletAddress] = useState(""); const [walletAddressInfo, setWalletAddressInfo] = useState(null); - // alerts + // alerts (live) const [liveAlerts, setLiveAlerts] = useState([]); - const [socketConnected, setSocketConnected] = useState(false); + const [socketConnected, setSocketConnected] = useState(false); const [socketInfo, setSocketInfo] = useState(null); - // loading / errors - const [loading, setLoading] = useState(true); - const [softError, setSoftError] = useState(null); - // modal trade - const [tradeOpen, setTradeOpen] = useState(false); + const [tradeOpen, setTradeOpen] = useState(false); const [tradeSide, setTradeSide] = useState("BUY"); - const [tradeQty, setTradeQty] = useState("0.01"); + const [tradeQty, setTradeQty] = useState("0.01"); const [tradeInfo, setTradeInfo] = useState(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 ( - + Chargement… @@ -353,30 +313,29 @@ export default function DashboardScreen() { return ( - + {!!softError && ( - + {softError} + + Base REST : {API_BASE_URL} + )} - {/* COMPTE UTILISATEUR */} - - - Compte utilisateur - {socketConnected ? "Socket: ON ✅" : "Socket: OFF ⚠️"} + {/* Compte */} + + Compte utilisateur + + UserId : {userId ?? "—"} + Socket : {socketConnected ? "ON ✅" : "OFF ⚠️"} - - - UserId : {userId ?? "—"} - - - {!!socketInfo && {socketInfo}} + {!!socketInfo && {socketInfo}} - {/* CHOIX CRYPTO + ADRESSE */} - - Choisir une cryptomonnaie + {/* Crypto + adresse */} + + Choisir une cryptomonnaie {CRYPTOS.map((c) => { @@ -385,14 +344,7 @@ export default function DashboardScreen() { { - setSelectedCrypto(c); - try { - await fetchPrice(`${c}/${currency}`); - } catch { - setSoftError("Prix indisponible pour le moment (API)."); - } - }} + onPress={() => setSelectedCrypto(c)} > {c} @@ -400,7 +352,7 @@ export default function DashboardScreen() { })} - Adresse du portefeuille + Adresse du portefeuille { @@ -417,75 +369,86 @@ export default function DashboardScreen() { Enregistrer l’adresse - {!!walletAddressInfo && {walletAddressInfo}} + {!!walletAddressInfo && {walletAddressInfo}} - {/* SOLDE + PRIX (comme le Web) */} - - - Solde - - {selectedQty.toFixed(6)} {selectedCrypto} - - Source : wallet local (+ API BTC fusionné) - - - - - Prix - - Actualiser - - - - - {(currentPrice ?? 0).toFixed(2)} {currency} - + {/* Solde + Prix */} + + Solde + + {selectedQty.toFixed(6)} {selectedCrypto} + + Portefeuille local (+ sync serveur si dispo) + - - Pair : {pair} — Maj : {lastPriceUpdateMs ? new Date(lastPriceUpdateMs).toLocaleTimeString() : "—"} - + + + Prix + {pair} + + {(summary?.price ?? 0).toFixed(2)} {currency} + + + Maj : {summary?.timestamp ? new Date(summary.timestamp).toLocaleTimeString() : "—"} + + + void refreshAll()}> + Actualiser + - {/* SIGNAL (minimal) */} - navigation.navigate("Alerts" as never)} activeOpacity={0.85}> - - Signal du marché - Ouvrir alertes - + {/* Signal */} + + Signal du marché - {urgentAlert ? ( + {summary ? ( <> - {String(urgentAlert.action ?? "HOLD")} - - {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.reason ?? urgentAlert.message ?? "—")} + {summary.decision} + + {summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}% - - {String(urgentAlert.pair ?? "")} — Confiance{" "} - {typeof urgentAlert.confidence === "number" ? `${Math.round(urgentAlert.confidence * 100)}%` : "—"} + + {summary.reason} ) : ( - Aucune alerte pour le moment. + Aucune donnée pour le moment. )} - - {/* STRATÉGIE (bouton) */} - navigation.navigate("Strategy" as never)} activeOpacity={0.85}> - - Stratégie - Configurer - + {/* Bonus : urgence live si dispo */} + {urgentAlert && ( + + + {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.action ?? "HOLD")} + + + {urgentAlert.reason ?? urgentAlert.message ?? "—"} + + + )} + - - (L’écran Stratégie reste inchangé — ici on ne fait que naviguer.) - + {/* Stratégie */} + navigation.navigate("Strategy" as never)} + > + + + Stratégie + Configurer + + + + Sélection : {settings?.selectedStrategyKey ?? "—"} + + - {/* ACTIONS (Acheter / Vendre) */} - - Actions + {/* Actions */} + + Actions openTrade("BUY")}> @@ -497,12 +460,12 @@ export default function DashboardScreen() { - + Note : Acheter/Vendre = simulation (registre local). Pas de trading réel. - {/* MODAL BUY/SELL */} + {/* Modal BUY/SELL */} setTradeOpen(false)}> @@ -511,11 +474,11 @@ export default function DashboardScreen() { {tradeSide === "BUY" ? "Acheter" : "Vendre"} {selectedCrypto} - - Prix : {(currentPrice ?? 0).toFixed(2)} {currency} + + Prix : {(summary?.price ?? 0).toFixed(2)} {currency} - Quantité + Quantité { @@ -542,51 +505,22 @@ export default function DashboardScreen() { - - {/* Bouton refresh global */} - void refreshAll()}> - Tout actualiser - ); } 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" }, diff --git a/Wallette/mobile/src/services/api/alertsApi.ts b/Wallette/mobile/src/services/api/alertsApi.ts index b0f282b..8c1cf87 100644 --- a/Wallette/mobile/src/services/api/alertsApi.ts +++ b/Wallette/mobile/src/services/api/alertsApi.ts @@ -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 { +type AlertsEventsResponse = { + events?: Alert[]; + count?: number; + limit?: number; +}; + +export async function getAlertHistory(limit = 10): Promise { 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(`/alerts/history?userId=${encodeURIComponent(userId)}`); + const data = await apiGet( + `/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 { diff --git a/Wallette/mobile/src/services/api/dashboardApi.ts b/Wallette/mobile/src/services/api/dashboardApi.ts index 6edc2f2..4f4e64c 100644 --- a/Wallette/mobile/src/services/api/dashboardApi.ts +++ b/Wallette/mobile/src/services/api/dashboardApi.ts @@ -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 { const session = await loadSession(); @@ -16,32 +20,88 @@ export async function getDashboardSummary(): Promise { 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(`/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( + `/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 diff --git a/Wallette/mobile/src/services/api/priceApi.ts b/Wallette/mobile/src/services/api/priceApi.ts index 09bba8d..9fbf20b 100644 --- a/Wallette/mobile/src/services/api/priceApi.ts +++ b/Wallette/mobile/src/services/api/priceApi.ts @@ -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 { - const data = await apiGet(`/prices/current?pair=${encodeURIComponent(pair)}`); + const data = await apiGet(`/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 diff --git a/Wallette/mobile/src/services/api/walletApi.ts b/Wallette/mobile/src/services/api/walletApi.ts index 89170c1..dbb11e2 100644 --- a/Wallette/mobile/src/services/api/walletApi.ts +++ b/Wallette/mobile/src/services/api/walletApi.ts @@ -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; +type WalletListItem = { + id?: string; + walletId?: string; + _id?: string; }; -export async function getPortfolio(): Promise { - 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(`/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 { + 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(`/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(`/wallets/${encodeURIComponent(walletId)}`); + + const assets = extractAssetsFromWalletDetails(details); + + return { + assets, + updatedAtMs: Date.now(), + }; } \ No newline at end of file