From: Thibaud Moustier Date: Sun, 1 Mar 2026 20:40:31 +0000 (+0100) Subject: Mobile : Correction faute et autres X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=94011f0b78d10ce71c8a9d7382f76d548810e92d;p=pdw25-26 Mobile : Correction faute et autres --- diff --git a/Wallette/mobile/src/models/UserSettings.ts b/Wallette/mobile/src/models/UserSettings.ts index 97b7181..e62d737 100644 --- a/Wallette/mobile/src/models/UserSettings.ts +++ b/Wallette/mobile/src/models/UserSettings.ts @@ -1,27 +1,10 @@ -/** - * UserSettings - * ------------ - * Paramètres utilisateur stockés localement (AsyncStorage). - * Step 1/2/3 : pas d'auth obligatoire, donc userId peut être absent. - */ -export interface UserSettings { - // (optionnel) utile pour Socket.IO et futur Step 4 (auth) - userId?: string; +export type Currency = "EUR" | "USDT"; - // Paramètres UI / usage - currency: "EUR" | "USD"; +export interface UserSettings { + currency: Currency; favoriteSymbol: string; - - // Préférences alertes alertPreference: "critical" | "all"; - - // Rafraîchissement dashboard refreshMode: "manual" | "auto"; - - // Notifications locales (Expo) notificationsEnabled: boolean; - - // Stratégie choisie (persistée) - // Exemple: "RSI_SIMPLE" selectedStrategyKey: string; } \ No newline at end of file diff --git a/Wallette/mobile/src/screens/AboutScreen.tsx b/Wallette/mobile/src/screens/AboutScreen.tsx index 02e683e..5ceaa5f 100644 --- a/Wallette/mobile/src/screens/AboutScreen.tsx +++ b/Wallette/mobile/src/screens/AboutScreen.tsx @@ -8,10 +8,10 @@ import { loadSession } from "../utils/sessionStorage"; import { findUserById } from "../utils/authUsersStorage"; /** - * AboutScreen - * ------------------------ - * "Détails du compte" + sections À propos / Support / Version. - * Les infos affichées viennent du compte local (AuthUsersStorage) : + * AboutScreen + * ----------- + * Détails du compte + sections À propos / Support / Version. + * Les infos affichées viennent du compte local : * - displayName (optionnel) * - username * - email @@ -48,7 +48,7 @@ export default function AboutScreen() { setEmail(user?.email ?? session.email); } - init(); + void init(); return () => { active = false; @@ -70,7 +70,12 @@ export default function AboutScreen() { {title} {!!subtitle && {subtitle}} - + ); @@ -79,7 +84,7 @@ export default function AboutScreen() { Détails du compte - {/* Carte profil (style WF-09) */} + {/* Carte profil */} {displayName} - {email} + + {email} + @{username} @@ -99,7 +106,9 @@ export default function AboutScreen() { UserId - {userId} + + {userId} + @@ -107,11 +116,15 @@ export default function AboutScreen() { À propos - + @@ -119,14 +132,13 @@ export default function AboutScreen() { Support - + {/* Version */} Version App : 1.0.0 - Build : Step 4 (sans API) {/* Bouton discret (optionnel) */} diff --git a/Wallette/mobile/src/screens/AlertsScreen.tsx b/Wallette/mobile/src/screens/AlertsScreen.tsx index e4ef5b0..dee535c 100644 --- a/Wallette/mobile/src/screens/AlertsScreen.tsx +++ b/Wallette/mobile/src/screens/AlertsScreen.tsx @@ -5,28 +5,11 @@ import { SafeAreaView } from "react-native-safe-area-context"; 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 - * ----------- - * Liste des alertes reçues (Socket.IO) stockées dans alertStore. - * - * Objectifs : - * - Affichage clair des enums (CRITICAL/WARNING/INFO, BUY/SELL/HOLD/STOP_LOSS) - * - Tri : CRITICAL > WARNING > INFO, puis récent -> ancien - * - Filtre (par défaut : CRITICAL) - * - Clear avec confirmation - */ type Filter = "CRITICAL" | "WARNING" | "INFO" | "ALL"; -function severityRank(level: Alert["alertLevel"]): number { +function severityRank(level?: Alert["alertLevel"]): number { switch (level) { case "CRITICAL": return 3; @@ -38,7 +21,7 @@ function severityRank(level: Alert["alertLevel"]): number { } } -function actionColor(action: Alert["action"]): string { +function actionColor(action?: Alert["action"]): string { switch (action) { case "BUY": return "#16a34a"; @@ -52,7 +35,7 @@ function actionColor(action: Alert["action"]): string { } } -function levelColor(level: Alert["alertLevel"]): string { +function levelColor(level?: Alert["alertLevel"]): string { switch (level) { case "CRITICAL": return "#b91c1c"; @@ -64,8 +47,9 @@ function levelColor(level: Alert["alertLevel"]): string { } } -function formatDate(ms: number): string { - return new Date(ms).toLocaleString(); +function formatDate(ms?: number): string { + const t = typeof ms === "number" && Number.isFinite(ms) ? ms : Date.now(); + return new Date(t).toLocaleString(); } export default function AlertsScreen() { @@ -73,10 +57,8 @@ export default function AlertsScreen() { const [filter, setFilter] = useState("CRITICAL"); useEffect(() => { - // Load initial setItems(alertStore.getAll?.() ?? []); - // Live updates if subscribe exists const unsub = alertStore.subscribe?.((all: Alert[]) => { setItems(all); }); @@ -88,13 +70,11 @@ export default function AlertsScreen() { const filteredSorted = useMemo(() => { const base = - filter === "ALL" ? items : items.filter((a) => a.alertLevel === filter); + filter === "ALL" ? items : items.filter((a) => (a.alertLevel ?? "INFO") === filter); return [...base].sort((a, b) => { - // 1) severity const r = severityRank(b.alertLevel) - severityRank(a.alertLevel); if (r !== 0) return r; - // 2) timestamp desc return (b.timestamp ?? 0) - (a.timestamp ?? 0); }); }, [items, filter]); @@ -102,7 +82,7 @@ export default function AlertsScreen() { const handleClear = useCallback(() => { RNAlert.alert( "Supprimer les alertes ?", - "Cette action supprime les alertes stockées localement (alertStore).", + "Cette action supprime les alertes stockées sur l’app.", [ { text: "Annuler", style: "cancel" }, { @@ -124,9 +104,7 @@ export default function AlertsScreen() { onPress={() => setFilter(value)} style={[styles.filterBtn, active && styles.filterBtnActive]} > - - {label} - + {label} ); }; @@ -137,9 +115,8 @@ export default function AlertsScreen() { contentContainerStyle={ui.container} data={filteredSorted} keyExtractor={(it, idx) => - // Alert n'a pas forcément d'id => fallback stable (it as any).id ?? - `${it.timestamp}-${it.pair}-${it.action}-${it.alertLevel}-${idx}` + `${it.timestamp ?? "0"}-${it.pair ?? ""}-${it.action ?? ""}-${it.alertLevel ?? ""}-${idx}` } ListHeaderComponent={ @@ -147,19 +124,19 @@ export default function AlertsScreen() { Alertes - Clear + Supprimer - - + + - Tri : CRITICAL → WARNING → INFO, puis récent → ancien. + Tri : critique → avertissement → info, puis du plus récent au plus ancien. } @@ -173,33 +150,40 @@ export default function AlertsScreen() { const aColor = actionColor(item.action); const lColor = levelColor(item.alertLevel); + const confidence = + typeof item.confidence === "number" && Number.isFinite(item.confidence) + ? item.confidence + : 0; + return ( - {item.pair} + {item.pair ?? "—"} {formatDate(item.timestamp)} - {item.action} + {item.action ?? "HOLD"} - {item.alertLevel} + {item.alertLevel ?? "INFO"} Confiance - {(item.confidence * 100).toFixed(0)}% + {Math.round(confidence * 100)}% - {item.reason} + + {item.reason ?? item.message ?? "—"} + - {!!item.price && ( + {typeof item.price === "number" && Number.isFinite(item.price) && ( - Prix (optionnel) : {item.price.toFixed(2)} + Prix : {item.price.toFixed(2)} )} diff --git a/Wallette/mobile/src/screens/AuthScreen.tsx b/Wallette/mobile/src/screens/AuthScreen.tsx index db3ab9e..30620ba 100644 --- a/Wallette/mobile/src/screens/AuthScreen.tsx +++ b/Wallette/mobile/src/screens/AuthScreen.tsx @@ -6,28 +6,27 @@ import { ui } from "../components/ui/uiStyles"; import { login as authLogin, register as authRegister } from "../services/api/authApi"; /** - * AuthScreen (Step local, sans API serveur) - * ---------------------------------------- - * Mode Connexion + Mode Création de compte. - * - Identifiant: email OU username - * - Mot de passe obligatoire + * AuthScreen + * ---------- + * Connexion + Création de compte. + * - Identifiant : email OU nom d’utilisateur + * - Mot de passe : minimum 6 caractères * - * IMPORTANT : - * - L'écran appelle authApi (façade). - * - Plus tard, authApi sera remplacé par des appels REST, - * sans modifier cet écran. + * Note technique (code) : + * - L’écran appelle authApi (façade). + * - Le stockage de session est géré côté utils. */ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => void }) { const [mode, setMode] = useState<"login" | "register">("login"); const [error, setError] = useState(null); // Connexion - const [login, setLogin] = useState("demo@example.com"); + const [login, setLogin] = useState(""); const [password, setPassword] = useState(""); - // Register - const [email, setEmail] = useState("demo@example.com"); - const [username, setUsername] = useState("demo"); + // Création compte + const [email, setEmail] = useState(""); + const [username, setUsername] = useState(""); const [displayName, setDisplayName] = useState(""); const [password2, setPassword2] = useState(""); @@ -43,7 +42,7 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => const p = password; if (!l) return setError("Veuillez entrer un email ou un nom d’utilisateur."); - if (!p || p.length < 6) return setError("Mot de passe invalide (min 6)."); + if (!p || p.length < 6) return setError("Mot de passe invalide (minimum 6 caractères)."); const res = await authLogin({ login: l, password: p }); if (!res.ok) return setError(res.message); @@ -61,8 +60,8 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => const p2 = password2; if (!isValidEmail(e)) return setError("Email invalide."); - if (!isValidUsername(u)) return setError("Nom d’utilisateur invalide (3-20, lettres/chiffres/_)."); - if (!p1 || p1.length < 6) return setError("Mot de passe trop court (min 6)."); + if (!isValidUsername(u)) return setError("Nom d’utilisateur invalide (3 à 20, lettres/chiffres/_)."); + if (!p1 || p1.length < 6) return setError("Mot de passe trop court (minimum 6 caractères)."); if (p1 !== p2) return setError("Les mots de passe ne correspondent pas."); const res = await authRegister({ @@ -82,7 +81,7 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => {title} - {/* Tabs */} + {/* Onglets */} value={login} onChangeText={setLogin} autoCapitalize="none" - placeholder="ex: demo@example.com ou demo" + placeholder="ex : user@example.com ou pseudo" style={styles.input} /> @@ -122,7 +121,7 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => value={password} onChangeText={setPassword} secureTextEntry - placeholder="min 6 caractères" + placeholder="minimum 6 caractères" style={styles.input} /> @@ -131,10 +130,6 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => Se connecter - - - (Mode local sans API serveur : destiné au développement / démo.) - ) : ( <> @@ -144,7 +139,7 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" - placeholder="ex: demo@example.com" + placeholder="ex : user@example.com" style={styles.input} /> @@ -153,15 +148,15 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => value={username} onChangeText={setUsername} autoCapitalize="none" - placeholder="ex: demo_123" + placeholder="ex : pseudo_123" style={styles.input} /> - Nom / Prénom (optionnel) + Nom / prénom (optionnel) @@ -170,16 +165,16 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => value={password} onChangeText={setPassword} secureTextEntry - placeholder="min 6 caractères" + placeholder="minimum 6 caractères" style={styles.input} /> - Confirmer + Confirmer le mot de passe @@ -188,10 +183,6 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => Créer le compte - - - (Sans API serveur : stockage local + hash SHA-256, pas de mot de passe en clair.) - )} diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index 8834cfd..bfb21ac 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -27,7 +27,7 @@ import { loadSession } from "../utils/sessionStorage"; import { loadSettings } from "../utils/settingsStorage"; import { loadPortfolio, savePortfolio } from "../utils/portfolioStorage"; -import { SERVER_URL, API_BASE_URL, ENV_MODE } from "../config/env"; +import { SERVER_URL } from "../config/env"; import { socketService } from "../services/socketService"; import { alertStore } from "../services/alertStore"; import { showAlertNotification } from "../services/notificationService"; @@ -38,6 +38,7 @@ import { getPortfolio as getPortfolioFromApi } from "../services/api/walletApi"; type CryptoSymbol = "BTC" | "ETH" | "LTC"; type TradeSide = "BUY" | "SELL"; + const CRYPTOS: CryptoSymbol[] = ["BTC", "ETH", "LTC"]; function walletAddressKey(userId: string) { @@ -75,7 +76,6 @@ export default function DashboardScreen() { }); const [selectedCrypto, setSelectedCrypto] = useState("BTC"); - const [summary, setSummary] = useState(null); const [walletAddress, setWalletAddress] = useState(""); @@ -90,8 +90,14 @@ export default function DashboardScreen() { const [tradeQty, setTradeQty] = useState("0.01"); const [tradeInfo, setTradeInfo] = useState(null); - const currency: "EUR" | "USD" = settings?.currency === "USD" ? "USD" : "EUR"; - const pair = useMemo(() => `${selectedCrypto}/${currency}`, [selectedCrypto, currency]); + // ✅ Currency alignée DB/serveur : EUR ou USDT + const quote: "EUR" | "USDT" = settings?.currency === "USDT" ? "USDT" : "EUR"; + + // ✅ Pair affiché et utilisé côté serveur (DB ne connaît que BTC/EUR et BTC/USDT) + const serverPair = useMemo(() => `BTC/${quote}`, [quote]); + + // UI pair : on peut afficher la crypto sélectionnée, mais sans inventer des paires serveur + const displayPair = useMemo(() => `${selectedCrypto}/${quote}`, [selectedCrypto, quote]); const selectedQty = useMemo(() => { const a = portfolio.assets.find((x) => normalizeSymbol(x.symbol) === selectedCrypto); @@ -130,16 +136,16 @@ export default function DashboardScreen() { setWalletAddress(""); } - // Dashboard summary (prix+signal) + // Dashboard summary (prix + signal) => dépend du pair BTC/EUR ou BTC/USDT try { const dash = await getDashboardSummary(); setSummary(dash); } catch { setSummary(null); - setSoftError(`Signal/Prix indisponibles (API). DEV=${ENV_MODE}. Base REST: ${API_BASE_URL}`); + setSoftError("Données indisponibles pour le moment. Réessayez dans quelques secondes."); } - // Wallet API (si dispo) -> fusion + // Wallet API (source de vérité) -> cache local if (uid) { try { const apiPortfolio = await getPortfolioFromApi(); @@ -181,7 +187,7 @@ export default function DashboardScreen() { }, [refreshAll]) ); - // Socket live (✅ connect seulement si userId existe) + // Socket live (connect uniquement si session) useEffect(() => { let unsub: null | (() => void) = null; let alive = true; @@ -204,7 +210,7 @@ export default function DashboardScreen() { setSocketConnected(true); } catch { if (!alive) return; - setSocketInfo("Socket indisponible (URL / serveur)."); + setSocketInfo("Socket indisponible pour le moment."); return; } @@ -244,7 +250,7 @@ export default function DashboardScreen() { const trimmed = walletAddress.trim(); if (trimmed.length < 6) { - setWalletAddressInfo("Adresse trop courte. Vérifie la saisie."); + setWalletAddressInfo("Adresse trop courte. Vérifiez la saisie."); return; } @@ -264,7 +270,7 @@ export default function DashboardScreen() { const qty = Number(tradeQty.replace(",", ".").trim()); if (!Number.isFinite(qty) || qty <= 0) { - setTradeInfo("Quantité invalide (ex: 0.01)."); + setTradeInfo("Quantité invalide (ex : 0.01)."); return; } @@ -277,7 +283,7 @@ export default function DashboardScreen() { if (tradeSide === "BUY") nextQty = currentQty + qty; else { if (qty > currentQty) { - setTradeInfo(`Vente impossible : tu n'as que ${currentQty.toFixed(6)} ${symbol}.`); + setTradeInfo(`Vente impossible : vous n'avez que ${currentQty.toFixed(6)} ${symbol}.`); return; } nextQty = currentQty - qty; @@ -329,7 +335,7 @@ export default function DashboardScreen() { {/* Crypto + adresse */} - Choisir une cryptomonnaie + Choix de cryptomonnaie {CRYPTOS.map((c) => { @@ -353,7 +359,7 @@ export default function DashboardScreen() { setWalletAddressInfo(null); setWalletAddress(t); }} - placeholder="Entrez une adresse crypto" + placeholder="Entrez une adresse" style={styles.input} autoCapitalize="none" autoCorrect={false} @@ -363,7 +369,9 @@ export default function DashboardScreen() { Enregistrer l’adresse - {!!walletAddressInfo && {walletAddressInfo}} + {!!walletAddressInfo && ( + {walletAddressInfo} + )} {/* Solde */} @@ -372,17 +380,25 @@ export default function DashboardScreen() { {selectedQty.toFixed(6)} {selectedCrypto} - Portefeuille local (+ sync serveur si dispo) + Synchronisé avec le serveur quand disponible. {/* Prix */} Prix - {pair} + {displayPair} + - {(summary?.price ?? 0).toFixed(2)} {currency} + {(summary?.price ?? 0).toFixed(2)} {quote} + {/* Info transparente : prix/signal serveur sont basés sur BTC (DB) */} + {selectedCrypto !== "BTC" && ( + + Note : les données serveur (prix/signal) sont disponibles uniquement pour BTC. + + )} + void refreshAll()}> Actualiser @@ -400,6 +416,7 @@ export default function DashboardScreen() { {summary.reason} + {serverPair} ) : ( Aucune donnée pour le moment. @@ -462,7 +479,7 @@ export default function DashboardScreen() { - Note : Acheter/Vendre = simulation (registre local). Pas de trading réel. + Note : acheter/vendre enregistre une opération locale. (Pas de trading réel) @@ -476,7 +493,7 @@ export default function DashboardScreen() { - Prix : {(summary?.price ?? 0).toFixed(2)} {currency} + Prix (serveur) : {(summary?.price ?? 0).toFixed(2)} {quote} Quantité diff --git a/Wallette/mobile/src/screens/HistoryScreen.tsx b/Wallette/mobile/src/screens/HistoryScreen.tsx index 8c29d50..ad2aef5 100644 --- a/Wallette/mobile/src/screens/HistoryScreen.tsx +++ b/Wallette/mobile/src/screens/HistoryScreen.tsx @@ -3,12 +3,7 @@ import { useEffect, useState } from "react"; import { SafeAreaView } from "react-native-safe-area-context"; import type { Signal, SignalAction, SignalCriticality } from "../types/Signal"; - -/** - * ✅ API-ready (façade) - */ import { getRecentSignals } from "../services/api/signalApi"; - import { ui } from "../components/ui/uiStyles"; function getActionColor(action: SignalAction): string { @@ -41,6 +36,12 @@ function formatDate(ms: number): string { return new Date(ms).toLocaleString(); } +function quoteFromPair(pair: string): string { + const p = String(pair ?? ""); + const parts = p.split("/"); + return parts.length === 2 ? parts[1] : ""; +} + export default function HistoryScreen() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); @@ -57,13 +58,13 @@ export default function HistoryScreen() { const data = await getRecentSignals(20); if (active) setItems(data); } catch { - if (active) setError("Impossible de charger l'historique."); + if (active) setError("Impossible de charger l’historique pour le moment."); } finally { if (active) setLoading(false); } } - load(); + void load(); return () => { active = false; @@ -73,7 +74,7 @@ export default function HistoryScreen() { if (loading) { return ( - Chargement de l'historique… + Chargement de l’historique… ); } @@ -91,7 +92,7 @@ export default function HistoryScreen() { it.signalId} + keyExtractor={(it, idx) => String((it as any).signalId ?? (it as any).id ?? idx)} ListEmptyComponent={ Aucun signal @@ -101,6 +102,7 @@ export default function HistoryScreen() { renderItem={({ item }) => { const actionColor = getActionColor(item.action); const critColor = getCriticalityColor(item.criticality); + const quote = quoteFromPair(item.pair); return ( @@ -109,7 +111,6 @@ export default function HistoryScreen() { {formatDate(item.timestamp)} - {/* Badges */} Prix au signal - {item.priceAtSignal.toFixed(2)} + + {item.priceAtSignal.toFixed(2)} {quote} + {item.reason} diff --git a/Wallette/mobile/src/screens/SettingsScreen.tsx b/Wallette/mobile/src/screens/SettingsScreen.tsx index b8790e7..831f50a 100644 --- a/Wallette/mobile/src/screens/SettingsScreen.tsx +++ b/Wallette/mobile/src/screens/SettingsScreen.tsx @@ -10,17 +10,6 @@ import { requestNotificationPermission } from "../services/notificationService"; import { ui } from "../components/ui/uiStyles"; import { setSeenTutorial } from "../utils/tutorialStorage"; -/** - * SettingsScreen - * -------------- - * - 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, }: { @@ -54,11 +43,6 @@ export default function SettingsScreen({ ); } - /** - * Sauvegarde "safe" - * - évite les doubles clics - * - affiche un message simple - */ const persist = async (next: UserSettings, msg?: string) => { try { setSaving(true); @@ -70,9 +54,10 @@ export default function SettingsScreen({ } }; + // EUR <-> USDT (aligné DB/serveur) const toggleCurrency = async () => { setInfoMessage(null); - const newCurrency = settings.currency === "EUR" ? "USD" : "EUR"; + const newCurrency = settings.currency === "EUR" ? "USDT" : "EUR"; await persist({ ...settings, currency: newCurrency }, `Devise : ${newCurrency} ✅`); }; @@ -85,7 +70,6 @@ export default function SettingsScreen({ const toggleNotifications = async () => { setInfoMessage(null); - // OFF -> ON : on demande la permission if (!settings.notificationsEnabled) { const granted = await requestNotificationPermission(); if (!granted) { @@ -96,24 +80,13 @@ export default function SettingsScreen({ return; } - await persist( - { ...settings, notificationsEnabled: true }, - "Notifications activées ✅" - ); + await persist({ ...settings, notificationsEnabled: true }, "Notifications activées ✅"); return; } - // ON -> OFF - await persist( - { ...settings, notificationsEnabled: false }, - "Notifications désactivées ✅" - ); + 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 () => { setInfoMessage(null); await persist(settings, "Paramètres sauvegardés ✅"); @@ -141,7 +114,6 @@ export default function SettingsScreen({ Paramètres - {/* Carte : Devise */} Devise Actuelle : {settings.currency} @@ -155,7 +127,6 @@ export default function SettingsScreen({ - {/* Carte : Refresh */} Rafraîchissement Mode : {settings.refreshMode} @@ -169,10 +140,11 @@ export default function SettingsScreen({ - {/* Carte : Notifications */} Notifications - Statut : {settings.notificationsEnabled ? "ON" : "OFF"} + + Statut : {settings.notificationsEnabled ? "ON" : "OFF"} + {infoMessage}} - {/* Carte : Tutoriel */} Tutoriel @@ -199,7 +170,6 @@ export default function SettingsScreen({ - {/* Bouton Save (garde-fou) */} { active = false; }; @@ -56,11 +56,11 @@ export default function StrategyScreen() { setBusy(true); setInfo(null); - // Pair alignée dashboard : BTC + devise settings - const currency = settings.currency === "USD" ? "USD" : "EUR"; - const pair = `BTC/${currency}`; + // Pair alignée DB/serveur : EUR -> BTC/EUR ; USDT -> BTC/USDT + const quote = settings.currency === "USDT" ? "USDT" : "EUR"; + const pair = `BTC/${quote}`; - // 1) Serveur d’abord (align Stéphane) + // 1) Serveur d’abord (source de vérité) await selectStrategy({ pair, mode: key, params: {} }); // 2) Puis local (affichage immédiat) @@ -75,7 +75,7 @@ export default function StrategyScreen() { await saveSettings(rollback); setSettings(rollback); - setInfo(`Erreur serveur stratégie : ${e?.message ?? "inconnue"}`); + setInfo(`Impossible de sélectionner la stratégie. (${e?.message ?? "Erreur inconnue"})`); } finally { setBusy(false); } @@ -90,7 +90,9 @@ export default function StrategyScreen() { ListHeaderComponent={ Stratégie - Choisis une stratégie. Elle est enregistrée sur le serveur. + + Choisissez une stratégie. Elle est enregistrée sur le serveur. + Actuelle : {settings.selectedStrategyKey} @@ -115,11 +117,11 @@ export default function StrategyScreen() { isSelected(item.key) && styles.btnSelected, busy && styles.btnDisabled, ]} - onPress={() => handleSelect(item.key)} + onPress={() => void handleSelect(item.key)} disabled={busy} > - {isSelected(item.key) ? "Sélectionnée ✅" : busy ? "Envoi..." : "Sélectionner"} + {isSelected(item.key) ? "Sélectionnée ✅" : busy ? "Envoi…" : "Sélectionner"} diff --git a/Wallette/mobile/src/screens/TutorialScreen.tsx b/Wallette/mobile/src/screens/TutorialScreen.tsx index 3f5f36c..84a9c20 100644 --- a/Wallette/mobile/src/screens/TutorialScreen.tsx +++ b/Wallette/mobile/src/screens/TutorialScreen.tsx @@ -18,39 +18,67 @@ export default function TutorialScreen({ onDone }: { onDone: () => void }) { () => [ { key: "intro", - title: "La crypto, c’est quoi ?", + title: "Bienvenue sur Wallette", text: - "Une cryptomonnaie est une monnaie numérique. Son prix varie selon l’offre et la demande, et peut bouger très vite.", + "Wallette vous aide à suivre le marché via des signaux et des alertes.\n\nObjectif : comprendre et observer. Pas “trader” pour de vrai.", icon: "sparkles-outline", }, { - key: "types", - title: "Types de cryptos", + key: "crypto_basics", + title: "La crypto, c’est quoi ?", text: - "Exemples :\n• BTC : la plus connue\n• ETH : écosystème de contrats\n• Stablecoins (USDT/USDC) : valeur ≈ 1 USD\n\nLes stablecoins servent souvent d’intermédiaire pour convertir.", - icon: "layers-outline", + "Une cryptomonnaie est une monnaie numérique.\nSon prix varie selon l’offre et la demande, parfois très vite.", + icon: "planet-outline", }, { - key: "role", - title: "Rôle de Wall-e-tte", + key: "pairs", + title: "Paires BTC/EUR et BTC/USDT", text: - "Wall-e-tte est un assistant : il aide à suivre le marché, les signaux et les alertes.\n\nCe n’est PAS un conseiller financier.", - icon: "shield-checkmark-outline", + "L’application utilise des paires de prix :\n• BTC/EUR\n• BTC/USDT\n\nUSDT est un “stablecoin” proche de 1 dollar.", + icon: "swap-horizontal-outline", + }, + { + key: "signal", + title: "Signal du marché", + text: + "Le signal résume la décision d’une stratégie :\n• BUY (acheter)\n• SELL (vendre)\n• HOLD (attendre)\n• STOP_LOSS (se protéger)\n\nLe signal inclut une confiance (%) et une raison.", + icon: "pulse-outline", + }, + { + key: "alerts", + title: "Alertes", + text: + "Les alertes sont envoyées en temps réel.\nElles peuvent être CRITICAL / WARNING / INFO.\n\nAstuce : commencez par les alertes CRITICAL.", + icon: "alert-circle-outline", }, { - key: "app", - title: "Comment utiliser l’app", + key: "wallet", + title: "Portefeuille", text: - "• Dashboard : résumé (prix, stratégie, urgence)\n• Portefeuille : ajouter des cryptos + quantités\n• Alertes : tri + filtres (CRITICAL en priorité)\n• Historique : signaux récents", - icon: "apps-outline", + "L’écran Portefeuille affiche votre solde BTC et l’historique des transactions.\n\nLes données proviennent du serveur du projet.", + icon: "wallet-outline", + }, + { + key: "strategy", + title: "Stratégie", + text: + "Une stratégie analyse le marché et produit des signaux.\n\nVous pouvez en sélectionner une dans l’écran “Stratégie”.", + icon: "analytics-outline", }, { key: "settings", - title: "Paramètres importants", + title: "Paramètres", text: - "• Stratégie : choisir la méthode d’analyse\n• Notifications : recevoir les alertes\n• Devise : EUR/USD\n\nLe tutoriel reste accessible dans Paramètres.", + "Vous pouvez régler :\n• Devise : EUR ou USDT\n• Rafraîchissement : manuel ou auto\n• Notifications : activer/désactiver\n\nLe tutoriel reste accessible via Paramètres.", icon: "settings-outline", }, + { + key: "warning", + title: "Avertissement", + text: + "Wallette est un outil éducatif.\nCe n’est pas un conseil financier.\n\nNe mettez jamais d’argent réel sur base d’un projet étudiant.", + icon: "shield-checkmark-outline", + }, ], [] ); @@ -91,7 +119,7 @@ export default function TutorialScreen({ onDone }: { onDone: () => void }) { - {/* CONTENT (on laisse de la place en bas pour la barre fixe) */} + {/* CONTENT */} @@ -124,9 +152,7 @@ export default function TutorialScreen({ onDone }: { onDone: () => void }) { style={[styles.primaryBtn]} onPress={isLast ? finish : next} > - - {isLast ? "Terminer" : "Suivant"} - + {isLast ? "Terminer" : "Suivant"} @@ -150,12 +176,11 @@ const styles = StyleSheet.create({ small: { fontWeight: "900", color: "#0f172a", opacity: 0.6 }, skip: { fontWeight: "900", color: "#0f172a", opacity: 0.75 }, - // On réserve de la place pour la barre fixe content: { flex: 1, alignItems: "center", justifyContent: "center", - paddingBottom: 140, // ✅ réserve l’espace pour les boutons fixes + paddingBottom: 140, }, iconWrap: { @@ -180,7 +205,6 @@ const styles = StyleSheet.create({ lineHeight: 20, }, - // ✅ barre du bas fixée bottomFixed: { position: "absolute", left: 0, @@ -221,7 +245,7 @@ const styles = StyleSheet.create({ paddingVertical: 12, alignItems: "center", justifyContent: "center", - backgroundColor: "#16a34a", // ✅ un vert "crypto friendly" + backgroundColor: "#16a34a", }, primaryText: { color: "#fff", fontWeight: "900" }, diff --git a/Wallette/mobile/src/screens/WalletScreen.tsx b/Wallette/mobile/src/screens/WalletScreen.tsx index 9ade6c1..e12b519 100644 --- a/Wallette/mobile/src/screens/WalletScreen.tsx +++ b/Wallette/mobile/src/screens/WalletScreen.tsx @@ -1,86 +1,110 @@ -import { - View, - Text, - StyleSheet, - TouchableOpacity, - TextInput, - Alert as RNAlert, - FlatList, -} from "react-native"; +import { View, Text, StyleSheet, FlatList } from "react-native"; import { useCallback, useMemo, useState } from "react"; import { SafeAreaView } from "react-native-safe-area-context"; import { useFocusEffect } from "@react-navigation/native"; import { ui } from "../components/ui/uiStyles"; -import type { PortfolioAsset, PortfolioState } from "../models/Portfolio"; import { loadSettings } from "../utils/settingsStorage"; import type { UserSettings } from "../models/UserSettings"; +import type { PortfolioState } from "../models/Portfolio"; -/** - * ✅ API-only (wallet) - */ -import { getPortfolio, upsertPortfolio, resetPortfolio } from "../services/api/walletApi"; - -/** - * ✅ API-only (price) - */ +import { getPortfolio, getPrimaryWalletId, getWalletEvents, type WalletEvent } from "../services/api/walletApi"; import { getCurrentPrice } from "../services/api/priceApi"; /** - * WalletScreen (Step 3/4) - * ----------------------- - * Multi-cryptos - * - quantité par asset - * - valeur globale + * WalletScreen (serveur/DB only) + * ----------------------------- + * Affiche uniquement ce que le serveur fournit : + * - Wallet BTC (quantité) + * - Valeur estimée (prix BTC/EUR ou BTC/USDT) + * - Historique events du wallet (/wallets/:id/events) * - * Prix : API-only (pas de mock) + * Aucun ajout/modif local ici (aligné consigne + DB). */ + +function formatNumber(n: number, decimals = 6) { + return Number.isFinite(n) ? n.toFixed(decimals) : "0.000000"; +} + +function quoteFromSettings(s: UserSettings): "EUR" | "USDT" { + return s.currency === "USDT" ? "USDT" : "EUR"; +} + +function asNumber(x: any): number { + const n = typeof x === "number" ? x : Number(x); + return Number.isFinite(n) ? n : 0; +} + +function formatDate(ms?: number) { + const t = typeof ms === "number" && Number.isFinite(ms) ? ms : Date.now(); + return new Date(t).toLocaleString(); +} + 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); + const [walletId, setWalletId] = useState(null); - // Ajout asset - const [symbolInput, setSymbolInput] = useState("BTC"); - const [qtyInput, setQtyInput] = useState("0"); + const [btcPrice, setBtcPrice] = useState(null); + const [priceError, setPriceError] = useState(null); - const [info, setInfo] = useState(null); + const [events, setEvents] = useState([]); + const [eventsError, setEventsError] = useState(null); useFocusEffect( useCallback(() => { let active = true; async function init() { - setInfo(null); - setPricesError(null); + setPriceError(null); + setEventsError(null); - const [p, s] = await Promise.all([getPortfolio(), loadSettings()]); + const s = await loadSettings(); if (!active) return; - - setPortfolio(p); setSettings(s); - // Charger les prix pour les assets actuels + // WalletId + Portfolio serveur 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; - }) - ); - + const [id, p] = await Promise.all([getPrimaryWalletId(), getPortfolio()]); if (!active) return; - const map: Record = {}; - for (const [sym, pr] of entries) map[sym] = pr; - setPrices(map); + setWalletId(id); + setPortfolio(p); + + // Prix BTC (pair DB only) + const quote = quoteFromSettings(s); + const pair = `BTC/${quote}`; + try { + const cur = await getCurrentPrice(pair); + if (!active) return; + setBtcPrice(cur.price); + } catch (e: any) { + if (!active) return; + setBtcPrice(null); + setPriceError(e?.message ?? "Impossible de charger le prix BTC."); + } + + // Events + if (id) { + try { + const evts = await getWalletEvents(50); + if (!active) return; + setEvents(evts); + } catch (e: any) { + if (!active) return; + setEvents([]); + setEventsError(e?.message ?? "Impossible de charger l’historique des transactions."); + } + } else { + setEvents([]); + } } catch (e: any) { if (!active) return; - setPricesError(e?.message ?? "Impossible de charger les prix (API)."); + setPortfolio({ assets: [], updatedAtMs: Date.now() }); + setWalletId(null); + setEvents([]); + setEventsError(e?.message ?? "Impossible de charger le portefeuille."); } } @@ -92,135 +116,18 @@ export default function WalletScreen() { }, []) ); - const lastUpdatedLabel = useMemo(() => { - if (!portfolio) return "—"; - return new Date(portfolio.updatedAtMs).toLocaleString(); + const btcQty = useMemo(() => { + if (!portfolio) return 0; + const a = portfolio.assets.find((x) => x.symbol === "BTC"); + return a?.quantity ?? 0; }, [portfolio]); - const parsedQty = useMemo(() => { - const normalized = qtyInput.replace(",", ".").trim(); - const val = Number(normalized); - if (!Number.isFinite(val)) return null; - if (val < 0) return null; - return val; - }, [qtyInput]); - - const normalizedSymbol = useMemo(() => symbolInput.toUpperCase().trim(), [symbolInput]); + const quote = useMemo(() => (settings ? quoteFromSettings(settings) : "EUR"), [settings]); const totalValue = useMemo(() => { - if (!portfolio) return 0; - - return portfolio.assets.reduce((sum, a) => { - const price = prices[a.symbol]; - if (typeof price !== "number") return sum; // prix absent => on ignore - return sum + a.quantity * price; - }, 0); - }, [portfolio, prices]); - - const handleAddOrUpdate = async () => { - if (!portfolio || !settings) return; - - setInfo(null); - - if (!normalizedSymbol || normalizedSymbol.length < 2) { - setInfo("Symbole invalide (ex: BTC)."); - return; - } - - if (parsedQty === null) { - setInfo("Quantité invalide. Exemple : 0.25"); - return; - } - - // ✅ 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; - } - - const existingIndex = portfolio.assets.findIndex((a) => a.symbol === normalizedSymbol); - - let updatedAssets: PortfolioAsset[]; - if (existingIndex >= 0) { - updatedAssets = portfolio.assets.map((a) => - a.symbol === normalizedSymbol ? { ...a, quantity: parsedQty } : a - ); - } else { - updatedAssets = [...portfolio.assets, { symbol: normalizedSymbol, quantity: parsedQty }]; - } - - const updated: PortfolioState = { - assets: updatedAssets, - updatedAtMs: Date.now(), - }; - - await upsertPortfolio(updated); - setPortfolio(updated); - - setInfo(existingIndex >= 0 ? "Asset mis à jour ✅" : "Asset ajouté ✅"); - }; - - const handleDelete = (symbol: string) => { - if (!portfolio) return; - - RNAlert.alert( - `Supprimer ${symbol} ?`, - "Cette action retire l’asset du portefeuille.", - [ - { text: "Annuler", style: "cancel" }, - { - text: "Supprimer", - style: "destructive", - onPress: async () => { - const updated: PortfolioState = { - assets: portfolio.assets.filter((a) => a.symbol !== symbol), - updatedAtMs: Date.now(), - }; - - await upsertPortfolio(updated); - setPortfolio(updated); - - // nettoyage prix local - setPrices((prev) => { - const next = { ...prev }; - delete next[symbol]; - return next; - }); - - setInfo(`${symbol} supprimé ✅`); - }, - }, - ] - ); - }; - - const handleClear = () => { - RNAlert.alert( - "Réinitialiser le portefeuille ?", - "Cela supprime tous les assets du portefeuille.", - [ - { text: "Annuler", style: "cancel" }, - { - text: "Réinitialiser", - style: "destructive", - onPress: async () => { - const fresh = await resetPortfolio(); - setPortfolio(fresh); - setPrices({}); - setInfo("Portefeuille réinitialisé ✅"); - }, - }, - ] - ); - }; + if (btcPrice === null) return null; + return btcQty * btcPrice; + }, [btcQty, btcPrice]); if (!portfolio || !settings) { return ( @@ -234,105 +141,91 @@ export default function WalletScreen() { it.symbol} + data={events} + keyExtractor={(it, idx) => String(it.event_id ?? idx)} ListHeaderComponent={ Portefeuille - {/* Résumé global */} + {/* Résumé wallet */} Résumé - - Valeur globale : + + WalletId : {walletId ?? "—"} + + + + Solde BTC + {formatNumber(btcQty, 6)} BTC + + + + Prix BTC - {totalValue.toFixed(2)} {settings.currency} + {btcPrice !== null ? `${btcPrice.toFixed(2)} ${quote}` : "—"} - - Dernière mise à jour : {lastUpdatedLabel} - + + Valeur estimée + + {totalValue !== null ? `${totalValue.toFixed(2)} ${quote}` : "—"} + + - {!!pricesError && ( - - ⚠️ {pricesError} + {!!priceError && ( + + {priceError} )} - - - Réinitialiser - - {/* Ajouter / modifier */} + {/* Historique */} - Ajouter / Modifier un asset - - Symbole (ex: BTC, ETH, SOL) - - - Quantité - - - - Enregistrer - - - {!!info && {info}} + Historique des transactions + + Données issues du serveur (events du wallet). + + + {!!eventsError && ( + + {eventsError} + + )} - Liste des assets : + Derniers événements : } ListEmptyComponent={ - Aucun asset - Ajoutez BTC/ETH/SOL… pour commencer. + Aucune transaction + Aucun événement enregistré pour l’instant. } renderItem={({ item }) => { - const price = prices[item.symbol]; - const value = typeof price === "number" ? item.quantity * price : null; + const type = String(item.event_type ?? "—").toUpperCase(); + const qty = asNumber(item.quantity_base); + const price = asNumber(item.price_quote); + const q = String(item.quote_symbol ?? quote); return ( - {item.symbol} - handleDelete(item.symbol)}> - Supprimer - + {type} + {formatDate(item.executed_at_ms)} Quantité - {item.quantity.toFixed(6)} + {formatNumber(qty, 6)} BTC Prix - {typeof price === "number" ? `${price.toFixed(2)} ${settings.currency}` : "—"} - - - - - Valeur - - {value !== null ? `${value.toFixed(2)} ${settings.currency}` : "—"} + {price > 0 ? `${price.toFixed(2)} ${q}` : "—"} @@ -344,55 +237,7 @@ export default function WalletScreen() { } const styles = StyleSheet.create({ - safeArea: { - flex: 1, - backgroundColor: ui.screen.backgroundColor, - }, - screenTitle: { - fontSize: 22, - fontWeight: "900", - marginBottom: 12, - color: "#0f172a", - }, - boldInline: { - fontWeight: "900", - color: "#0f172a", - }, - - input: { - borderWidth: 1, - borderColor: "#e5e7eb", - borderRadius: 10, - paddingHorizontal: 12, - paddingVertical: 10, - marginTop: 8, - backgroundColor: "#fff", - color: "#0f172a", - }, - - fullButton: { - flexGrow: 0, - flexBasis: "auto", - width: "100%", - marginTop: 12, - }, - - secondaryButton: { - marginTop: 10, - paddingVertical: 10, - borderRadius: 10, - borderWidth: 1, - borderColor: "#e5e7eb", - alignItems: "center", - backgroundColor: "#fff", - }, - secondaryButtonText: { - fontWeight: "900", - color: "#dc2626", - }, - - deleteText: { - fontWeight: "900", - color: "#dc2626", - }, + safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor }, + screenTitle: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" }, + boldInline: { fontWeight: "900", color: "#0f172a" }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/services/api/authApi.ts b/Wallette/mobile/src/services/api/authApi.ts index 347dd62..7da2921 100644 --- a/Wallette/mobile/src/services/api/authApi.ts +++ b/Wallette/mobile/src/services/api/authApi.ts @@ -7,21 +7,18 @@ import { loadSession, saveSession, clearSession } from "../../utils/sessionStora /** * authApi.ts * ---------- - * Façade d'authentification. + * Décision projet : PAS d'auth serveur. * - * Aujourd'hui (Step local) : - * - login/register/logout en local (AsyncStorage) + * IMPORTANT : + * - Les services (alerts/signal/wallets) utilisent un userId FIXE côté serveur : "user-123". + * - Donc la session côté mobile doit utiliser ce userId pour toutes les routes API. * - * Demain (API ready) : - * - tu pourras remplacer l'implémentation par : - * POST /api/auth/login - * POST /api/auth/register - * GET /api/auth/me - * POST /api/auth/logout - * - * L'UI ne change pas : elle appelle toujours authApi. + * L'auth locale sert uniquement à l'expérience utilisateur (écran de connexion), + * mais le userId utilisé pour l'API = "user-123" (align serveur). */ +const SERVER_USER_ID = "user-123"; + export type AuthResult = | { ok: true; user: AuthUser; session: Session } | { ok: false; message: string }; @@ -35,8 +32,9 @@ export async function register(params: { const res = await createUser(params); if (!res.ok) return { ok: false, message: res.message }; + // ✅ userId aligné serveur const session: Session = { - userId: res.user.userId, + userId: SERVER_USER_ID, email: res.user.email, createdAtMs: Date.now(), }; @@ -53,8 +51,9 @@ export async function login(params: { const res = await verifyLogin(params); if (!res.ok) return { ok: false, message: res.message }; + // ✅ userId aligné serveur const session: Session = { - userId: res.user.userId, + userId: SERVER_USER_ID, email: res.user.email, createdAtMs: Date.now(), }; @@ -69,19 +68,18 @@ export async function logout(): Promise { } /** - * Retourne l'utilisateur courant (si session active). - * Utile pour afficher profil / sécuriser certaines actions. + * Retourne l'utilisateur courant (profil local). + * (Le userId serveur étant fixe, on conserve le profil local via la recherche par session.email si besoin.) */ export async function getCurrentUser(): Promise { const session = await loadSession(); - if (!session?.userId) return null; + if (!session) return null; - return await findUserById(session.userId); + // On tente par userId local d'abord (si jamais), sinon on retombe sur findUserById. + // Note : si ton système local lie le profil à un autre userId, on peut améliorer plus tard. + return await findUserById((session as any).userId ?? SERVER_USER_ID); } -/** - * Juste la session (pratique pour guards). - */ export async function getSession(): Promise { return await loadSession(); } \ No newline at end of file diff --git a/Wallette/mobile/src/services/api/dashboardApi.ts b/Wallette/mobile/src/services/api/dashboardApi.ts index 0cb2dbc..2a1e8ab 100644 --- a/Wallette/mobile/src/services/api/dashboardApi.ts +++ b/Wallette/mobile/src/services/api/dashboardApi.ts @@ -16,51 +16,36 @@ function safeLevel(level: any): AlertLevel { return "INFO"; } -/** - * dashboardApi (contrat Stéphane - STRICT) - * --------------------------------------- - * - GET /api/price/current?pair=BTC/EUR - * - GET /api/signal/current?userId=...&pair=BTC/EUR - */ 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 currency: "EUR" | "USD" = settings.currency === "USD" ? "USD" : "EUR"; + const quote = settings.currency === "USDT" ? "USDT" : "EUR"; + const pair = `BTC/${quote}`; - // Dashboard strict : BTC seulement (align web). Adaptable plus tard. - const pair = `BTC/${currency}`; - - // 1) Prix (service prix) const price = await getCurrentPrice(pair); - // 2) Signal (service stratégies) const sig = await apiGet( `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}` ); - const decision = safeDecision(sig?.action ?? sig?.decision); - const alertLevel = safeLevel(sig?.alertLevel ?? sig?.level ?? sig?.criticality); - const confidence = typeof sig?.confidence === "number" ? sig.confidence : 0; - const reason = String(sig?.reason ?? sig?.message ?? "—"); - - const timestamp = - typeof sig?.timestamp === "number" - ? sig.timestamp - : typeof sig?.timestamp_ms === "number" - ? sig.timestamp_ms - : price.timestampMs; + const data = sig ?? null; return { pair, price: price.price, strategy: settings.selectedStrategyKey, - decision, - confidence, - reason, - alertLevel, - timestamp, + decision: safeDecision(data?.action ?? data?.decision), + confidence: typeof data?.confidence === "number" ? data.confidence : 0, + reason: String(data?.reason ?? data?.message ?? "—"), + alertLevel: safeLevel(data?.alertLevel ?? data?.level ?? data?.criticality), + timestamp: + typeof data?.timestamp_ms === "number" + ? data.timestamp_ms + : typeof data?.timestamp === "number" + ? data.timestamp + : price.timestampMs, }; } \ 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 404429a..7122ca8 100644 --- a/Wallette/mobile/src/services/api/signalApi.ts +++ b/Wallette/mobile/src/services/api/signalApi.ts @@ -1,22 +1,113 @@ -import type { Signal } from "../../types/Signal"; import { apiGet } from "./http"; import { loadSession } from "../../utils/sessionStorage"; +import type { Signal, SignalAction, SignalCriticality, SignalStatus } from "../../types/Signal"; /** - * signalApi (API-only) - * -------------------- - * NOTE: endpoint à confirmer/aligner avec le chef. - * Proposition : - * - GET /api/signal/recent?userId=...&limit=20 + * signalApi (align serveur + gateway, robust) + * ------------------------------------------ + * - Charge l'historique des signaux depuis strategy-service. + * - Normalise les champs (snake_case -> camelCase) pour coller à types/Signal.ts + * - Pas de mock. */ + +function asArray(x: any): any[] { + if (Array.isArray(x)) return x; + if (Array.isArray(x?.signals)) return x.signals; + if (Array.isArray(x?.events)) return x.events; + if (Array.isArray(x?.items)) return x.items; + return []; +} + +function safeAction(a: any): SignalAction { + const v = String(a ?? "HOLD").toUpperCase(); + if (v === "BUY" || v === "SELL" || v === "STOP_LOSS") return v as SignalAction; + return "HOLD"; +} + +function safeCriticality(c: any): SignalCriticality { + const v = String(c ?? "INFO").toUpperCase(); + if (v === "CRITICAL" || v === "WARNING") return v as SignalCriticality; + return "INFO"; +} + +function safeStatus(s: any): SignalStatus { + const v = String(s ?? "ACTIVE").toUpperCase(); + if (v === "SUPERSEDED" || v === "EXECUTED" || v === "INVALID") return v as SignalStatus; + return "ACTIVE"; +} + +function toNumber(x: any, fallback = 0): number { + const n = typeof x === "number" ? x : Number(x); + return Number.isFinite(n) ? n : fallback; +} + +function normalizeSignal(raw: any, idx: number): Signal { + // id + const id = + String( + raw?.signalId ?? + raw?.signal_id ?? + raw?.id ?? + raw?.event_id ?? + `sig_${Date.now()}_${idx}` + ); + + const pair = String(raw?.pair ?? raw?.pair_code ?? "BTC/EUR"); + + const timestamp = toNumber(raw?.timestamp ?? raw?.timestamp_ms ?? raw?.created_at_ms ?? Date.now(), Date.now()); + + const action = safeAction(raw?.action ?? raw?.decision); + const criticality = safeCriticality(raw?.criticality ?? raw?.alertLevel ?? raw?.level); + const status = safeStatus(raw?.status); + + const confidence = Math.max(0, Math.min(1, toNumber(raw?.confidence ?? 0, 0))); + + const reason = String(raw?.reason ?? raw?.message ?? "—"); + + const priceAtSignal = toNumber( + raw?.priceAtSignal ?? raw?.price_at_signal ?? raw?.price ?? raw?.current_price ?? 0, + 0 + ); + + return { + signalId: id, + pair, + timestamp, + action, + criticality, + status, + confidence, + reason, + priceAtSignal, + }; +} + export async function getRecentSignals(limit = 20): Promise { const session = await loadSession(); const userId = session?.userId; - if (!userId) throw new Error("Session absente : impossible de charger l'historique."); + if (!userId) throw new Error("Session absente."); - const data = await apiGet<{ signals: Signal[] }>( - `/signal/recent?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}` - ); + const candidates = [ + `/signal/recent?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`, + `/signal/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`, + `/signal/history?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`, + ]; + + let lastErr: unknown = null; + + for (const path of candidates) { + try { + const data = await apiGet(path); + const arr = asArray(data); + + // si route existe mais vide -> ok + if (arr.length === 0 && (data || data === null)) return []; + + return arr.map((x, i) => normalizeSignal(x, i)); + } catch (e) { + lastErr = e; + } + } - return data.signals ?? []; + throw lastErr ?? new Error("Impossible de charger les signaux."); } \ 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 d022938..336ab73 100644 --- a/Wallette/mobile/src/services/api/strategyApi.ts +++ b/Wallette/mobile/src/services/api/strategyApi.ts @@ -13,12 +13,12 @@ import type { StrategyKey } from "../../types/Strategy"; */ function pairToPairId(pair: string): number { - // Convention du projet : (MarketDataRepo mentionne pairId=1 pour BTC) const p = pair.trim().toUpperCase(); - if (p.startsWith("BTC/")) return 1; - if (p.startsWith("ETH/")) return 2; - if (p.startsWith("LTC/")) return 3; - throw new Error(`Pair non supportée (pas de pairId) : ${pair}`); + + if (p === "BTC/EUR") return 1; + if (p === "BTC/USDT") return 2; + + throw new Error(`Pair non supportée par la DB (pairs) : ${pair}`); } export async function selectStrategy(params: { diff --git a/Wallette/mobile/src/services/api/walletApi.ts b/Wallette/mobile/src/services/api/walletApi.ts index 2f1a4a2..9c96ca5 100644 --- a/Wallette/mobile/src/services/api/walletApi.ts +++ b/Wallette/mobile/src/services/api/walletApi.ts @@ -1,16 +1,21 @@ -import { apiGet } from "./http"; +import { apiGet, apiPost } from "./http"; import { loadSession } from "../../utils/sessionStorage"; import type { PortfolioState } from "../../models/Portfolio"; /** - * walletApi (contrat Stéphane - STRICT) - * ------------------------------------ + * walletApi (aligné wallet-service) + * -------------------------------- * - GET /api/wallets?userId=... + * -> { wallets:[...], count } * - GET /api/wallets/:id + * -> { wallet:{...} } * - GET /api/wallets/:id/events + * -> { events:[...], count } + * - POST /api/wallets/:id/events + * -> body.event_type obligatoire */ -type WalletListResponse = any; +type WalletListResponse = { wallets?: any[]; count?: number } | any; function pickWalletId(list: any): string | null { const arr = @@ -24,6 +29,7 @@ function pickWalletId(list: any): string | null { const first = arr[0]; return ( + (typeof first?.wallet_id === "string" && first.wallet_id) || (typeof first?.walletId === "string" && first.walletId) || (typeof first?.id === "string" && first.id) || (typeof first?._id === "string" && first._id) || @@ -31,57 +37,45 @@ function pickWalletId(list: any): string | null { ); } -function extractAssets(details: any): { symbol: string; quantity: number }[] { - 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)); - } - - const balances = details?.balances; - if (balances && typeof balances === "object") { - return Object.entries(balances) - .filter(([, qty]) => typeof qty === "number" && Number.isFinite(qty) && qty > 0) - .map(([symbol, qty]) => ({ symbol: String(symbol).toUpperCase(), quantity: Number(qty) })) - .sort((a, b) => a.symbol.localeCompare(b.symbol)); - } - - return []; -} - export async function getPrimaryWalletId(): Promise { const session = await loadSession(); const userId = session?.userId; if (!userId) throw new Error("Session absente : impossible de charger les wallets."); - const list = await apiGet(`/wallets?userId=${encodeURIComponent(userId)}`); - return pickWalletId(list); + const data = await apiGet(`/wallets?userId=${encodeURIComponent(userId)}`); + return pickWalletId(data); } export async function getPortfolio(): Promise { const walletId = await getPrimaryWalletId(); if (!walletId) return { assets: [], updatedAtMs: Date.now() }; - const details = await apiGet(`/wallets/${encodeURIComponent(walletId)}`); - const assets = extractAssets(details); + const data = await apiGet(`/wallets/${encodeURIComponent(walletId)}`); - return { assets, updatedAtMs: Date.now() }; + // wallet-service: { wallet: {...} } + const w = data?.wallet ?? data; + + const symbol = String(w?.asset_symbol ?? w?.assetSymbol ?? "BTC").toUpperCase(); + const qtyRaw = w?.quantity ?? "0"; + const qty = Number(qtyRaw); + + if (!Number.isFinite(qty) || qty <= 0) { + return { assets: [], updatedAtMs: Date.now() }; + } + + return { + assets: [{ symbol, quantity: qty }], + updatedAtMs: Date.now(), + }; } export type WalletEvent = { - id?: string; - type?: string; // BUY/SELL - symbol?: string; // BTC - quantity?: number; - price?: number; - timestamp?: number; + event_id?: string; + event_type?: string; // BUY/SELL/... + quantity_base?: string; // "0.0100000000" + price_quote?: string; // "42150.2300000000" + quote_symbol?: string; // "EUR" ou "USDT" + executed_at_ms?: number; }; export async function getWalletEvents(limit = 50): Promise { @@ -92,8 +86,18 @@ export async function getWalletEvents(limit = 50): Promise { `/wallets/${encodeURIComponent(walletId)}/events?limit=${encodeURIComponent(String(limit))}` ); - if (Array.isArray(data)) return data as WalletEvent[]; if (Array.isArray(data?.events)) return data.events as WalletEvent[]; if (Array.isArray(data?.items)) return data.items as WalletEvent[]; + if (Array.isArray(data)) return data as WalletEvent[]; return []; +} + +export async function addWalletEvent(params: { + walletId: string; + event_type: "BUY" | "SELL"; + quantity_base: string; // ex: "0.0100000000" + price_quote?: string; // requis pour BUY + quote_symbol?: "EUR" | "USDT"; // requis pour BUY +}): Promise { + await apiPost(`/wallets/${encodeURIComponent(params.walletId)}/events`, params); } \ No newline at end of file diff --git a/Wallette/mobile/src/types/DashboardSummary.ts b/Wallette/mobile/src/types/DashboardSummary.ts index 78af51b..13db616 100644 --- a/Wallette/mobile/src/types/DashboardSummary.ts +++ b/Wallette/mobile/src/types/DashboardSummary.ts @@ -1,16 +1,9 @@ -export type TradeDecision = - | "BUY" - | "SELL" - | "HOLD" - | "STOP_LOSS"; +export type TradeDecision = "BUY" | "SELL" | "HOLD" | "STOP_LOSS"; -export type AlertLevel = - | "CRITICAL" - | "WARNING" - | "INFO"; +export type AlertLevel = "CRITICAL" | "WARNING" | "INFO"; export interface DashboardSummary { - pair: string; + pair: string; // ex: BTC/EUR ou BTC/USDT price: number; strategy: string; decision: TradeDecision; @@ -18,5 +11,4 @@ export interface DashboardSummary { reason: string; alertLevel: AlertLevel; timestamp: number; -} - +} \ No newline at end of file diff --git a/Wallette/mobile/src/utils/settingsStorage.ts b/Wallette/mobile/src/utils/settingsStorage.ts index 1fe6b39..00cc2b9 100644 --- a/Wallette/mobile/src/utils/settingsStorage.ts +++ b/Wallette/mobile/src/utils/settingsStorage.ts @@ -2,9 +2,6 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import type { UserSettings } from "../models/UserSettings"; import { loadSession } from "./sessionStorage"; -/** - * KEY devient "userSettings:" - */ function keyFor(userId: string) { return `userSettings:${userId}`; } @@ -18,16 +15,30 @@ const DEFAULT_SETTINGS: UserSettings = { selectedStrategyKey: "RSI_SIMPLE", }; +function normalizeCurrency(input: any): UserSettings["currency"] { + const v = String(input ?? "").toUpperCase(); + + // Migration anciens formats + if (v === "USD") return "USDT"; + if (v === "USDT") return "USDT"; + return "EUR"; +} + export async function loadSettings(): Promise { const session = await loadSession(); - if (!session) return DEFAULT_SETTINGS; // sécurité (normalement, App bloque sans session) + if (!session) return DEFAULT_SETTINGS; const raw = await AsyncStorage.getItem(keyFor(session.userId)); if (!raw) return DEFAULT_SETTINGS; try { const parsed = JSON.parse(raw) as Partial; - return { ...DEFAULT_SETTINGS, ...parsed }; + + return { + ...DEFAULT_SETTINGS, + ...parsed, + currency: normalizeCurrency((parsed as any).currency), + }; } catch { return DEFAULT_SETTINGS; } @@ -37,5 +48,11 @@ export async function saveSettings(settings: UserSettings): Promise { const session = await loadSession(); if (!session) return; - await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(settings)); + // sécurité : on force la currency à un enum valide + const safe: UserSettings = { + ...settings, + currency: normalizeCurrency((settings as any).currency), + }; + + await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(safe)); } \ No newline at end of file