From 7b95c96751eaae86dc076722ccf968463f49f6a6 Mon Sep 17 00:00:00 2001 From: Thibaud Moustier Date: Sat, 28 Feb 2026 02:38:45 +0100 Subject: [PATCH] =?utf8?q?Mobile=20:=20Mise=20en=20place=20des=20futur=20e?= =?utf8?q?mplacmeent=20cl=C3=A9e=20API=20+=20modification=20mineur?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- Wallette/mobile/src/config/env.ts | 45 ++++- Wallette/mobile/src/screens/AlertsScreen.tsx | 14 +- .../mobile/src/screens/DashboardScreen.tsx | 158 ++++++++++++++---- Wallette/mobile/src/screens/HistoryScreen.tsx | 47 ++---- .../mobile/src/screens/SettingsScreen.tsx | 110 +++++++++--- .../mobile/src/screens/StrategyScreen.tsx | 28 ++-- Wallette/mobile/src/screens/WalletScreen.tsx | 125 +++++++++----- Wallette/mobile/src/services/api/alertsApi.ts | 33 ++-- .../mobile/src/services/api/dashboardApi.ts | 47 ++++++ Wallette/mobile/src/services/api/http.ts | 43 +++++ Wallette/mobile/src/services/api/priceApi.ts | 34 +++- Wallette/mobile/src/services/api/signalApi.ts | 25 ++- .../mobile/src/services/api/strategyApi.ts | 29 ++-- Wallette/mobile/src/services/api/walletApi.ts | 52 ++++++ 14 files changed, 600 insertions(+), 190 deletions(-) create mode 100644 Wallette/mobile/src/services/api/dashboardApi.ts create mode 100644 Wallette/mobile/src/services/api/http.ts create mode 100644 Wallette/mobile/src/services/api/walletApi.ts diff --git a/Wallette/mobile/src/config/env.ts b/Wallette/mobile/src/config/env.ts index 57d42e8..4ffc475 100644 --- a/Wallette/mobile/src/config/env.ts +++ b/Wallette/mobile/src/config/env.ts @@ -1 +1,44 @@ -const DEV_LAN_IP = "192.168.129.121"; \ No newline at end of file +import { Platform } from "react-native"; + +/** + * env.ts + * ------ + * Objectif : 1 seul point de config réseau. + * + * DEV : IP LAN (PC qui fait tourner gateway en local) + * PROD : URL publique (serveur prof / déploiement) + * + * Le mobile parle UNIQUEMENT au Gateway : + * - REST : /api/... + * - Socket : (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"; + +// Si tu veux autoriser un PROD en http (pas recommandé iOS), tu peux. +// const PROD_GATEWAY = "http://CHANGE_ME_GATEWAY_URL: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; + +/** + * Helpers (debug) + */ +export const ENV_MODE = __DEV__ ? "DEV" : "PROD"; +export const PLATFORM = Platform.OS; \ No newline at end of file diff --git a/Wallette/mobile/src/screens/AlertsScreen.tsx b/Wallette/mobile/src/screens/AlertsScreen.tsx index 7668c21..e4ef5b0 100644 --- a/Wallette/mobile/src/screens/AlertsScreen.tsx +++ b/Wallette/mobile/src/screens/AlertsScreen.tsx @@ -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={ diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index d97a3e6..2aeeca9 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -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(null); const [refreshing, setRefreshing] = useState(false); + // ✅ Prix (API-only) pour TOP 3 assets + const [assetPrices, setAssetPrices] = useState>({}); + const [assetPricesError, setAssetPricesError] = useState(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 = {}; + 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() { - Valeur globale : + Valeur (top 3) : - {portfolioTotalValue.toFixed(2)} {settings.currency} + {portfolioPartialValue !== null ? `${portfolioPartialValue.toFixed(2)} ${settings.currency}` : "—"} + {!!assetPricesError && ( + + ⚠️ {assetPricesError} + + )} + {portfolio.assets.length === 0 ? ( Aucun asset (ajoute BTC/ETH/SOL dans Portefeuille) @@ -337,10 +427,6 @@ export default function DashboardScreen() { )} )} - - - BTC @ {summary.price.toFixed(2)} {settings.currency} (mock) - diff --git a/Wallette/mobile/src/screens/HistoryScreen.tsx b/Wallette/mobile/src/screens/HistoryScreen.tsx index ef568e9..8c29d50 100644 --- a/Wallette/mobile/src/screens/HistoryScreen.tsx +++ b/Wallette/mobile/src/screens/HistoryScreen.tsx @@ -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", }} > - - - {item.action} - + + {item.action} - - - {item.criticality} - + + {item.criticality} - - - {item.status} - + + {item.status} Confiance - - {(item.confidence * 100).toFixed(1)}% - + {(item.confidence * 100).toFixed(1)}% diff --git a/Wallette/mobile/src/screens/SettingsScreen.tsx b/Wallette/mobile/src/screens/SettingsScreen.tsx index aa1540a..b8790e7 100644 --- a/Wallette/mobile/src/screens/SettingsScreen.tsx +++ b/Wallette/mobile/src/screens/SettingsScreen.tsx @@ -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(null); const [infoMessage, setInfoMessage] = useState(null); + const [saving, setSaving] = useState(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({ Devise Actuelle : {settings.currency} - + Changer devise @@ -113,7 +160,11 @@ export default function SettingsScreen({ Rafraîchissement Mode : {settings.refreshMode} - + Changer mode @@ -123,7 +174,11 @@ export default function SettingsScreen({ Notifications Statut : {settings.notificationsEnabled ? "ON" : "OFF"} - + {settings.notificationsEnabled ? "Désactiver" : "Activer"} les notifications @@ -144,9 +199,13 @@ export default function SettingsScreen({ - {/* Bouton Save */} - - Sauvegarder + {/* Bouton Save (garde-fou) */} + + {saving ? "Sauvegarde…" : "Sauvegarder"} @@ -188,4 +247,7 @@ const styles = StyleSheet.create({ color: "#0f172a", opacity: 0.85, }, + btnDisabled: { + opacity: 0.7, + }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/StrategyScreen.tsx b/Wallette/mobile/src/screens/StrategyScreen.tsx index f3ebfdc..17e0af9 100644 --- a/Wallette/mobile/src/screens/StrategyScreen.tsx +++ b/Wallette/mobile/src/screens/StrategyScreen.tsx @@ -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(null); const [strategies, setStrategies] = useState([]); @@ -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() { {item.description} handleSelect(item.key)} > diff --git a/Wallette/mobile/src/screens/WalletScreen.tsx b/Wallette/mobile/src/screens/WalletScreen.tsx index 0368f01..9ade6c1 100644 --- a/Wallette/mobile/src/screens/WalletScreen.tsx +++ b/Wallette/mobile/src/screens/WalletScreen.tsx @@ -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(null); const [settings, setSettings] = useState(null); + // Prix chargés depuis l'API (par symbole) + const [prices, setPrices] = useState>({}); + const [pricesError, setPricesError] = useState(null); + // Ajout asset const [symbolInput, setSymbolInput] = useState("BTC"); const [qtyInput, setQtyInput] = useState("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 = {}; + 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() { - Dernière mise à jour :{" "} - {lastUpdatedLabel} + Dernière mise à jour : {lastUpdatedLabel} + {!!pricesError && ( + + ⚠️ {pricesError} + + )} + Réinitialiser @@ -218,7 +270,7 @@ export default function WalletScreen() { Ajouter / Modifier un asset - Symbole (ex: BTC, ETH, SOL, ADA) + Symbole (ex: BTC, ETH, SOL) {!!info && {info}} - - Prix utilisés = mock pour Step 3 (API à brancher plus tard). - - - Liste des assets : - + Liste des assets : } ListEmptyComponent={ @@ -258,8 +305,8 @@ export default function WalletScreen() { } 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 ( @@ -276,9 +323,9 @@ export default function WalletScreen() { - Prix (mock) + Prix - {price !== null ? `${price.toFixed(2)} ${settings.currency}` : "—"} + {typeof price === "number" ? `${price.toFixed(2)} ${settings.currency}` : "—"} diff --git a/Wallette/mobile/src/services/api/alertsApi.ts b/Wallette/mobile/src/services/api/alertsApi.ts index 3c9e7d2..0e51f28 100644 --- a/Wallette/mobile/src/services/api/alertsApi.ts +++ b/Wallette/mobile/src/services/api/alertsApi.ts @@ -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 { - const all = alertStore.getAll?.() ?? []; - return all.slice(0, limit); +export async function getAlertEvents(limit = 10): Promise { + 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 { +/** + * 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 { 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 index 0000000..6edc2f2 --- /dev/null +++ b/Wallette/mobile/src/services/api/dashboardApi.ts @@ -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 { + 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 index 0000000..8956e3b --- /dev/null +++ b/Wallette/mobile/src/services/api/http.ts @@ -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(path: string): Promise { + 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(path: string, body: unknown): Promise { + 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 diff --git a/Wallette/mobile/src/services/api/priceApi.ts b/Wallette/mobile/src/services/api/priceApi.ts index 224660d..30eb157 100644 --- a/Wallette/mobile/src/services/api/priceApi.ts +++ b/Wallette/mobile/src/services/api/priceApi.ts @@ -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> { - 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 { + 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 diff --git a/Wallette/mobile/src/services/api/signalApi.ts b/Wallette/mobile/src/services/api/signalApi.ts index 8a2d075..404429a 100644 --- a/Wallette/mobile/src/services/api/signalApi.ts +++ b/Wallette/mobile/src/services/api/signalApi.ts @@ -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 { - 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 diff --git a/Wallette/mobile/src/services/api/strategyApi.ts b/Wallette/mobile/src/services/api/strategyApi.ts index 5f5b366..dccd3ff 100644 --- a/Wallette/mobile/src/services/api/strategyApi.ts +++ b/Wallette/mobile/src/services/api/strategyApi.ts @@ -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 { - 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 index 0000000..b6986bc --- /dev/null +++ b/Wallette/mobile/src/services/api/walletApi.ts @@ -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 { + 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 { + 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 { + const empty: PortfolioState = { assets: [], updatedAtMs: nowMs() }; + return await upsertPortfolio(empty); +} \ No newline at end of file -- 2.50.1