From: Thibaud Moustier Date: Sat, 28 Feb 2026 19:17:44 +0000 (+0100) Subject: Mobile : Refractor + modif X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=56eb94ddc60d9286e7ceae2b1f49aff4c855d490;p=pdw25-26 Mobile : Refractor + modif --- diff --git a/Wallette/mobile/src/config/api.ts b/Wallette/mobile/src/config/api.ts index 17e7418..15bff3d 100644 --- a/Wallette/mobile/src/config/api.ts +++ b/Wallette/mobile/src/config/api.ts @@ -1,32 +1,31 @@ import { Platform } from "react-native"; -import { API_BASE_URL, SERVER_URL, ENV_MODE, PLATFORM } from "./env"; /** - * gatewayUrls.ts (ou apiConfig.ts) - * ------------------------------- - * Source de vérité = env.ts + * env.ts + * ------ + * 1 seul point de config réseau. * - * - REST : API_BASE_URL (déjà contient /api) - * - Socket : SERVER_URL (gateway root, sans /api) + * IMPORTANT : + * - REST = https://wallette.duckdns.org/api + * - Socket.IO = https://wallette.duckdns.org (PAS /api) * - * NOTE : - * - En DEV, env.ts pointe vers ton gateway local (http://IP:3000) - * - En PROD, env.ts pointe vers duckdns (https://wallette.duckdns.org) + * Expo = __DEV__ => on peut forcer PROD pour tester le serveur déployé. */ +// ✅ PROD (duckdns) +const PROD_GATEWAY = "https://wallette.duckdns.org"; + +// ✅ DEV (pour test Expo sur tel) : force PROD +// Si tu veux revenir au local plus tard : remets ton IP LAN + http://IP:3000 +const DEV_GATEWAY = "https://wallette.duckdns.org"; + +export const GATEWAY_BASE_URL = __DEV__ ? DEV_GATEWAY : PROD_GATEWAY; + // REST (via gateway) -export const REST_BASE_URL = API_BASE_URL; +export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`; -// Socket.IO (via gateway) -export const SOCKET_BASE_URL = SERVER_URL; +// Socket.IO (via gateway) -> RACINE (pas /api) +export const SERVER_URL = GATEWAY_BASE_URL; -/** - * Debug helper : pratique pour afficher dans un screen "About / Debug" - */ -export const DEBUG_NETWORK_INFO = { - env: ENV_MODE, - platform: PLATFORM, - rest: REST_BASE_URL, - socket: SOCKET_BASE_URL, - rnPlatform: Platform.OS, -}; \ No newline at end of file +export const ENV_MODE = __DEV__ ? "DEV" : "PROD"; +export const PLATFORM = Platform.OS; \ No newline at end of file diff --git a/Wallette/mobile/src/config/env.ts b/Wallette/mobile/src/config/env.ts index bd7d115..15bff3d 100644 --- a/Wallette/mobile/src/config/env.ts +++ b/Wallette/mobile/src/config/env.ts @@ -3,38 +3,29 @@ import { Platform } from "react-native"; /** * env.ts * ------ - * Objectif : 1 seul point de config réseau. + * 1 seul point de config réseau. * - * DEV : IP LAN (PC qui fait tourner gateway en local) - * PROD : URL publique (duckdns / serveur) + * IMPORTANT : + * - REST = https://wallette.duckdns.org/api + * - Socket.IO = https://wallette.duckdns.org (PAS /api) * - * Le mobile parle UNIQUEMENT au Gateway : - * - REST : /api/... - * - Socket : (socket.io proxy via gateway) + * Expo = __DEV__ => on peut forcer PROD pour tester le serveur déployé. */ -// ✅ 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 (duckdns) const PROD_GATEWAY = "https://wallette.duckdns.org"; -/** - * Pour l'instant : - * - en Expo / dev => DEV_GATEWAY - * - en build (APK/IPA) => PROD_GATEWAY - */ +// ✅ DEV (pour test Expo sur tel) : force PROD +// Si tu veux revenir au local plus tard : remets ton IP LAN + http://IP:3000 +const DEV_GATEWAY = "https://wallette.duckdns.org"; + 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) +// Socket.IO (via gateway) -> RACINE (pas /api) 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/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index 6dde816..b0aab8d 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -43,11 +43,9 @@ const CRYPTOS: CryptoSymbol[] = ["BTC", "ETH", "LTC"]; function walletAddressKey(userId: string) { return `walletAddress:${userId}`; } - function normalizeSymbol(s: string) { return s.trim().toUpperCase(); } - function mergePortfolios(base: PortfolioState, patch: PortfolioState): PortfolioState { const map = new Map(); for (const a of base.assets) map.set(normalizeSymbol(a.symbol), a.quantity); @@ -73,19 +71,15 @@ export default function DashboardScreen() { const [selectedCrypto, setSelectedCrypto] = useState("BTC"); - // dashboard summary = signal + prix const [summary, setSummary] = useState(null); - // wallet address const [walletAddress, setWalletAddress] = useState(""); const [walletAddressInfo, setWalletAddressInfo] = useState(null); - // alerts (live) const [liveAlerts, setLiveAlerts] = useState([]); const [socketConnected, setSocketConnected] = useState(false); const [socketInfo, setSocketInfo] = useState(null); - // modal trade const [tradeOpen, setTradeOpen] = useState(false); const [tradeSide, setTradeSide] = useState("BUY"); const [tradeQty, setTradeQty] = useState("0.01"); @@ -118,11 +112,9 @@ export default function DashboardScreen() { const s = await loadSettings(); setSettings(s); - // local portfolio const localPortfolio = await loadPortfolio(); setPortfolio(localPortfolio); - // wallet address if (uid) { const addr = (await AsyncStorage.getItem(walletAddressKey(uid))) ?? ""; setWalletAddress(addr); @@ -130,18 +122,14 @@ export default function DashboardScreen() { setWalletAddress(""); } - // summary (signal+prix) -> API try { const dash = await getDashboardSummary(); setSummary(dash); } catch { setSummary(null); - setSoftError( - `Signal/Prix indisponibles (API). Vérifie si tu es en DEV: ${ENV_MODE}.` - ); + setSoftError(`Signal/Prix indisponibles (API). DEV=${ENV_MODE}. Base REST: ${API_BASE_URL}`); } - // wallet API (si dispo) -> fusion if (uid) { try { const apiPortfolio = await getPortfolioFromApi(); @@ -149,18 +137,17 @@ export default function DashboardScreen() { setPortfolio(merged); await savePortfolio(merged); } catch { - // pas bloquant + // non bloquant } } - // alert history REST (si dispo) if (uid) { try { const history = await getAlertHistory(10); for (const a of history) alertStore.add(a); setLiveAlerts(history.slice(0, 50)); } catch { - // pas bloquant + // non bloquant } } }, []); @@ -182,7 +169,7 @@ export default function DashboardScreen() { }, [refreshAll]) ); - // Socket live (non bloquant) + // ✅ FIX string|null : on connecte socket seulement si uid existe useEffect(() => { let unsub: null | (() => void) = null; let alive = true; @@ -192,7 +179,7 @@ export default function DashboardScreen() { setSocketConnected(false); const session = await loadSession(); - const uid = session?.userId; + const uid = session?.userId ?? null; if (!uid) { setSocketInfo("Socket désactivé : session absente."); @@ -275,9 +262,8 @@ export default function DashboardScreen() { let nextQty = currentQty; - if (tradeSide === "BUY") { - nextQty = currentQty + qty; - } else { + if (tradeSide === "BUY") nextQty = currentQty + qty; + else { if (qty > currentQty) { setTradeInfo(`Vente impossible : tu n'as que ${currentQty.toFixed(6)} ${symbol}.`); return; @@ -290,10 +276,7 @@ export default function DashboardScreen() { .concat(nextQty > 0 ? [{ symbol, quantity: nextQty }] : []) .sort((a, b) => normalizeSymbol(a.symbol).localeCompare(normalizeSymbol(b.symbol))); - const nextPortfolio: PortfolioState = { - assets: nextAssets, - updatedAtMs: Date.now(), - }; + const nextPortfolio: PortfolioState = { assets: nextAssets, updatedAtMs: Date.now() }; await savePortfolio(nextPortfolio); setPortfolio(nextPortfolio); @@ -317,13 +300,9 @@ export default function DashboardScreen() { {!!softError && ( {softError} - - Base REST : {API_BASE_URL} - )} - {/* Compte */} Compte utilisateur @@ -333,7 +312,6 @@ export default function DashboardScreen() { {!!socketInfo && {socketInfo}} - {/* Crypto + adresse */} Choisir une cryptomonnaie @@ -372,51 +350,27 @@ export default function DashboardScreen() { {!!walletAddressInfo && {walletAddressInfo}} - {/* Solde + Prix */} - Solde - - {selectedQty.toFixed(6)} {selectedCrypto} - - Portefeuille local (+ sync serveur si dispo) - - - - - Prix - {pair} - - - {(summary?.price ?? 0).toFixed(2)} {currency} - - - Maj : {summary?.timestamp ? new Date(summary.timestamp).toLocaleTimeString() : "—"} - - + Prix + {pair} + {(summary?.price ?? 0).toFixed(2)} {currency} void refreshAll()}> Actualiser - {/* Signal */} Signal du marché - {summary ? ( <> {summary.decision} - - {summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}% - - - {summary.reason} - + {summary.alertLevel} — Confiance {Math.round(summary.confidence * 100)}% + {summary.reason} ) : ( Aucune donnée pour le moment. )} - {/* Bonus : urgence live si dispo */} {urgentAlert && ( @@ -429,43 +383,30 @@ export default function DashboardScreen() { )} - {/* Stratégie */} - navigation.navigate("Strategy" as never)} - > + navigation.navigate("Strategy" as never)}> Stratégie Configurer - Sélection : {settings?.selectedStrategyKey ?? "—"} - {/* Actions */} Actions - openTrade("BUY")}> Acheter - openTrade("SELL")}> Vendre - - - Note : Acheter/Vendre = simulation (registre local). Pas de trading réel. - - {/* Modal BUY/SELL */} setTradeOpen(false)}> @@ -474,9 +415,7 @@ export default function DashboardScreen() { {tradeSide === "BUY" ? "Acheter" : "Vendre"} {selectedCrypto} - - Prix : {(summary?.price ?? 0).toFixed(2)} {currency} - + Prix : {(summary?.price ?? 0).toFixed(2)} {currency} Quantité { `/alerts/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}` ); - const events = Array.isArray(data?.events) ? data.events : []; - return events; + return Array.isArray(data?.events) ? (data!.events as Alert[]) : []; } export async function clearAlertsLocal(): Promise { diff --git a/Wallette/mobile/src/services/api/dashboardApi.ts b/Wallette/mobile/src/services/api/dashboardApi.ts index 4f4e64c..f37605f 100644 --- a/Wallette/mobile/src/services/api/dashboardApi.ts +++ b/Wallette/mobile/src/services/api/dashboardApi.ts @@ -1,107 +1,65 @@ import type { DashboardSummary, TradeDecision, AlertLevel } from "../../types/DashboardSummary"; -import { apiGet } from "./http"; +import type { Alert } from "../../types/Alert"; + import { loadSession } from "../../utils/sessionStorage"; import { loadSettings } from "../../utils/settingsStorage"; -/** - * dashboardApi (API-only) - * ----------------------- - * Construit un DashboardSummary via les endpoints gateway. - * - * Aligné Web pour le prix : - * - GET /api/prices/current?pair=BTC/EUR -> { price } - * - * Signal : - * - GET /api/signal/current?userId=...&pair=BTC/EUR - */ +import { getCurrentPrice } from "./priceApi"; +import { getAlertHistory } from "./alertsApi"; + +function safeDecision(action: any): TradeDecision { + const a = String(action ?? "HOLD").toUpperCase(); + if (a === "BUY" || a === "SELL" || a === "STOP_LOSS") return a as TradeDecision; + return "HOLD"; +} + +function safeLevel(level: any): AlertLevel { + const l = String(level ?? "INFO").toUpperCase(); + if (l === "CRITICAL" || l === "WARNING") return l as AlertLevel; + return "INFO"; +} + 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 = settings.currency === "USD" ? "USD" : "EUR"; - const pair = `BTC/${currency}`; - - // 1) Prix courant (aligné Web) - const priceRaw = await apiGet(`/prices/current?pair=${encodeURIComponent(pair)}`); + const currency: "EUR" | "USD" = settings.currency === "USD" ? "USD" : "EUR"; - const price = - (typeof priceRaw?.price === "number" ? priceRaw.price : null) ?? - (typeof priceRaw?.current_price === "number" ? priceRaw.current_price : null) ?? - (typeof priceRaw?.data?.price === "number" ? priceRaw.data.price : null) ?? - (typeof priceRaw?.data?.current_price === "number" ? priceRaw.data.current_price : null); - - if (typeof price !== "number" || !Number.isFinite(price)) { - throw new Error("Prix invalide (API)."); - } - - const tsPrice = - (typeof priceRaw?.timestampMs === "number" ? priceRaw.timestampMs : null) ?? - (typeof priceRaw?.timestamp_ms === "number" ? priceRaw.timestamp_ms : null) ?? - Date.now(); - - // 2) Signal courant (API) - const signalRaw = await apiGet( - `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}` - ); - - // action (défaut HOLD) - const actionStr = String( - signalRaw?.action ?? - signalRaw?.data?.action ?? - "HOLD" - ).toUpperCase(); - - const decision: TradeDecision = - actionStr === "BUY" || actionStr === "SELL" || actionStr === "STOP_LOSS" - ? (actionStr as TradeDecision) - : "HOLD"; - - // alertLevel (défaut INFO) - const lvlStr = String( - signalRaw?.alertLevel ?? - signalRaw?.criticality ?? - signalRaw?.data?.alertLevel ?? - signalRaw?.data?.criticality ?? - "INFO" - ).toUpperCase(); + const pair = `BTC/${currency}`; - const alertLevel: AlertLevel = - lvlStr === "CRITICAL" || lvlStr === "WARNING" - ? (lvlStr as AlertLevel) - : "INFO"; + const price = await getCurrentPrice(pair); - const confidence = - typeof signalRaw?.confidence === "number" - ? signalRaw.confidence - : typeof signalRaw?.data?.confidence === "number" - ? signalRaw.data.confidence - : 0; + let decision: TradeDecision = "HOLD"; + let alertLevel: AlertLevel = "INFO"; + let confidence = 0; + let reason = "Aucune alerte récente."; + let timestamp = price.timestampMs; - const reason = String( - signalRaw?.reason ?? - signalRaw?.message ?? - signalRaw?.data?.reason ?? - signalRaw?.data?.message ?? - "—" - ); + try { + const events = await getAlertHistory(10); + const last: Alert | undefined = events[0]; - const tsSignal = - (typeof signalRaw?.timestamp === "number" ? signalRaw.timestamp : null) ?? - (typeof signalRaw?.timestamp_ms === "number" ? signalRaw.timestamp_ms : null) ?? - (typeof signalRaw?.data?.timestamp === "number" ? signalRaw.data.timestamp : null) ?? - (typeof signalRaw?.data?.timestamp_ms === "number" ? signalRaw.data.timestamp_ms : null) ?? - Number(tsPrice); + if (last) { + decision = safeDecision(last.action); + alertLevel = safeLevel(last.alertLevel); + confidence = typeof last.confidence === "number" ? last.confidence : 0; + reason = String(last.reason ?? last.message ?? "—"); + timestamp = typeof last.timestamp === "number" ? last.timestamp : timestamp; + } + } catch { + // non bloquant + } return { pair, - price: Number(price), + price: price.price, strategy: settings.selectedStrategyKey, decision, - confidence: Number(confidence), + confidence, reason, alertLevel, - timestamp: Number(tsSignal), + timestamp, }; } \ No newline at end of file diff --git a/Wallette/mobile/src/services/api/http.ts b/Wallette/mobile/src/services/api/http.ts index a699996..6814903 100644 --- a/Wallette/mobile/src/services/api/http.ts +++ b/Wallette/mobile/src/services/api/http.ts @@ -5,7 +5,7 @@ import { API_BASE_URL } from "../../config/env"; * ------------------- * Compatible avec 2 formats : * A) "wrap" : { ok:true, data: ... } / { ok:false, error:{message} } - * B) "raw" : { price: ... } / { balance: ... } / Alert[] + * B) "raw" : { ... } / Alert[] etc. */ async function parseJsonSafe(res: Response) { @@ -23,14 +23,11 @@ function buildUrl(path: string) { } function unwrapOrRaw(json: any): T { - // Format "wrap" if (json && typeof json === "object" && "ok" in json) { if (json.ok === true) return json.data as T; const msg = json?.error?.message ?? "Réponse API invalide (ok=false)"; throw new Error(msg); } - - // Format "raw" return json as T; } diff --git a/Wallette/mobile/src/services/api/priceApi.ts b/Wallette/mobile/src/services/api/priceApi.ts index 9fbf20b..c91a378 100644 --- a/Wallette/mobile/src/services/api/priceApi.ts +++ b/Wallette/mobile/src/services/api/priceApi.ts @@ -1,23 +1,5 @@ import { apiGet } from "./http"; -/** - * priceApi (aligné serveur déployé) - * --------------------------------- - * Route dispo : - * - GET /api/price/current?pair=BTC/EUR - * - * Réponse : - * { - * "ok": true, - * "data": { - * "pair": "BTC/EUR", - * "current_price": 42150.23, - * "timestamp_ms": 1700000000000, - * "source": "..." - * } - * } - */ - export type PriceCurrent = { pair: string; timestampMs: number; diff --git a/Wallette/mobile/src/services/api/walletApi.ts b/Wallette/mobile/src/services/api/walletApi.ts index dbb11e2..7cf5c7d 100644 --- a/Wallette/mobile/src/services/api/walletApi.ts +++ b/Wallette/mobile/src/services/api/walletApi.ts @@ -2,41 +2,19 @@ import { apiGet } from "./http"; import { loadSession } from "../../utils/sessionStorage"; import type { PortfolioState } from "../../models/Portfolio"; -/** - * walletApi (aligné serveur déployé) - * --------------------------------- - * Routes dispo : - * - GET /api/wallets?userId=... - * - GET /api/wallets/:walletId - * - GET /api/wallets/:walletId/events - * - * Objectif côté mobile (pour l’instant) : - * - Récupérer un portefeuille (assets + quantités) si possible. - * - * Si le backend ne fournit pas (encore) un format multi-assets clair, - * on renvoie un portefeuille vide plutôt que casser l’app. - */ - -type WalletListItem = { - id?: string; - walletId?: string; - _id?: string; -}; - -type WalletListResponse = - | { wallets?: WalletListItem[] } - | { items?: WalletListItem[] } - | WalletListItem[] - | any; - -type WalletDetailsResponse = any; +type WalletListItem = { id?: string; walletId?: string; _id?: string }; +type WalletListResponse = any; function pickWalletId(list: any): string | null { - // cas 1: data = array - const arr = Array.isArray(list) ? list : Array.isArray(list?.wallets) ? list.wallets : Array.isArray(list?.items) ? list.items : null; - if (!arr || arr.length === 0) return null; + const arr = + Array.isArray(list) ? list : + Array.isArray(list?.wallets) ? list.wallets : + Array.isArray(list?.items) ? list.items : + null; + if (!arr || arr.length === 0) return null; const first = arr[0]; + return ( (typeof first?.walletId === "string" && first.walletId) || (typeof first?.id === "string" && first.id) || @@ -45,11 +23,7 @@ function pickWalletId(list: any): string | null { ); } -function extractAssetsFromWalletDetails(details: any): { symbol: string; quantity: number }[] { - // On tente plusieurs formats possibles : - // - details.portfolio.assets - // - details.assets - // - details.balances { BTC: 0.1, ETH: 2 } +function extractAssets(details: any): { symbol: string; quantity: number }[] { const assetsArr = (Array.isArray(details?.portfolio?.assets) && details.portfolio.assets) || (Array.isArray(details?.assets) && details.assets) || @@ -71,7 +45,6 @@ function extractAssetsFromWalletDetails(details: any): { symbol: string; quantit .sort((a, b) => a.symbol.localeCompare(b.symbol)); } - // fallback : rien exploitable return []; } @@ -80,22 +53,13 @@ export async function getPortfolio(): Promise { const userId = session?.userId; if (!userId) throw new Error("Session absente : impossible de charger le portefeuille."); - // 1) récupérer la liste des wallets de l’utilisateur const list = await apiGet(`/wallets?userId=${encodeURIComponent(userId)}`); const walletId = pickWalletId(list); - if (!walletId) { - // Aucun wallet côté serveur : on renvoie vide (l’app reste stable) - return { assets: [], updatedAtMs: Date.now() }; - } - - // 2) récupérer le wallet détail - const details = await apiGet(`/wallets/${encodeURIComponent(walletId)}`); + if (!walletId) return { assets: [], updatedAtMs: Date.now() }; - const assets = extractAssetsFromWalletDetails(details); + const details = await apiGet(`/wallets/${encodeURIComponent(walletId)}`); + const assets = extractAssets(details); - return { - assets, - updatedAtMs: Date.now(), - }; + return { assets, updatedAtMs: Date.now() }; } \ No newline at end of file diff --git a/Wallette/mobile/src/services/socketService.ts b/Wallette/mobile/src/services/socketService.ts index 4d93f28..76d9fd7 100644 --- a/Wallette/mobile/src/services/socketService.ts +++ b/Wallette/mobile/src/services/socketService.ts @@ -1,19 +1,6 @@ import { io, Socket } from "socket.io-client"; import type { Alert } from "../types/Alert"; -/** - * socketService.ts - * ---------------- - * Objectif : - * - Connexion Socket.IO via le Gateway (duckdns ou local) - * - Auth par event "auth" (payload = userId) - * - Réception des alertes via event "alert" - * - * Important : - * - Le Gateway doit proxy /socket.io/* vers alerts-service. - * - En prod (https), socket.io bascule en wss automatiquement. - */ - class SocketService { private socket: Socket | null = null; private listeners = new Set<(alert: Alert) => void>(); @@ -24,7 +11,6 @@ class SocketService { if (!serverUrl) throw new Error("serverUrl is required"); if (!userId) throw new Error("userId is required"); - // Si on est déjà connecté au même serveur avec le même userId -> rien à faire if ( this.socket && this.socket.connected && @@ -34,65 +20,73 @@ class SocketService { return; } - // Si on change de serveur ou de userId, on repart proprement - if (this.socket) { - this.disconnect(); - } + if (this.socket) this.disconnect(); this.currentServerUrl = serverUrl; this.currentUserId = userId; - this.socket = io(serverUrl, { - // Très important en environnement proxy - path: "/socket.io", + const attachHandlers = (sock: Socket) => { + const emitAuth = () => sock.emit("auth", userId); - // Mobile : websocket + fallback polling - transports: ["websocket", "polling"], + sock.on("connect", () => { + console.log("✅ Socket connecté:", sock.id); + emitAuth(); + }); - reconnection: true, - reconnectionAttempts: 10, - reconnectionDelay: 800, - reconnectionDelayMax: 3000, - timeout: 10000, - }); - - const emitAuth = () => { - if (!this.socket || !this.currentUserId) return; - this.socket.emit("auth", this.currentUserId); - }; + sock.on("auth_success", (data: any) => { + console.log("✅ Auth success:", data?.message ?? data); + }); - this.socket.on("connect", () => { - console.log("✅ Socket connecté:", this.socket?.id); - emitAuth(); - }); + sock.on("alert", (alert: Alert) => { + for (const cb of this.listeners) cb(alert); + }); - // Si le serveur supporte le message - this.socket.on("auth_success", (data: any) => { - console.log("✅ Auth success:", data?.message ?? data); - }); + sock.on("disconnect", (reason: string) => { + console.log("⚠️ Socket disconnect:", reason); + }); - // Alertes live - this.socket.on("alert", (alert: Alert) => { - for (const cb of this.listeners) cb(alert); - }); - - // Debug utile - this.socket.on("connect_error", (err: any) => { - console.log("❌ Socket connect_error:", err?.message ?? err); - }); - - this.socket.on("error", (err: any) => { - console.log("❌ Socket error:", err?.message ?? err); - }); + sock.on("error", (err: any) => { + console.log("❌ Socket error:", err?.message ?? err); + }); + }; - this.socket.on("reconnect", (attempt: number) => { - console.log("🔁 Socket reconnect:", attempt); - // Après reconnexion, on renvoie auth (certains backends oublient la session socket) - emitAuth(); + // tentative websocket + polling + const sock = io(serverUrl, { + path: "/socket.io", + transports: ["websocket", "polling"], + reconnection: true, + reconnectionAttempts: 10, + timeout: 10000, }); - this.socket.on("disconnect", (reason: string) => { - console.log("⚠️ Socket disconnect:", reason); + this.socket = sock; + attachHandlers(sock); + + sock.on("connect_error", (err: any) => { + const msg = String(err?.message ?? err); + console.log("❌ Socket connect_error:", msg); + + // fallback polling-only si websocket échoue + if (msg.toLowerCase().includes("websocket")) { + console.log("↩️ Fallback: polling-only"); + this.disconnect(); + + const sockPolling = io(serverUrl, { + path: "/socket.io", + transports: ["polling"], + upgrade: false, + reconnection: true, + reconnectionAttempts: 10, + timeout: 10000, + }); + + this.socket = sockPolling; + attachHandlers(sockPolling); + + sockPolling.on("connect_error", (e: any) => { + console.log("❌ Socket polling connect_error:", e?.message ?? e); + }); + } }); } @@ -105,11 +99,6 @@ class SocketService { this.socket?.emit("ping_alerts"); } - onPong(callback: (data: any) => void) { - this.socket?.on("pong_alerts", callback); - return () => this.socket?.off("pong_alerts", callback); - } - disconnect() { if (!this.socket) return; this.socket.removeAllListeners(); diff --git a/Wallette/mobile/src/types/Alert.ts b/Wallette/mobile/src/types/Alert.ts index ac89884..d1b7caa 100644 --- a/Wallette/mobile/src/types/Alert.ts +++ b/Wallette/mobile/src/types/Alert.ts @@ -1,28 +1,16 @@ -/** - * Alert.ts - * -------- - * Format d'alerte utilisé par : - * - Socket.IO (event "alert") - * - REST history : GET /api/alerts/history?userId=... - * - * Le Web utilise parfois `alert.message`. - * Donc on rend ce champ optionnel (pour compatibilité). - */ - export type AlertLevel = "CRITICAL" | "WARNING" | "INFO"; export type AlertAction = "BUY" | "SELL" | "HOLD" | "STOP_LOSS"; export interface Alert { - action?: AlertAction; // BUY / SELL / HOLD / STOP_LOSS - pair?: string; // ex: BTC/EUR - confidence?: number; // 0..1 - reason?: string; // texte explicatif + action?: AlertAction; + pair?: string; + confidence?: number; + reason?: string; - // ✅ Compatibilité Web (script.js affiche alert.message si présent) + // compat web message?: string; - alertLevel?: AlertLevel; // CRITICAL / WARNING / INFO - timestamp?: number; // ms - - price?: number; // prix au moment de l’alerte + alertLevel?: AlertLevel; + timestamp?: number; + price?: number; } \ No newline at end of file