From ba55485bd7e6d95211f4bc5b21ab174f1d742e96 Mon Sep 17 00:00:00 2001 From: Thibaud Moustier Date: Mon, 2 Mar 2026 09:18:28 +0100 Subject: [PATCH] Mobile : Mis a jour app - Fonctionnel --- Wallette/mobile/App.tsx | 142 +++-------- .../mobile/src/components/AccountMenu.tsx | 107 ++++---- Wallette/mobile/src/config/env.ts | 2 +- Wallette/mobile/src/screens/AccountScreen.tsx | 189 -------------- Wallette/mobile/src/screens/AuthScreen.tsx | 233 ------------------ .../mobile/src/screens/DashboardScreen.tsx | 22 +- Wallette/mobile/src/services/api/authApi.ts | 85 ------- .../mobile/src/services/api/dashboardApi.ts | 63 +++-- Wallette/mobile/src/services/api/http.ts | 12 +- Wallette/mobile/src/services/api/priceApi.ts | 23 +- .../src/services/notificationService.ts | 68 +++-- Wallette/mobile/src/services/socketService.ts | 80 ++---- .../mobile/src/services/strategyService.ts | 82 ++++-- 13 files changed, 273 insertions(+), 835 deletions(-) delete mode 100644 Wallette/mobile/src/screens/AccountScreen.tsx delete mode 100644 Wallette/mobile/src/screens/AuthScreen.tsx delete mode 100644 Wallette/mobile/src/services/api/authApi.ts diff --git a/Wallette/mobile/App.tsx b/Wallette/mobile/App.tsx index 8645062..0cb5c4a 100644 --- a/Wallette/mobile/App.tsx +++ b/Wallette/mobile/App.tsx @@ -3,7 +3,7 @@ import { createNavigationContainerRef, } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { TouchableOpacity, View, Text, Alert as RNAlert } from "react-native"; +import { TouchableOpacity, View, Text } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useEffect, useState } from "react"; @@ -14,17 +14,13 @@ import AlertsScreen from "./src/screens/AlertsScreen"; import StrategyScreen from "./src/screens/StrategyScreen"; import WalletScreen from "./src/screens/WalletScreen"; -import AuthScreen from "./src/screens/AuthScreen"; import TutorialScreen from "./src/screens/TutorialScreen"; - -import AccountScreen from "./src/screens/AccountScreen"; import AboutScreen from "./src/screens/AboutScreen"; import AccountMenu from "./src/components/AccountMenu"; -import { loadSession } from "./src/utils/sessionStorage"; +import { loadSession, saveSession, type Session } from "./src/utils/sessionStorage"; import { hasSeenTutorial } from "./src/utils/tutorialStorage"; -import { logout as authLogout } from "./src/services/api/authApi"; export type RootStackParamList = { Dashboard: undefined; @@ -33,74 +29,60 @@ export type RootStackParamList = { Alerts: undefined; Strategy: undefined; Wallet: undefined; - Account: undefined; About: undefined; }; const Stack = createNativeStackNavigator(); - -// ✅ navigationRef (navigate depuis le Modal AccountMenu) export const navigationRef = createNavigationContainerRef(); +const SERVER_USER_ID = "user-123"; + export default function App() { const [ready, setReady] = useState(false); - // Auth state - const [isAuthed, setIsAuthed] = useState(false); - const [sessionEmail, setSessionEmail] = useState("—"); + // affichage simple dans le menu + const [sessionLabel, setSessionLabel] = useState(SERVER_USER_ID); - // Tutorial gate + // Tutoriel const [needsTutorial, setNeedsTutorial] = useState(false); - // Account menu + // Menu const [menuVisible, setMenuVisible] = useState(false); useEffect(() => { let active = true; async function init() { + // ✅ Session fixe (pas d’auth) const s = await loadSession(); if (!active) return; - const authed = !!s; - setIsAuthed(authed); - setSessionEmail(s?.email ?? "—"); - - if (authed) { - const seen = await hasSeenTutorial(); + if (!s) { + const session: Session = { + userId: SERVER_USER_ID, + email: SERVER_USER_ID, + createdAtMs: Date.now(), + }; + await saveSession(session); if (!active) return; - setNeedsTutorial(!seen); + setSessionLabel(session.userId); } else { - setNeedsTutorial(false); + setSessionLabel(s.userId ?? SERVER_USER_ID); } + const seen = await hasSeenTutorial(); + if (!active) return; + setNeedsTutorial(!seen); + setReady(true); } void init(); - return () => { active = false; }; }, []); - const doLogout = () => { - RNAlert.alert("Déconnexion", "Se déconnecter du compte ?", [ - { text: "Annuler", style: "cancel" }, - { - text: "Déconnexion", - style: "destructive", - onPress: async () => { - await authLogout(); - setIsAuthed(false); - setSessionEmail("—"); - setNeedsTutorial(false); - setMenuVisible(false); - }, - }, - ]); - }; - const go = (route: keyof RootStackParamList) => { if (!navigationRef.isReady()) return; navigationRef.navigate(route); @@ -114,24 +96,7 @@ export default function App() { ); } - // 1) Pas connecté -> Auth (hors stack) - if (!isAuthed) { - return ( - { - const s = await loadSession(); - setSessionEmail(s?.email ?? "—"); - setIsAuthed(true); - - // Après login, on vérifie le tutoriel - const seen = await hasSeenTutorial(); - setNeedsTutorial(!seen); - }} - /> - ); - } - - // 2) Connecté mais tuto pas vu -> Tutoriel (hors stack) + // Tutoriel au premier lancement if (needsTutorial) { return ( App normale return ( setMenuVisible(false)} - onGoAccount={() => go("Account")} onGoAbout={() => go("About")} - onLogout={doLogout} /> @@ -162,72 +124,36 @@ export default function App() { title: "Dashboard", headerRight: () => ( - {/* 👤 Menu Compte */} + {/* Menu */} setMenuVisible(true)}> - + - {/* ⚙️ Paramètres */} - navigation.navigate("Settings")} - > - + {/* Paramètres */} + navigation.navigate("Settings")}> + ), })} /> - - - - + + + + {() => ( { - // Force le tuto à s'afficher hors stack setNeedsTutorial(true); }} /> )} - - + ); diff --git a/Wallette/mobile/src/components/AccountMenu.tsx b/Wallette/mobile/src/components/AccountMenu.tsx index 2be54cb..ce5841d 100644 --- a/Wallette/mobile/src/components/AccountMenu.tsx +++ b/Wallette/mobile/src/components/AccountMenu.tsx @@ -1,97 +1,80 @@ -import { Modal, View, Text, StyleSheet, TouchableOpacity, Pressable } from "react-native"; +import { Modal, View, Text, StyleSheet, TouchableOpacity } from "react-native"; import { Ionicons } from "@expo/vector-icons"; -import { ui } from "./ui/uiStyles"; type Props = { visible: boolean; - email: string; + label: string; // ex: user-123 onClose: () => void; - - onGoAccount: () => void; onGoAbout: () => void; - onLogout: () => void; }; -export default function AccountMenu({ - visible, - email, - onClose, - onGoAccount, - onGoAbout, - onLogout, -}: Props) { +export default function AccountMenu({ visible, label, onClose, onGoAbout }: Props) { return ( - - {/* Overlay */} - - {/* Stop propagation */} - null}> + + + - - - Compte - {email} + + + {label} + - { onClose(); onGoAccount(); }}> - - Modification du compte - - - - { onClose(); onGoAbout(); }}> - + { + onClose(); + onGoAbout(); + }} + > + À propos - - { onClose(); onLogout(); }}> - - Déconnexion + + + Fermer - - + + ); } const styles = StyleSheet.create({ - overlay: { + backdrop: { flex: 1, - backgroundColor: "rgba(15, 23, 42, 0.35)", - justifyContent: "flex-start", - paddingTop: 70, - paddingHorizontal: 16, + backgroundColor: "rgba(0,0,0,0.35)", + justifyContent: "center", + padding: 16, }, - card: { backgroundColor: "#fff", - borderRadius: 14, + borderRadius: 16, + padding: 14, borderWidth: 1, borderColor: "#e5e7eb", - padding: 12, }, - headerRow: { flexDirection: "row", + justifyContent: "space-between", alignItems: "center", - gap: 10, - paddingBottom: 10, - borderBottomWidth: 1, - borderBottomColor: "#e5e7eb", marginBottom: 10, }, - - title: { - fontSize: 16, + userRow: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + label: { fontWeight: "900", color: "#0f172a", + opacity: 0.9, }, - item: { flexDirection: "row", alignItems: "center", @@ -99,21 +82,17 @@ const styles = StyleSheet.create({ paddingVertical: 12, paddingHorizontal: 10, borderRadius: 12, + borderWidth: 1, + borderColor: "#e5e7eb", + backgroundColor: "#fff", + marginTop: 10, }, - - itemDanger: { - marginTop: 6, - backgroundColor: "#dc262611", - }, - - itemIcon: { - width: 22, + itemSecondary: { opacity: 0.9, }, - itemText: { - flex: 1, fontWeight: "900", color: "#0f172a", + opacity: 0.85, }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/config/env.ts b/Wallette/mobile/src/config/env.ts index 859dbcf..a41f826 100644 --- a/Wallette/mobile/src/config/env.ts +++ b/Wallette/mobile/src/config/env.ts @@ -13,7 +13,7 @@ const PROD_GATEWAY = "https://wallette.duckdns.org"; export const GATEWAY_BASE_URL = PROD_GATEWAY; // REST (via gateway) -export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`; +export const API_BASE_URL = "https://wallette.duckdns.org/api"; // Socket.IO (via gateway) export const SERVER_URL = GATEWAY_BASE_URL; diff --git a/Wallette/mobile/src/screens/AccountScreen.tsx b/Wallette/mobile/src/screens/AccountScreen.tsx deleted file mode 100644 index d7be829..0000000 --- a/Wallette/mobile/src/screens/AccountScreen.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native"; -import { useEffect, useState } from "react"; -import { SafeAreaView } from "react-native-safe-area-context"; - -import { ui } from "../components/ui/uiStyles"; -import { loadSession } from "../utils/sessionStorage"; -import { findUserById, updateCurrentUserProfile } from "../utils/authUsersStorage"; - -/** - * AccountScreen (Step 4 - sans API) - * -------------------------------- - * - Email : affiché mais NON modifiable (important) - * - Username : affiché mais NON modifiable (sinon userId local casse) - * - DisplayName : modifiable - * - Mot de passe : modifiable (hash local) - * - * Plus tard : remplacement par une route API (DB). - */ -export default function AccountScreen() { - const [loading, setLoading] = useState(true); - - const [userId, setUserId] = useState(""); - - const [email, setEmail] = useState(""); // affichage only - const [username, setUsername] = useState(""); // affichage only - const [displayName, setDisplayName] = useState(""); - - const [password, setPassword] = useState(""); - const [password2, setPassword2] = useState(""); - - const [info, setInfo] = useState(null); - const [error, setError] = useState(null); - - useEffect(() => { - let active = true; - - async function init() { - const session = await loadSession(); - if (!session) { - if (!active) return; - setError("Session absente."); - setLoading(false); - return; - } - - const u = await findUserById(session.userId); - - if (!active) return; - - setUserId(session.userId); - setEmail(u?.email ?? session.email); - setUsername(u?.username ?? session.userId.replace("user_", "")); - setDisplayName(u?.displayName ?? ""); - - setLoading(false); - } - - init(); - - return () => { - active = false; - }; - }, []); - - if (loading) { - return ( - - Chargement du compte… - - ); - } - - const handleSave = async () => { - setInfo(null); - setError(null); - - // Mot de passe : optionnel, mais si rempli => validations - const wantsPasswordChange = password.trim().length > 0 || password2.trim().length > 0; - - if (wantsPasswordChange) { - if (password.trim().length < 6) { - setError("Mot de passe trop court (min 6)."); - return; - } - if (password !== password2) { - setError("Les mots de passe ne correspondent pas."); - return; - } - } - - const res = await updateCurrentUserProfile({ - displayName, - newPassword: wantsPasswordChange ? password : undefined, - }); - - if (!res.ok) { - setError(res.message); - return; - } - - // reset password fields - setPassword(""); - setPassword2(""); - - setInfo("Profil sauvegardé ✅ (local, en attendant l’API)."); - }; - - return ( - - - Modification du compte - - - Informations - - UserId - {userId || "—"} - - Email (non modifiable) - - - Nom d’utilisateur (non modifiable) - - - Nom affiché - - - - ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation). - - - - - Mot de passe - - Change optionnel. (Local pour l’instant, sera relié à l’API plus tard.) - - - Nouveau mot de passe - - - Confirmation - - - - {!!error && {error}} - {!!info && {info}} - - - Sauvegarder - - - - ); -} - -const styles = StyleSheet.create({ - safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor }, - - title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" }, - - mono: { - marginTop: 6, - fontFamily: "monospace", - color: "#0f172a", - opacity: 0.8, - }, - - input: { - borderWidth: 1, - borderColor: "#e5e7eb", - borderRadius: 10, - paddingHorizontal: 12, - paddingVertical: 10, - marginTop: 8, - backgroundColor: "#fff", - color: "#0f172a", - }, - - inputDisabled: { - backgroundColor: "#f3f4f6", - color: "#111827", - opacity: 0.8, - }, - - fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 }, - - errorText: { color: "#dc2626", fontWeight: "900", marginBottom: 10 }, -}); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/AuthScreen.tsx b/Wallette/mobile/src/screens/AuthScreen.tsx deleted file mode 100644 index 30620ba..0000000 --- a/Wallette/mobile/src/screens/AuthScreen.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { useMemo, useState } from "react"; - -import { ui } from "../components/ui/uiStyles"; -import { login as authLogin, register as authRegister } from "../services/api/authApi"; - -/** - * AuthScreen - * ---------- - * Connexion + Création de compte. - * - Identifiant : email OU nom d’utilisateur - * - Mot de passe : minimum 6 caractères - * - * 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(""); - const [password, setPassword] = useState(""); - - // Création compte - const [email, setEmail] = useState(""); - const [username, setUsername] = useState(""); - const [displayName, setDisplayName] = useState(""); - const [password2, setPassword2] = useState(""); - - const isValidEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val.trim().toLowerCase()); - const isValidUsername = (val: string) => /^[a-zA-Z0-9_]{3,20}$/.test(val.trim()); - - const title = useMemo(() => (mode === "login" ? "Connexion" : "Créer un compte"), [mode]); - - const handleLogin = async () => { - setError(null); - - const l = login.trim(); - 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 (minimum 6 caractères)."); - - const res = await authLogin({ login: l, password: p }); - if (!res.ok) return setError(res.message); - - onAuthenticated(); - }; - - const handleRegister = async () => { - setError(null); - - const e = email.trim().toLowerCase(); - const u = username.trim(); - const d = displayName.trim(); - const p1 = password; - 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 (minimum 6 caractères)."); - if (p1 !== p2) return setError("Les mots de passe ne correspondent pas."); - - const res = await authRegister({ - email: e, - username: u, - displayName: d || undefined, - password: p1, - }); - - if (!res.ok) return setError(res.message); - - onAuthenticated(); - }; - - return ( - - - {title} - - {/* Onglets */} - - { - setMode("login"); - setError(null); - }} - > - Connexion - - - { - setMode("register"); - setError(null); - }} - > - Créer un compte - - - - - {mode === "login" ? ( - <> - Email ou nom d’utilisateur - - - Mot de passe - - - {!!error && {error}} - - - Se connecter - - - ) : ( - <> - Email - - - Nom d’utilisateur - - - Nom / prénom (optionnel) - - - Mot de passe - - - Confirmer le mot de passe - - - {!!error && {error}} - - - Créer le compte - - - )} - - - - ); -} - -const styles = StyleSheet.create({ - safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor }, - - title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" }, - - tabsRow: { - flexDirection: "row", - gap: 10, - marginBottom: 12, - }, - tab: { - flex: 1, - paddingVertical: 10, - borderRadius: 12, - borderWidth: 1, - borderColor: "#e5e7eb", - backgroundColor: "#fff", - alignItems: "center", - }, - tabActive: { - borderColor: "#0f172a", - }, - tabText: { fontWeight: "900", color: "#0f172a", opacity: 0.7 }, - tabTextActive: { opacity: 1 }, - - 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 }, - - errorText: { marginTop: 10, color: "#dc2626", fontWeight: "900" }, -}); \ No newline at end of file diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index bfb21ac..ef2a04a 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -138,12 +138,20 @@ export default function DashboardScreen() { // Dashboard summary (prix + signal) => dépend du pair BTC/EUR ou BTC/USDT try { - const dash = await getDashboardSummary(); - setSummary(dash); - } catch { - setSummary(null); - setSoftError("Données indisponibles pour le moment. Réessayez dans quelques secondes."); - } + const dash = await getDashboardSummary(); + setSummary(dash); +} catch (e: any) { + setSummary(null); + + const msg = String(e?.message ?? ""); + if (msg.includes("401")) { + setSoftError("Accès refusé. Vérifiez l’accès protégé du serveur."); + } else if (msg.toLowerCase().includes("prix")) { + setSoftError(msg); + } else { + setSoftError("Signal indisponible pour le moment."); + } +} // Wallet API (source de vérité) -> cache local if (uid) { @@ -389,7 +397,7 @@ export default function DashboardScreen() { {displayPair} - {(summary?.price ?? 0).toFixed(2)} {quote} + {summary ? `${summary.price.toFixed(2)} ${quote}` : "—"} {/* Info transparente : prix/signal serveur sont basés sur BTC (DB) */} diff --git a/Wallette/mobile/src/services/api/authApi.ts b/Wallette/mobile/src/services/api/authApi.ts deleted file mode 100644 index 7da2921..0000000 --- a/Wallette/mobile/src/services/api/authApi.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { AuthUser } from "../../models/AuthUser"; -import type { Session } from "../../utils/sessionStorage"; - -import { createUser, verifyLogin, findUserById } from "../../utils/authUsersStorage"; -import { loadSession, saveSession, clearSession } from "../../utils/sessionStorage"; - -/** - * authApi.ts - * ---------- - * Décision projet : PAS d'auth serveur. - * - * 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. - * - * 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 }; - -export async function register(params: { - email: string; - username: string; - displayName?: string; - password: string; -}): Promise { - const res = await createUser(params); - if (!res.ok) return { ok: false, message: res.message }; - - // ✅ userId aligné serveur - const session: Session = { - userId: SERVER_USER_ID, - email: res.user.email, - createdAtMs: Date.now(), - }; - - await saveSession(session); - - return { ok: true, user: res.user, session }; -} - -export async function login(params: { - login: string; // email OU username - password: string; -}): Promise { - const res = await verifyLogin(params); - if (!res.ok) return { ok: false, message: res.message }; - - // ✅ userId aligné serveur - const session: Session = { - userId: SERVER_USER_ID, - email: res.user.email, - createdAtMs: Date.now(), - }; - - await saveSession(session); - - return { ok: true, user: res.user, session }; -} - -export async function logout(): Promise { - await clearSession(); -} - -/** - * 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) return null; - - // 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); -} - -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 e7810f2..214d294 100644 --- a/Wallette/mobile/src/services/api/dashboardApi.ts +++ b/Wallette/mobile/src/services/api/dashboardApi.ts @@ -10,37 +10,56 @@ function safeDecision(action: any): TradeDecision { return "HOLD"; } +function toNumber(x: any, fallback = 0): number { + const n = typeof x === "number" ? x : Number(x); + return Number.isFinite(n) ? n : fallback; +} + function confidenceToLevel(conf: number): AlertLevel { if (conf >= 0.85) return "CRITICAL"; if (conf >= 0.65) return "WARNING"; return "INFO"; } -function toNumber(x: any, fallback = 0): number { - const n = typeof x === "number" ? x : Number(x); - return Number.isFinite(n) ? n : fallback; -} - 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."); + if (!userId) throw new Error("Session absente."); const settings = await loadSettings(); const quote: "EUR" | "USDT" = settings.currency === "USDT" ? "USDT" : "EUR"; const pair = `BTC/${quote}`; - // 1) Prix (toujours OK) - const priceRes = await getCurrentPrice(pair); + // 1) Prix + let priceRes; + try { + priceRes = await getCurrentPrice(pair); + } catch (e: any) { + throw new Error(e?.message ?? `Prix indisponible pour ${pair}.`); + } // 2) Signal (peut être null) - const sig = await apiGet( - `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}` - ); + let sig: any = null; + try { + sig = await apiGet( + `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}` + ); + } catch (e: any) { + // Signal indisponible, mais prix OK → on renvoie un “dashboard minimal” + return { + pair, + price: priceRes.price, + strategy: settings.selectedStrategyKey, + decision: "HOLD", + confidence: 0, + reason: "Signal indisponible pour le moment.", + alertLevel: "INFO", + timestamp: priceRes.timestampMs, + }; + } - // serveur renvoie { ok:true, data: ... } => http.ts unwrap => sig = data + // Pas de signal récent if (!sig) { - // pas de signal récent : on renvoie un dashboard “HOLD” propre return { pair, price: priceRes.price, @@ -53,23 +72,21 @@ export async function getDashboardSummary(): Promise { }; } - // Champs réels observés : - // action, confidence (string), reason, timestamp_ms, pair_code const decision = safeDecision(sig.action ?? sig.decision); const confidence = toNumber(sig.confidence, 0); const reason = String(sig.reason ?? sig.message ?? "—"); - const timestamp = toNumber(sig.timestamp_ms, 0) || toNumber(sig.timestamp, 0) || priceRes.timestampMs; - const alertLevel = - (String(sig.alertLevel ?? sig.criticality ?? "").toUpperCase() as AlertLevel) || - confidenceToLevel(confidence); - - // pair_code si présent, sinon pair demandé const pairOut = String(sig.pair_code ?? sig.pair ?? pair); + const levelFromServer = String(sig.alertLevel ?? sig.criticality ?? "").toUpperCase(); + + const alertLevel: AlertLevel = + levelFromServer === "CRITICAL" || levelFromServer === "WARNING" || levelFromServer === "INFO" + ? (levelFromServer as AlertLevel) + : confidenceToLevel(confidence); return { pair: pairOut, @@ -78,9 +95,7 @@ export async function getDashboardSummary(): Promise { decision, confidence, reason, - alertLevel: alertLevel === "CRITICAL" || alertLevel === "WARNING" || alertLevel === "INFO" - ? alertLevel - : confidenceToLevel(confidence), + alertLevel, 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 a7f5106..7123d8b 100644 --- a/Wallette/mobile/src/services/api/http.ts +++ b/Wallette/mobile/src/services/api/http.ts @@ -1,16 +1,6 @@ -import { API_BASE_URL } from "../../config/env"; -import { BASIC_AUTH_USER, BASIC_AUTH_PASS } from "../../config/env"; +import { API_BASE_URL, BASIC_AUTH_USER, BASIC_AUTH_PASS } from "../../config/env"; import { Buffer } from "buffer"; -/** - * HTTP helper (fetch) - * ------------------- - * - Ajoute Basic Auth si configuré (pour passer la barrière 401) - * - Compatible avec 2 formats : - * A) wrap : { ok:true, data: ... } / { ok:false, error:{message} } - * B) raw : { ... } / [...] - */ - function getBasicAuthHeader(): string | null { const u = (BASIC_AUTH_USER ?? "").trim(); const p = (BASIC_AUTH_PASS ?? "").trim(); diff --git a/Wallette/mobile/src/services/api/priceApi.ts b/Wallette/mobile/src/services/api/priceApi.ts index c91a378..da59959 100644 --- a/Wallette/mobile/src/services/api/priceApi.ts +++ b/Wallette/mobile/src/services/api/priceApi.ts @@ -7,26 +7,35 @@ export type PriceCurrent = { source?: string; }; +function toNumber(x: any): number | null { + if (typeof x === "number" && Number.isFinite(x)) return x; + if (typeof x === "string") { + const n = Number(x); + return Number.isFinite(n) ? n : null; + } + return null; +} + export async function getCurrentPrice(pair: string): Promise { const data = await apiGet(`/price/current?pair=${encodeURIComponent(pair)}`); const price = - (typeof data?.current_price === "number" ? data.current_price : null) ?? - (typeof data?.price === "number" ? data.price : null); + toNumber(data?.current_price) ?? + toNumber(data?.price); - if (typeof price !== "number" || !Number.isFinite(price)) { - throw new Error("Prix invalide (API)."); + if (price === null) { + throw new Error(`Prix indisponible pour ${pair}.`); } const ts = - (typeof data?.timestamp_ms === "number" ? data.timestamp_ms : null) ?? - (typeof data?.timestampMs === "number" ? data.timestampMs : null) ?? + toNumber(data?.timestamp_ms) ?? + toNumber(data?.timestampMs) ?? Date.now(); return { pair: String(data?.pair ?? pair), timestampMs: Number(ts), - price: Number(price), + price, source: typeof data?.source === "string" ? data.source : undefined, }; } \ No newline at end of file diff --git a/Wallette/mobile/src/services/notificationService.ts b/Wallette/mobile/src/services/notificationService.ts index 5e6cf16..2bb80c2 100644 --- a/Wallette/mobile/src/services/notificationService.ts +++ b/Wallette/mobile/src/services/notificationService.ts @@ -2,29 +2,15 @@ import * as Notifications from "expo-notifications"; import { Platform } from "react-native"; import type { Alert } from "../types/Alert"; -/** - * Notification handler (Foreground) - * --------------------------------- - * Sur certaines versions, Expo demande aussi shouldShowBanner/shouldShowList. - * On les met explicitement pour éviter les erreurs TypeScript. - */ Notifications.setNotificationHandler({ handleNotification: async () => ({ - shouldShowAlert: true, - shouldPlaySound: true, - shouldSetBadge: false, - - // ✅ champs demandés par les versions récentes shouldShowBanner: true, shouldShowList: true, + shouldPlaySound: false, + shouldSetBadge: false, }), }); -/** - * Initialisation Android (channel) - * -------------------------------- - * Sur Android, un channel "alerts" permet d'avoir une importance HIGH. - */ export async function initNotificationChannel(): Promise { if (Platform.OS !== "android") return; @@ -37,20 +23,33 @@ export async function initNotificationChannel(): Promise { }); } +function safeLevel(alert: Alert): string { + return String(alert.alertLevel ?? alert.criticality ?? "INFO"); +} + +function safeAction(alert: Alert): string { + return String(alert.action ?? "HOLD"); +} + +function safePair(alert: Alert): string { + return String(alert.pair ?? "—"); +} + +function safeConfidence(alert: Alert): number { + const c = alert.confidence; + return typeof c === "number" && Number.isFinite(c) ? c : 0; +} + function formatTitle(alert: Alert): string { - return `${alert.alertLevel} — ${alert.action} ${alert.pair}`; + return `${safeLevel(alert)} — ${safeAction(alert)} ${safePair(alert)}`; } function formatBody(alert: Alert): string { - const conf = Math.round(alert.confidence * 100); - return `${conf}% — ${alert.reason}`; + const confPct = Math.round(safeConfidence(alert) * 100); + const reason = String(alert.reason ?? alert.message ?? "—"); + return `${confPct}% — ${reason}`; } -/** - * Demande de permission - * --------------------- - * Retourne true si l'utilisateur accepte. - */ export async function requestNotificationPermission(): Promise { const current = await Notifications.getPermissionsAsync(); if (current.status === "granted") return true; @@ -59,22 +58,17 @@ export async function requestNotificationPermission(): Promise { return req.status === "granted"; } -/** - * Afficher une notification locale immédiate - * ------------------------------------------ - * data doit être un Record (pas un objet typé direct). - */ export async function showAlertNotification(alert: Alert): Promise { await initNotificationChannel(); const data: Record = { - pair: alert.pair, - action: alert.action, - alertLevel: alert.alertLevel, - confidence: alert.confidence, - reason: alert.reason, - price: alert.price ?? null, - timestamp: alert.timestamp ?? Date.now(), + pair: alert.pair ?? null, + action: alert.action ?? null, + alertLevel: alert.alertLevel ?? alert.criticality ?? null, + confidence: safeConfidence(alert), + reason: alert.reason ?? alert.message ?? null, + price: typeof alert.price === "number" ? alert.price : null, + timestamp: typeof alert.timestamp === "number" ? alert.timestamp : Date.now(), }; await Notifications.scheduleNotificationAsync({ @@ -84,6 +78,6 @@ export async function showAlertNotification(alert: Alert): Promise { data, sound: "default", }, - trigger: null, // immédiat + trigger: null, }); } \ No newline at end of file diff --git a/Wallette/mobile/src/services/socketService.ts b/Wallette/mobile/src/services/socketService.ts index 76d9fd7..c622621 100644 --- a/Wallette/mobile/src/services/socketService.ts +++ b/Wallette/mobile/src/services/socketService.ts @@ -25,68 +25,42 @@ class SocketService { this.currentServerUrl = serverUrl; this.currentUserId = userId; - const attachHandlers = (sock: Socket) => { - const emitAuth = () => sock.emit("auth", userId); - - sock.on("connect", () => { - console.log("✅ Socket connecté:", sock.id); - emitAuth(); - }); - - sock.on("auth_success", (data: any) => { - console.log("✅ Auth success:", data?.message ?? data); - }); - - sock.on("alert", (alert: Alert) => { - for (const cb of this.listeners) cb(alert); - }); - - sock.on("disconnect", (reason: string) => { - console.log("⚠️ Socket disconnect:", reason); - }); - - sock.on("error", (err: any) => { - console.log("❌ Socket error:", err?.message ?? err); - }); - }; - - // tentative websocket + polling const sock = io(serverUrl, { path: "/socket.io", - transports: ["websocket", "polling"], + transports: ["websocket"], // ✅ on force websocket (plus stable en prod) + upgrade: false, // ✅ évite switch polling->ws reconnection: true, - reconnectionAttempts: 10, + reconnectionAttempts: Infinity, // ✅ mais... + reconnectionDelay: 1500, timeout: 10000, }); this.socket = sock; - attachHandlers(sock); + + sock.on("connect", () => { + console.log("✅ Socket connecté:", sock.id); + sock.emit("auth", userId); + }); + + sock.on("auth_success", (data: any) => { + console.log("✅ Auth success:", data?.message ?? data); + }); + + sock.on("alert", (alert: Alert) => { + for (const cb of this.listeners) cb(alert); + }); + + sock.on("disconnect", (reason: string) => { + console.log("⚠️ Socket disconnect:", reason); + }); 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); - }); - } + // ✅ log simple, pas de fallback spam + console.log("❌ Socket connect_error:", err?.message ?? err); + }); + + sock.on("error", (err: any) => { + console.log("❌ Socket error:", err?.message ?? err); }); } diff --git a/Wallette/mobile/src/services/strategyService.ts b/Wallette/mobile/src/services/strategyService.ts index 2d7a558..f0f7812 100644 --- a/Wallette/mobile/src/services/strategyService.ts +++ b/Wallette/mobile/src/services/strategyService.ts @@ -1,23 +1,73 @@ import type { StrategyOption, StrategyKey } from "../types/Strategy"; -import { getStrategies } from "../mocks/strategies.mock"; - -/** - * strategyService - * --------------- - * Aujourd'hui : mock - * Demain : REST - * - * REST attendu (contrat groupe) : - * - POST /api/strategy/select - */ +import { apiGet } from "./api/http"; + +type Risk = "SAFE" | "NORMAL" | "AGGRESSIVE"; + +function asArray(x: any): any[] { + if (Array.isArray(x)) return x; + if (Array.isArray(x?.strategies)) return x.strategies; + if (Array.isArray(x?.items)) return x.items; + if (Array.isArray(x?.data)) return x.data; + return []; +} + +function normalizeRisk(raw: any): Risk { + const v = String(raw ?? "").toUpperCase(); + if (v === "SAFE") return "SAFE"; + if (v === "AGGRESSIVE") return "AGGRESSIVE"; + if (v === "NORMAL") return "NORMAL"; + return "NORMAL"; +} + +function normalizeStrategy(raw: any): StrategyOption | null { + const key = String(raw?.key ?? raw?.mode ?? raw?.strategy_key ?? raw?.strategyKey ?? "").trim(); + if (!key) return null; + + return { + key: key as StrategyKey, + label: String(raw?.label ?? raw?.name ?? key), + description: String(raw?.description ?? "—"), + risk: normalizeRisk(raw?.risk ?? raw?.level ?? raw?.modeRisk), + }; +} + export async function fetchStrategies(): Promise { - return await getStrategies(); + const candidates = ["/strategy/list", "/strategy/available", "/strategy/modes"]; + + for (const path of candidates) { + try { + const data = await apiGet(path); + const arr = asArray(data); + const normalized = arr.map(normalizeStrategy).filter(Boolean) as StrategyOption[]; + if (normalized.length > 0) return normalized; + } catch { + // continue + } + } + + // Fallback “propre” (sans mocks) + return [ + { + key: "RSI_SIMPLE" as StrategyKey, + label: "RSI Simple", + description: "Signal basé sur RSI (surachat / survente).", + risk: "NORMAL", + }, + { + key: "EMA_CROSS" as StrategyKey, + label: "EMA Cross", + description: "Croisement de moyennes mobiles exponentielles.", + risk: "NORMAL", + }, + { + key: "ALWAYS_HOLD" as StrategyKey, + label: "Always HOLD", + description: "Ne déclenche jamais d’achat/vente.", + risk: "SAFE", + }, + ]; } -/** - * Placeholder : plus tard on fera le POST ici. - * Pour le moment, la sélection est gérée via AsyncStorage (settings). - */ export async function selectStrategy(_strategyKey: StrategyKey): Promise { return; } \ No newline at end of file -- 2.50.1