From: Thibaud Moustier Date: Wed, 25 Feb 2026 19:26:43 +0000 (+0100) Subject: Mobile : modification portfolio et setting X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=f28bd57f334b4b7bf9e0ae6c2cb2e29a74ae6722;p=pdw25-26 Mobile : modification portfolio et setting --- diff --git a/Wallette/mobile/App.tsx b/Wallette/mobile/App.tsx index 8f9403f..0e33c56 100644 --- a/Wallette/mobile/App.tsx +++ b/Wallette/mobile/App.tsx @@ -1,7 +1,8 @@ import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { TouchableOpacity } from "react-native"; +import { TouchableOpacity, View, Text } from "react-native"; import { Ionicons } from "@expo/vector-icons"; +import { useEffect, useState } from "react"; import DashboardScreen from "./src/screens/DashboardScreen"; import SettingsScreen from "./src/screens/SettingsScreen"; @@ -9,8 +10,10 @@ import HistoryScreen from "./src/screens/HistoryScreen"; 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 { loadSession, clearSession } from "./src/utils/sessionStorage"; -// Types des routes (pour éviter les erreurs de navigation) export type RootStackParamList = { Dashboard: undefined; Settings: undefined; @@ -23,6 +26,46 @@ export type RootStackParamList = { const Stack = createNativeStackNavigator(); export default function App() { + const [ready, setReady] = useState(false); + const [isAuthed, setIsAuthed] = useState(false); + + useEffect(() => { + let active = true; + + async function init() { + const s = await loadSession(); + if (!active) return; + setIsAuthed(!!s); + setReady(true); + } + + init(); + + return () => { + active = false; + }; + }, []); + + if (!ready) { + return ( + + Initialisation… + + ); + } + + // Pas connecté -> écran Auth + if (!isAuthed) { + return ( + { + setIsAuthed(true); + }} + /> + ); + } + + // Connecté -> stack normal return ( @@ -32,42 +75,34 @@ export default function App() { options={({ navigation }) => ({ title: "Dashboard", headerRight: () => ( - navigation.navigate("Settings")}> - - + + {/* ⚙️ Settings */} + navigation.navigate("Settings")}> + + + + {/* ⎋ Logout */} + { + await clearSession(); + setIsAuthed(false); + }} + > + + + ), })} /> - - - - - - - - - + + + + + + {() => setIsAuthed(false)} />} + ); diff --git a/Wallette/mobile/src/screens/AuthScreen.tsx b/Wallette/mobile/src/screens/AuthScreen.tsx new file mode 100644 index 0000000..8e15dac --- /dev/null +++ b/Wallette/mobile/src/screens/AuthScreen.tsx @@ -0,0 +1,112 @@ +import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native"; +import { useMemo, useState } from "react"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import { ui } from "../components/ui/uiStyles"; +import { saveSession } from "../utils/sessionStorage"; + +/** + * AuthScreen (Step 4 - sans API) + * ------------------------------ + * Simule un login local : + * - Email -> userId stable + * - Session stockée en AsyncStorage + * + * Plus tard : on remplace par une auth REST réelle. + */ +export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => void }) { + const [email, setEmail] = useState("demo@example.com"); + const [error, setError] = useState(null); + + const normalizedEmail = useMemo(() => email.trim().toLowerCase(), [email]); + + const isValidEmail = useMemo(() => { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail); + }, [normalizedEmail]); + + const makeUserId = (mail: string) => `user_${mail.replace(/[^a-z0-9]/g, "_")}`; + + const handleLogin = async () => { + setError(null); + + if (!isValidEmail) { + setError("Email invalide."); + return; + } + + const userId = makeUserId(normalizedEmail); + + await saveSession({ + userId, + email: normalizedEmail, + createdAtMs: Date.now(), + }); + + onAuthenticated(); + }; + + return ( + + + Connexion + + + Email + + + + {!!error && {error}} + + + Se connecter + + + + Step 4 (sans API) : session locale. Plus tard, auth réelle via serveur. + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: ui.screen.backgroundColor, + }, + title: { + fontSize: 22, + fontWeight: "900", + marginBottom: 12, + 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, + }, + 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 f165513..d1e60f4 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -13,6 +13,7 @@ import { Ionicons } from "@expo/vector-icons"; import type { DashboardSummary } from "../types/DashboardSummary"; import { fetchDashboardSummary } from "../services/dashboardService"; + import { loadSettings } from "../utils/settingsStorage"; import type { UserSettings } from "../models/UserSettings"; @@ -24,22 +25,18 @@ import type { Alert } from "../types/Alert"; import { alertStore } from "../services/alertStore"; import { showAlertNotification } from "../services/notificationService"; -// ✅ Step 3 portfolio import { loadPortfolio } from "../utils/portfolioStorage"; import type { PortfolioState, PortfolioAsset } from "../models/Portfolio"; import { getMockPrice } from "../mocks/prices.mock"; +import { loadSession } from "../utils/sessionStorage"; + /** - * DashboardScreen (WF-01) — Step 3 - * -------------------------------- - * - Portefeuille multi-cryptos (portfolioStep3, AsyncStorage) - * - Valeur globale = somme(quantity * prix mock) - * - Cartes cliquables (chevron subtil) : - * * Portefeuille -> Wallet - * * Urgence -> Alertes - * * Prix BTC -> Historique - * - Conseiller : bouton -> Stratégie - * - Socket.IO non bloquant + notifications locales + * DashboardScreen — Step 4 + * ------------------------ + * - Multi-users : userId vient de la session (AsyncStorage) + * - Settings/Portfolio sont déjà "scopés" par userId (via storages) + * - Socket.IO auth utilise userId de session */ export default function DashboardScreen() { const { height } = useWindowDimensions(); @@ -47,8 +44,6 @@ export default function DashboardScreen() { const [summary, setSummary] = useState(null); const [settings, setSettings] = useState(null); - - // ✅ Step 3 portfolio const [portfolio, setPortfolio] = useState(null); const [loading, setLoading] = useState(true); @@ -64,10 +59,6 @@ export default function DashboardScreen() { const navigation = useNavigation(); - /** - * Load initial + reload au focus (retour depuis Settings/Strategy/Wallet) - * On recharge aussi le portfolio local (Step 3). - */ useFocusEffect( useCallback(() => { let isActive = true; @@ -88,7 +79,6 @@ export default function DashboardScreen() { setSummary(dashboardData); setSettings(userSettings); setPortfolio(portfolioData); - setLastRefreshMs(Date.now()); } catch { if (isActive) setError("Impossible de charger le dashboard."); @@ -105,9 +95,6 @@ export default function DashboardScreen() { }, []) ); - /** - * Refresh auto (si activé) - */ useEffect(() => { if (!settings) return; if (settings.refreshMode !== "auto") return; @@ -132,9 +119,6 @@ export default function DashboardScreen() { }; }, [settings]); - /** - * Refresh manuel - */ const handleManualRefresh = async () => { try { setRefreshing(true); @@ -151,51 +135,65 @@ export default function DashboardScreen() { /** * Socket.IO (non bloquant) - * Step 1/2/3 : userId de test + * - userId = session.userId */ useEffect(() => { - if (!settings) return; + let unsub: null | (() => void) = null; + let active = true; - setSocketError(null); + async function initSocket() { + if (!settings) return; - const userId = "test-user"; + setSocketError(null); - try { - socketService.connect(SERVER_URL, userId); - setSocketConnected(true); - } catch { - setSocketConnected(false); - setSocketError("Socket : paramètres invalides (URL ou userId)."); - return; - } + const session = await loadSession(); + const userId = session?.userId; - const unsubscribeAlert = socketService.onAlert((alert) => { - alertStore.add(alert); - setLiveAlerts((prev) => [alert, ...prev].slice(0, 100)); - - if (settings.notificationsEnabled) { - void (async () => { - try { - await showAlertNotification(alert); - } catch (e) { - console.log("⚠️ Notification error:", e); - } - })(); + if (!userId) { + setSocketConnected(false); + setSocketError("Socket désactivé : session absente."); + return; } - }); - socketService.ping(); + try { + socketService.connect(SERVER_URL, userId); + if (!active) return; + setSocketConnected(true); + } catch { + if (!active) return; + setSocketConnected(false); + setSocketError("Socket : paramètres invalides (URL ou userId)."); + return; + } + + unsub = socketService.onAlert((alert) => { + alertStore.add(alert); + setLiveAlerts((prev) => [alert, ...prev].slice(0, 100)); + + if (settings.notificationsEnabled) { + void (async () => { + try { + await showAlertNotification(alert); + } catch (e) { + console.log("⚠️ Notification error:", e); + } + })(); + } + }); + + socketService.ping(); + } + + void initSocket(); return () => { - unsubscribeAlert(); + active = false; + if (unsub) unsub(); socketService.disconnect(); setSocketConnected(false); }; }, [settings]); - /** - * Urgence : 1 seule alerte, la plus importante. - */ const urgentAlert: Alert | null = useMemo(() => { if (liveAlerts.length === 0) return null; @@ -208,10 +206,6 @@ export default function DashboardScreen() { return liveAlerts[0]; }, [liveAlerts]); - /** - * Valeur globale du portefeuille (Step 3) - * total = somme(quantity * prix(mock)) - */ const portfolioTotalValue = useMemo(() => { if (!portfolio) return 0; @@ -222,21 +216,15 @@ export default function DashboardScreen() { }, 0); }, [portfolio]); - /** - * Résumé : top assets à afficher sur le dashboard - * - on prend 3 assets max - */ const topAssets: PortfolioAsset[] = useMemo(() => { if (!portfolio) return []; - // tri simple par valeur estimée (desc) si prix connu const withValue = portfolio.assets.map((a) => { const price = getMockPrice(a.symbol) ?? 0; return { ...a, _value: a.quantity * price }; }); withValue.sort((a, b) => (b as any)._value - (a as any)._value); - return withValue.slice(0, 3).map(({ symbol, quantity }) => ({ symbol, quantity })); }, [portfolio]); @@ -245,7 +233,6 @@ export default function DashboardScreen() { return Math.max(0, portfolio.assets.length - topAssets.length); }, [portfolio, topAssets]); - // Loading / erreurs bloquantes if (loading) { return ( @@ -315,7 +302,7 @@ export default function DashboardScreen() { - {/* 2) PORTEFEUILLE — cliquable => Wallet (Step 3) */} + {/* 2) PORTEFEUILLE */} navigation.navigate("Wallet" as never)}> @@ -330,7 +317,6 @@ export default function DashboardScreen() { - {/* Résumé assets */} {portfolio.assets.length === 0 ? ( Aucun asset (ajoute BTC/ETH/SOL dans Portefeuille) @@ -352,14 +338,13 @@ export default function DashboardScreen() { )} - {/* Ligne informative BTC @ prix (mock) */} - BTC @ {summary.price.toFixed(2)} {settings.currency} (source mock) + BTC @ {summary.price.toFixed(2)} {settings.currency} (mock) - {/* 3) URGENCE — cliquable => Alertes */} + {/* 3) URGENCE */} navigation.navigate("Alerts" as never)}> @@ -387,7 +372,7 @@ export default function DashboardScreen() { - {/* 4) PRIX BTC — cliquable => Historique */} + {/* 4) PRIX BTC */} navigation.navigate("History" as never)}> diff --git a/Wallette/mobile/src/screens/SettingsScreen.tsx b/Wallette/mobile/src/screens/SettingsScreen.tsx index 24c9e77..4f753fb 100644 --- a/Wallette/mobile/src/screens/SettingsScreen.tsx +++ b/Wallette/mobile/src/screens/SettingsScreen.tsx @@ -6,16 +6,26 @@ import { loadSettings, saveSettings } from "../utils/settingsStorage"; import type { UserSettings } from "../models/UserSettings"; import { requestNotificationPermission } from "../services/notificationService"; +import { clearSession, loadSession } from "../utils/sessionStorage"; + import { ui } from "../components/ui/uiStyles"; -export default function SettingsScreen() { +/** + * SettingsScreen (Step 4) + * ---------------------- + * Paramètres stockés par userId (via settingsStorage). + * Ajout : bouton Déconnexion. + */ +export default function SettingsScreen({ onLogout }: { onLogout?: () => void }) { const [settings, setSettings] = useState(null); const [infoMessage, setInfoMessage] = useState(null); + const [sessionLabel, setSessionLabel] = useState(""); useEffect(() => { async function init() { - const data = await loadSettings(); + const [data, session] = await Promise.all([loadSettings(), loadSession()]); setSettings(data); + setSessionLabel(session?.email ?? "—"); } init(); }, []); @@ -63,11 +73,26 @@ export default function SettingsScreen() { setInfoMessage("Paramètres sauvegardés ✅"); }; + const handleLogout = async () => { + await clearSession(); + onLogout?.(); // App.tsx repasse sur Auth + }; + return ( Paramètres + {/* Carte session */} + + Compte + Connecté : {sessionLabel} + + + Se déconnecter + + + {/* Carte : Devise */} Devise @@ -91,9 +116,7 @@ export default function SettingsScreen() { {/* Carte : Notifications */} Notifications - - Statut : {settings.notificationsEnabled ? "ON" : "OFF"} - + Statut : {settings.notificationsEnabled ? "ON" : "OFF"} @@ -101,7 +124,7 @@ export default function SettingsScreen() { - {!!infoMessage && {infoMessage}} + {!!infoMessage && {infoMessage}} {/* Bouton Save */} @@ -124,25 +147,33 @@ const styles = StyleSheet.create({ marginBottom: 12, color: "#0f172a", }, - infoText: { - marginTop: 10, - opacity: 0.8, + boldInline: { + fontWeight: "900", + color: "#0f172a", }, - /** - * fullButton - * ---------- - * On neutralise les propriétés "grille" de ui.button (flexGrow/flexBasis), - * car elles sont utiles dans ActionsCard, mais pas dans Settings. - */ fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginTop: 10, }, - saveButton: { + marginTop: 4, marginBottom: 10, }, + + secondaryButton: { + marginTop: 10, + paddingVertical: 10, + borderRadius: 10, + borderWidth: 1, + borderColor: "#e5e7eb", + alignItems: "center", + backgroundColor: "#fff", + }, + secondaryButtonText: { + fontWeight: "900", + color: "#dc2626", + }, }); \ No newline at end of file diff --git a/Wallette/mobile/src/utils/portfolioStorage.ts b/Wallette/mobile/src/utils/portfolioStorage.ts index 887ed8c..8774d15 100644 --- a/Wallette/mobile/src/utils/portfolioStorage.ts +++ b/Wallette/mobile/src/utils/portfolioStorage.ts @@ -1,18 +1,24 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import type { PortfolioState } from "../models/Portfolio"; - -const KEY = "portfolioStep3"; +import { loadSession } from "./sessionStorage"; /** - * Portfolio par défaut : vide + * KEY devient "portfolio:" */ +function keyFor(userId: string) { + return `portfolio:${userId}`; +} + const DEFAULT_PORTFOLIO: PortfolioState = { assets: [], updatedAtMs: Date.now(), }; export async function loadPortfolio(): Promise { - const raw = await AsyncStorage.getItem(KEY); + const session = await loadSession(); + if (!session) return DEFAULT_PORTFOLIO; + + const raw = await AsyncStorage.getItem(keyFor(session.userId)); if (!raw) return DEFAULT_PORTFOLIO; try { @@ -28,13 +34,20 @@ export async function loadPortfolio(): Promise { } export async function savePortfolio(portfolio: PortfolioState): Promise { + const session = await loadSession(); + if (!session) return; + const safe: PortfolioState = { assets: Array.isArray(portfolio.assets) ? portfolio.assets : [], updatedAtMs: Date.now(), }; - await AsyncStorage.setItem(KEY, JSON.stringify(safe)); + + await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(safe)); } export async function clearPortfolio(): Promise { - await AsyncStorage.removeItem(KEY); + const session = await loadSession(); + if (!session) return; + + await AsyncStorage.removeItem(keyFor(session.userId)); } \ No newline at end of file diff --git a/Wallette/mobile/src/utils/sessionStorage.ts b/Wallette/mobile/src/utils/sessionStorage.ts new file mode 100644 index 0000000..0c756c8 --- /dev/null +++ b/Wallette/mobile/src/utils/sessionStorage.ts @@ -0,0 +1,45 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export type Session = { + userId: string; + email: string; + createdAtMs: number; +}; + +const KEY = "session"; + +/** + * Charge la session courante (si l'utilisateur est "connecté"). + */ +export async function loadSession(): Promise { + const raw = await AsyncStorage.getItem(KEY); + if (!raw) return null; + + try { + const parsed = JSON.parse(raw) as Partial; + if (!parsed.userId || !parsed.email) return null; + + return { + userId: String(parsed.userId), + email: String(parsed.email), + createdAtMs: Number(parsed.createdAtMs ?? Date.now()), + }; + } catch { + return null; + } +} + +/** + * Sauvegarde une session. + * Ici c'est un login "local" (sans API). + */ +export async function saveSession(session: Session): Promise { + await AsyncStorage.setItem(KEY, JSON.stringify(session)); +} + +/** + * Déconnexion. + */ +export async function clearSession(): Promise { + await AsyncStorage.removeItem(KEY); +} \ No newline at end of file diff --git a/Wallette/mobile/src/utils/settingsStorage.ts b/Wallette/mobile/src/utils/settingsStorage.ts index 07570d7..1fe6b39 100644 --- a/Wallette/mobile/src/utils/settingsStorage.ts +++ b/Wallette/mobile/src/utils/settingsStorage.ts @@ -1,26 +1,28 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import type { UserSettings } from "../models/UserSettings"; - -const KEY = "userSettings"; +import { loadSession } from "./sessionStorage"; /** - * Settings par défaut - * ------------------- - * On les fusionne avec ce qui est en storage. + * KEY devient "userSettings:" */ +function keyFor(userId: string) { + return `userSettings:${userId}`; +} + const DEFAULT_SETTINGS: UserSettings = { currency: "EUR", favoriteSymbol: "BTC", alertPreference: "critical", refreshMode: "manual", notificationsEnabled: false, - - // ✅ stratégie par défaut selectedStrategyKey: "RSI_SIMPLE", }; export async function loadSettings(): Promise { - const raw = await AsyncStorage.getItem(KEY); + const session = await loadSession(); + if (!session) return DEFAULT_SETTINGS; // sécurité (normalement, App bloque sans session) + + const raw = await AsyncStorage.getItem(keyFor(session.userId)); if (!raw) return DEFAULT_SETTINGS; try { @@ -32,5 +34,8 @@ export async function loadSettings(): Promise { } export async function saveSettings(settings: UserSettings): Promise { - await AsyncStorage.setItem(KEY, JSON.stringify(settings)); + const session = await loadSession(); + if (!session) return; + + await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(settings)); } \ No newline at end of file