+import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
View,
Text,
StyleSheet,
- ScrollView,
TouchableOpacity,
- useWindowDimensions,
+ ScrollView,
+ TextInput,
+ Modal,
+ KeyboardAvoidingView,
+ Platform,
+ ActivityIndicator,
+ Alert as RNAlert,
} from "react-native";
-import { useState, useCallback, useEffect, useMemo } from "react";
+import AsyncStorage from "@react-native-async-storage/async-storage";
import { SafeAreaView } from "react-native-safe-area-context";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
-import { Ionicons } from "@expo/vector-icons";
-import type { DashboardSummary } from "../types/DashboardSummary";
-
-/**
- * ✅ Façade Dashboard (API-ready)
- * - aujourd'hui : ce fichier peut encore être mock si ton dashboardApi l'est
- * - demain : REST via gateway (/api/...)
- */
-import { getDashboardSummary } from "../services/api/dashboardApi";
-
-import { loadSettings } from "../utils/settingsStorage";
+import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
import type { UserSettings } from "../models/UserSettings";
+import type { Alert as AlertType } from "../types/Alert";
-import { ui } from "../components/ui/uiStyles";
+import { loadSession } from "../utils/sessionStorage";
+import { loadSettings } from "../utils/settingsStorage";
+import { loadPortfolio, savePortfolio } from "../utils/portfolioStorage";
-import { socketService } from "../services/socketService";
import { SERVER_URL } from "../config/env";
-import type { Alert } from "../types/Alert";
+import { socketService } from "../services/socketService";
import { alertStore } from "../services/alertStore";
import { showAlertNotification } from "../services/notificationService";
-import { loadPortfolio } from "../utils/portfolioStorage";
-import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
-
-import { loadSession } from "../utils/sessionStorage";
-
-/**
- * ✅ API-only (price)
- */
import { getCurrentPrice } from "../services/api/priceApi";
+import { getAlertHistory } from "../services/api/alertsApi";
+import { getPortfolio as getPortfolioFromApi } from "../services/api/walletApi";
/**
- * 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
+ * DashboardScreen (aligné Web)
+ * ---------------------------
+ * Web consomme :
+ * - GET /api/prices/current?pair=BTC/EUR -> { price }
+ * - GET /api/wallet/:userId -> { balance }
+ * - GET /api/alerts/history?userId=... -> Alert[]
*
- * IMPORTANT :
- * - Les prix (portfolio) sont chargés via API-only (pas de mock).
- * - Pour performance : on charge les prix du TOP 3 (affichage "valeur partielle").
+ * Mobile :
+ * - UI identique dans l’esprit (crypto + adresse + buy/sell)
+ * - "Acheter/Vendre" = simulation (local) -> pas de trading réel
*/
+
+type CryptoSymbol = "BTC" | "ETH" | "LTC";
+type TradeSide = "BUY" | "SELL";
+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 {
+ // patch écrase base sur les symboles présents dans patch
+ const map = new Map<string, number>();
+
+ for (const a of base.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
+ for (const a of patch.assets) map.set(normalizeSymbol(a.symbol), a.quantity);
+
+ const assets: PortfolioAsset[] = Array.from(map.entries())
+ .filter(([, qty]) => Number.isFinite(qty) && qty > 0)
+ .map(([symbol, quantity]) => ({ symbol, quantity }))
+ .sort((a, b) => a.symbol.localeCompare(b.symbol));
+
+ return { assets, updatedAtMs: Date.now() };
+}
+
export default function DashboardScreen() {
- const { height } = useWindowDimensions();
- const compact = height < 760;
+ const navigation = useNavigation();
- const [summary, setSummary] = useState<DashboardSummary | null>(null);
+ // session / settings
+ const [userId, setUserId] = useState<string | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
- const [portfolio, setPortfolio] = useState<PortfolioState | null>(null);
- const [loading, setLoading] = useState<boolean>(true);
- const [error, setError] = useState<string | null>(null);
+ // dashboard data
+ const [selectedCrypto, setSelectedCrypto] = useState<CryptoSymbol>("BTC");
+ const [currentPrice, setCurrentPrice] = useState<number | null>(null);
+ const [lastPriceUpdateMs, setLastPriceUpdateMs] = useState<number | null>(null);
+ const [portfolio, setPortfolio] = useState<PortfolioState>({ assets: [], updatedAtMs: Date.now() });
+
+ // wallet address
+ const [walletAddress, setWalletAddress] = useState<string>("");
+ const [walletAddressInfo, setWalletAddressInfo] = useState<string | null>(null);
+
+ // alerts
+ const [liveAlerts, setLiveAlerts] = useState<AlertType[]>([]);
const [socketConnected, setSocketConnected] = useState<boolean>(false);
- const [socketError, setSocketError] = useState<string | null>(null);
+ const [socketInfo, setSocketInfo] = useState<string | null>(null);
+
+ // loading / errors
+ const [loading, setLoading] = useState<boolean>(true);
+ const [softError, setSoftError] = useState<string | null>(null);
- const [liveAlerts, setLiveAlerts] = useState<Alert[]>([]);
+ // modal trade
+ const [tradeOpen, setTradeOpen] = useState<boolean>(false);
+ const [tradeSide, setTradeSide] = useState<TradeSide>("BUY");
+ const [tradeQty, setTradeQty] = useState<string>("0.01");
+ const [tradeInfo, setTradeInfo] = useState<string | null>(null);
- const [lastRefreshMs, setLastRefreshMs] = useState<number | null>(null);
- const [refreshing, setRefreshing] = useState<boolean>(false);
+ const currency: "EUR" | "USD" = settings?.currency === "USD" ? "USD" : "EUR";
+ const pair = useMemo(() => `${selectedCrypto}/${currency}`, [selectedCrypto, currency]);
- // ✅ Prix (API-only) pour TOP 3 assets
- const [assetPrices, setAssetPrices] = useState<Record<string, number>>({});
- const [assetPricesError, setAssetPricesError] = useState<string | null>(null);
+ const selectedQty = useMemo(() => {
+ const a = portfolio.assets.find((x) => normalizeSymbol(x.symbol) === selectedCrypto);
+ return a?.quantity ?? 0;
+ }, [portfolio, selectedCrypto]);
- const navigation = useNavigation();
+ const urgentAlert = useMemo(() => {
+ if (liveAlerts.length === 0) return null;
- /**
- * Top 3 assets (par quantité, puis symbole — simple et défendable)
- * NOTE: si tu veux une vraie logique "top valeur", il faut les prix de tous les assets.
- */
- const topAssets: PortfolioAsset[] = useMemo(() => {
- if (!portfolio) return [];
-
- const copy = [...portfolio.assets];
- copy.sort((a, b) => b.quantity - a.quantity || a.symbol.localeCompare(b.symbol));
- return copy.slice(0, 3);
- }, [portfolio]);
-
- const remainingCount = useMemo(() => {
- if (!portfolio) return 0;
- return Math.max(0, portfolio.assets.length - topAssets.length);
- }, [portfolio, topAssets]);
-
- /**
- * Charge prix API-only pour les TOP 3 assets
- */
- const loadTopAssetPrices = useCallback(async (assets: PortfolioAsset[], currency: string) => {
- setAssetPricesError(null);
-
- if (assets.length === 0) {
- setAssetPrices({});
- return;
+ const critical = liveAlerts.find((a) => String(a.alertLevel).toUpperCase() === "CRITICAL");
+ if (critical) return critical;
+
+ const warning = liveAlerts.find((a) => String(a.alertLevel).toUpperCase() === "WARNING");
+ if (warning) return warning;
+
+ return liveAlerts[0];
+ }, [liveAlerts]);
+
+ const fetchPrice = useCallback(
+ async (p: string) => {
+ const res = await getCurrentPrice(p);
+ setCurrentPrice(res.price);
+ setLastPriceUpdateMs(res.timestampMs ?? Date.now());
+ },
+ []
+ );
+
+ const refreshAll = useCallback(async () => {
+ setSoftError(null);
+
+ const session = await loadSession();
+ const uid = session?.userId ?? null;
+ setUserId(uid);
+
+ const s = await loadSettings();
+ setSettings(s);
+
+ // Local portfolio (fallback + base)
+ const localPortfolio = await loadPortfolio();
+ setPortfolio(localPortfolio);
+
+ // Adresse wallet (local, par userId)
+ if (uid) {
+ const addr = (await AsyncStorage.getItem(walletAddressKey(uid))) ?? "";
+ setWalletAddress(addr);
+ } else {
+ setWalletAddress("");
}
+ // Prix (API)
try {
- const entries = await Promise.all(
- assets.map(async (a) => {
- const pair = `${a.symbol}/${currency}`;
- const cur = await getCurrentPrice(pair);
- return [a.symbol, cur.price] as const;
- })
- );
-
- const map: Record<string, number> = {};
- for (const [sym, pr] of entries) map[sym] = pr;
- setAssetPrices(map);
- } catch (e: any) {
- setAssetPrices({});
- setAssetPricesError(e?.message ?? "Impossible de charger les prix (API).");
+ await fetchPrice(`${selectedCrypto}/${s.currency === "USD" ? "USD" : "EUR"}`);
+ } catch {
+ setSoftError("Prix indisponible pour le moment (API).");
}
- }, []);
- /**
- * Chargement initial (focus)
- * - dashboard
- * - settings
- * - portfolio
- * + prix (API-only) sur top 3
- */
+ // Wallet (API) -> fusion pour ne pas écraser ETH/LTC locaux
+ if (uid) {
+ try {
+ const apiPortfolio = await getPortfolioFromApi();
+ const merged = mergePortfolios(localPortfolio, apiPortfolio);
+ setPortfolio(merged);
+ await savePortfolio(merged);
+ } catch {
+ // pas bloquant
+ setSoftError((prev) => prev ?? "Wallet indisponible pour le moment (API).");
+ }
+ }
+
+ // Alert history (API) + socket live ensuite
+ if (uid) {
+ try {
+ const history = await getAlertHistory();
+ // On met en store + state
+ for (const a of history) alertStore.add(a);
+ setLiveAlerts(history.slice(0, 50));
+ } catch {
+ // pas bloquant
+ }
+ }
+ }, [fetchPrice, selectedCrypto]);
+
useFocusEffect(
useCallback(() => {
- let isActive = true;
+ let active = true;
- async function loadData() {
+ (async () => {
try {
- setError(null);
setLoading(true);
-
- const [dashboardData, userSettings, portfolioData] = await Promise.all([
- getDashboardSummary(),
- loadSettings(),
- loadPortfolio(),
- ]);
-
- if (!isActive) return;
-
- setSummary(dashboardData);
- setSettings(userSettings);
- setPortfolio(portfolioData);
- setLastRefreshMs(Date.now());
-
- // prix API-only sur top 3
- const copy = [...portfolioData.assets];
- copy.sort((a, b) => b.quantity - a.quantity || a.symbol.localeCompare(b.symbol));
- const top = copy.slice(0, 3);
- await loadTopAssetPrices(top, userSettings.currency);
- } catch {
- if (isActive) setError("Impossible de charger le dashboard.");
+ await refreshAll();
} finally {
- if (isActive) setLoading(false);
+ if (active) setLoading(false);
}
- }
-
- void loadData();
+ })();
return () => {
- isActive = false;
+ active = false;
};
- }, [loadTopAssetPrices])
+ }, [refreshAll])
);
- /**
- * Refresh auto (si activé)
- */
- useEffect(() => {
- if (!settings) return;
- if (settings.refreshMode !== "auto") return;
-
- let cancelled = false;
-
- const intervalId = setInterval(async () => {
- try {
- const data = await getDashboardSummary();
- if (!cancelled) {
- setSummary(data);
- setLastRefreshMs(Date.now());
- }
- } catch {
- if (!cancelled) setError("Erreur lors du rafraîchissement automatique.");
- }
- }, 5000);
-
- return () => {
- cancelled = true;
- clearInterval(intervalId);
- };
- }, [settings]);
-
- /**
- * Refresh manuel
- */
- const handleManualRefresh = async () => {
- try {
- setRefreshing(true);
- const data = await getDashboardSummary();
- setSummary(data);
- setLastRefreshMs(Date.now());
- setError(null);
-
- // refresh prix top assets aussi (API-only)
- if (settings) {
- await loadTopAssetPrices(topAssets, settings.currency);
- }
- } catch {
- setError("Erreur lors de l'actualisation.");
- } finally {
- setRefreshing(false);
- }
- };
-
- /**
- * Socket.IO (non bloquant)
- * - userId = session.userId
- */
+ // Socket : alert live (non bloquant)
useEffect(() => {
let unsub: null | (() => void) = null;
- let active = true;
+ let alive = true;
- async function initSocket() {
- if (!settings) return;
-
- setSocketError(null);
+ (async () => {
+ setSocketInfo(null);
+ setSocketConnected(false);
const session = await loadSession();
- const userId = session?.userId;
+ const uid = session?.userId;
- if (!userId) {
- setSocketConnected(false);
- setSocketError("Socket désactivé : session absente.");
+ if (!uid) {
+ setSocketInfo("Socket désactivé : session absente.");
return;
}
try {
- socketService.connect(SERVER_URL, userId);
- if (!active) return;
+ socketService.connect(SERVER_URL, uid);
+ if (!alive) return;
setSocketConnected(true);
} catch {
- if (!active) return;
- setSocketConnected(false);
- setSocketError("Socket : paramètres invalides (URL ou userId).");
+ if (!alive) return;
+ setSocketInfo("Socket indisponible (URL ou serveur).");
return;
}
- unsub = socketService.onAlert((alert) => {
- alertStore.add(alert);
- setLiveAlerts((prev) => [alert, ...prev].slice(0, 100));
+ unsub = socketService.onAlert((a) => {
+ alertStore.add(a);
+ setLiveAlerts((prev) => [a, ...prev].slice(0, 100));
- if (settings.notificationsEnabled) {
+ // notifications si activées
+ if (settings?.notificationsEnabled) {
void (async () => {
try {
- await showAlertNotification(alert);
- } catch (e) {
- console.log("⚠️ Notification error:", e);
+ await showAlertNotification(a);
+ } catch {
+ // silence
}
})();
}
});
+ // Optionnel
socketService.ping();
- }
-
- void initSocket();
+ })();
return () => {
- active = false;
+ alive = false;
if (unsub) unsub();
socketService.disconnect();
setSocketConnected(false);
};
}, [settings]);
- /**
- * Alerte "urgente" (priorité : CRITICAL > WARNING > INFO)
- * Le dashboard n'affiche qu'une seule alerte (la plus importante).
- */
- const urgentAlert: Alert | null = useMemo(() => {
- if (liveAlerts.length === 0) return null;
+ const handleSaveWalletAddress = useCallback(async () => {
+ setWalletAddressInfo(null);
- const critical = liveAlerts.filter((a) => a.alertLevel === "CRITICAL");
- if (critical.length > 0) return critical[0];
+ if (!userId) {
+ setWalletAddressInfo("Impossible : session absente.");
+ return;
+ }
- const warning = liveAlerts.filter((a) => a.alertLevel === "WARNING");
- if (warning.length > 0) return warning[0];
+ const trimmed = walletAddress.trim();
+ if (trimmed.length < 6) {
+ setWalletAddressInfo("Adresse trop courte. Vérifie la saisie.");
+ return;
+ }
- return liveAlerts[0];
- }, [liveAlerts]);
+ await AsyncStorage.setItem(walletAddressKey(userId), trimmed);
+ setWalletAddressInfo("Adresse enregistrée ✅");
+ }, [walletAddress, userId]);
- /**
- * Valeur portefeuille (partielle = top3), API-only
- */
- const portfolioPartialValue = useMemo(() => {
- if (!portfolio) return null;
+ const openTrade = useCallback((side: TradeSide) => {
+ setTradeSide(side);
+ setTradeQty("0.01");
+ setTradeInfo(null);
+ setTradeOpen(true);
+ }, []);
- let sum = 0;
- let hasAny = false;
+ const confirmTrade = useCallback(async () => {
+ setTradeInfo(null);
- for (const a of topAssets) {
- const price = assetPrices[a.symbol];
- if (typeof price !== "number") continue;
- hasAny = true;
- sum += a.quantity * price;
+ const qty = Number(tradeQty.replace(",", ".").trim());
+ if (!Number.isFinite(qty) || qty <= 0) {
+ setTradeInfo("Quantité invalide (ex: 0.01).");
+ return;
}
- return hasAny ? sum : null;
- }, [portfolio, topAssets, assetPrices]);
+ const symbol = selectedCrypto;
+ const current = portfolio.assets.find((a) => normalizeSymbol(a.symbol) === symbol);
+ const currentQty = current?.quantity ?? 0;
- if (loading) {
- return (
- <View style={ui.centered}>
- <Text>Chargement du dashboard…</Text>
- </View>
- );
- }
+ let nextQty = currentQty;
- if (error && (!summary || !settings)) {
- return (
- <View style={ui.centered}>
- <Text style={styles.errorText}>{error}</Text>
- </View>
- );
- }
+ if (tradeSide === "BUY") {
+ nextQty = currentQty + qty;
+ } else {
+ if (qty > currentQty) {
+ setTradeInfo(`Vente impossible : tu n'as que ${currentQty.toFixed(6)} ${symbol}.`);
+ return;
+ }
+ nextQty = currentQty - qty;
+ }
+
+ const nextAssets = portfolio.assets
+ .filter((a) => normalizeSymbol(a.symbol) !== symbol)
+ .concat(nextQty > 0 ? [{ symbol, quantity: nextQty }] : [])
+ .sort((a, b) => normalizeSymbol(a.symbol).localeCompare(normalizeSymbol(b.symbol)));
- if (!summary || !settings || !portfolio) {
+ const nextPortfolio: PortfolioState = {
+ assets: nextAssets,
+ updatedAtMs: Date.now(),
+ };
+
+ // Aujourd’hui : local (simulation)
+ await savePortfolio(nextPortfolio);
+ setPortfolio(nextPortfolio);
+
+ setTradeOpen(false);
+
+ RNAlert.alert("Transaction enregistrée ✅", `${tradeSide} ${qty} ${symbol}`);
+ }, [tradeQty, tradeSide, selectedCrypto, portfolio]);
+
+ const handleManualRefreshPrice = useCallback(async () => {
+ try {
+ setSoftError(null);
+ await fetchPrice(pair);
+ } catch {
+ setSoftError("Prix indisponible pour le moment (API).");
+ }
+ }, [fetchPrice, pair]);
+
+ if (loading) {
return (
- <View style={ui.centered}>
- <Text>Initialisation…</Text>
+ <View style={styles.centered}>
+ <ActivityIndicator />
+ <Text style={{ marginTop: 10 }}>Chargement…</Text>
</View>
);
}
- const Chevron = () => (
- <Ionicons
- name="chevron-forward-outline"
- size={18}
- color="#0f172a"
- style={{ opacity: 0.35 }}
- />
- );
-
return (
<SafeAreaView style={styles.safeArea}>
- <ScrollView
- contentContainerStyle={[ui.container, compact && styles.containerCompact]}
- showsVerticalScrollIndicator={false}
- >
- {error && (
- <View style={[ui.banner, styles.bannerWarning, compact && styles.bannerCompact]}>
- <Text style={[ui.bannerText, styles.bannerWarningText]} numberOfLines={2}>
- {error}
- </Text>
+ <ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
+ {!!softError && (
+ <View style={styles.banner}>
+ <Text style={styles.bannerText}>{softError}</Text>
</View>
)}
- {/* 1) CONSEILLER */}
- <View style={[ui.card, compact && styles.cardCompact]}>
- <Text style={[ui.title, compact && styles.titleCompact]}>Conseiller</Text>
-
- <Text style={[ui.bigCenter, compact && styles.bigCompact]}>{summary.decision}</Text>
+ {/* COMPTE UTILISATEUR */}
+ <View style={styles.card}>
+ <View style={styles.rowBetween}>
+ <Text style={styles.title}>Compte utilisateur</Text>
+ <Text style={styles.muted}>{socketConnected ? "Socket: ON ✅" : "Socket: OFF ⚠️"}</Text>
+ </View>
- <Text style={[ui.muted, styles.centerText]} numberOfLines={compact ? 2 : 3}>
- Pourquoi ? {summary.reason}
+ <Text style={styles.muted}>
+ UserId : <Text style={styles.bold}>{userId ?? "—"}</Text>
</Text>
- <Text style={[ui.muted, { marginTop: compact ? 6 : 10 }]} numberOfLines={1}>
- Stratégie : <Text style={styles.boldInline}>{settings.selectedStrategyKey}</Text>
- </Text>
+ {!!socketInfo && <Text style={[styles.muted, { marginTop: 6 }]}>{socketInfo}</Text>}
+ </View>
- <TouchableOpacity
- style={[ui.button, styles.fullButton, compact && styles.buttonCompact]}
- onPress={() => navigation.navigate("Strategy" as never)}
- >
- <Text style={ui.buttonText}>Sélectionner stratégie</Text>
+ {/* CHOIX CRYPTO + ADRESSE */}
+ <View style={styles.card}>
+ <Text style={styles.title}>Choisir une cryptomonnaie</Text>
+
+ <View style={styles.cryptoRow}>
+ {CRYPTOS.map((c) => {
+ const active = c === selectedCrypto;
+ return (
+ <TouchableOpacity
+ key={c}
+ style={[styles.cryptoBtn, active && styles.cryptoBtnActive]}
+ onPress={async () => {
+ setSelectedCrypto(c);
+ try {
+ await fetchPrice(`${c}/${currency}`);
+ } catch {
+ setSoftError("Prix indisponible pour le moment (API).");
+ }
+ }}
+ >
+ <Text style={[styles.cryptoText, active && styles.cryptoTextActive]}>{c}</Text>
+ </TouchableOpacity>
+ );
+ })}
+ </View>
+
+ <Text style={[styles.muted, { marginTop: 12 }]}>Adresse du portefeuille</Text>
+ <TextInput
+ value={walletAddress}
+ onChangeText={(t) => {
+ setWalletAddressInfo(null);
+ setWalletAddress(t);
+ }}
+ placeholder="Entrez une adresse crypto"
+ style={styles.input}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+
+ <TouchableOpacity style={styles.secondaryButton} onPress={handleSaveWalletAddress}>
+ <Text style={styles.secondaryButtonText}>Enregistrer l’adresse</Text>
</TouchableOpacity>
+
+ {!!walletAddressInfo && <Text style={[styles.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
</View>
- {/* 2) PORTEFEUILLE */}
- <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Wallet" as never)}>
- <View style={[ui.card, compact && styles.cardCompact]}>
- <View style={styles.headerRow}>
- <Text style={[ui.title, compact && styles.titleCompact]}>Portefeuille</Text>
- <Chevron />
- </View>
+ {/* SOLDE + PRIX (comme le Web) */}
+ <View style={styles.rowGap}>
+ <View style={[styles.card, styles.half]}>
+ <Text style={styles.title}>Solde</Text>
+ <Text style={styles.bigValue}>
+ {selectedQty.toFixed(6)} {selectedCrypto}
+ </Text>
+ <Text style={styles.muted}>Source : wallet local (+ API BTC fusionné)</Text>
+ </View>
- <View style={ui.rowBetween}>
- <Text style={ui.value}>Valeur (top 3) :</Text>
- <Text style={ui.valueBold}>
- {portfolioPartialValue !== null ? `${portfolioPartialValue.toFixed(2)} ${settings.currency}` : "—"}
- </Text>
+ <View style={[styles.card, styles.half]}>
+ <View style={styles.rowBetween}>
+ <Text style={styles.title}>Prix</Text>
+ <TouchableOpacity onPress={handleManualRefreshPrice} style={styles.refreshBtn}>
+ <Text style={styles.refreshText}>Actualiser</Text>
+ </TouchableOpacity>
</View>
- {!!assetPricesError && (
- <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={2}>
- ⚠️ {assetPricesError}
- </Text>
- )}
+ <Text style={styles.bigValue}>
+ {(currentPrice ?? 0).toFixed(2)} {currency}
+ </Text>
- {portfolio.assets.length === 0 ? (
- <Text style={[ui.muted, { marginTop: 8 }]}>
- Aucun asset (ajoute BTC/ETH/SOL dans Portefeuille)
- </Text>
- ) : (
- <View style={{ marginTop: 8 }}>
- {topAssets.map((a) => (
- <View key={a.symbol} style={[ui.rowBetween, { marginTop: 4 }]}>
- <Text style={ui.muted}>{a.symbol}</Text>
- <Text style={ui.valueBold}>{a.quantity.toFixed(6)}</Text>
- </View>
- ))}
-
- {remainingCount > 0 && (
- <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
- +{remainingCount} autre(s) asset(s)
- </Text>
- )}
- </View>
- )}
+ <Text style={styles.muted}>
+ Pair : {pair} — Maj : {lastPriceUpdateMs ? new Date(lastPriceUpdateMs).toLocaleTimeString() : "—"}
+ </Text>
</View>
- </TouchableOpacity>
+ </View>
- {/* 3) URGENCE */}
- <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Alerts" as never)}>
- <View style={[ui.card, compact && styles.cardCompact]}>
- <View style={styles.headerRow}>
- <Text style={[ui.title, compact && styles.titleCompact]}>Alertes</Text>
- <Chevron />
- </View>
+ {/* SIGNAL (minimal) */}
+ <TouchableOpacity style={styles.card} onPress={() => navigation.navigate("Alerts" as never)} activeOpacity={0.85}>
+ <View style={styles.rowBetween}>
+ <Text style={styles.title}>Signal du marché</Text>
+ <Text style={styles.muted}>Ouvrir alertes</Text>
+ </View>
- {urgentAlert ? (
- <View style={[styles.urgentBox, compact && styles.urgentBoxCompact]}>
- <Text style={styles.urgentTitle} numberOfLines={2}>
- {urgentAlert.alertLevel} : {urgentAlert.reason}
- </Text>
- <Text style={ui.muted} numberOfLines={1}>
- {urgentAlert.action} {urgentAlert.pair} — {(urgentAlert.confidence * 100).toFixed(0)}%
- </Text>
- </View>
- ) : (
- <Text style={ui.muted}>Aucune alerte pour le moment.</Text>
- )}
+ {urgentAlert ? (
+ <>
+ <Text style={styles.signalAction}>{String(urgentAlert.action ?? "HOLD")}</Text>
+ <Text style={styles.muted} numberOfLines={2}>
+ {String(urgentAlert.alertLevel ?? "INFO")} — {String(urgentAlert.reason ?? urgentAlert.message ?? "—")}
+ </Text>
+ <Text style={styles.muted}>
+ {String(urgentAlert.pair ?? "")} — Confiance{" "}
+ {typeof urgentAlert.confidence === "number" ? `${Math.round(urgentAlert.confidence * 100)}%` : "—"}
+ </Text>
+ </>
+ ) : (
+ <Text style={styles.muted}>Aucune alerte pour le moment.</Text>
+ )}
+ </TouchableOpacity>
- <Text style={[ui.muted, { marginTop: compact ? 6 : 8 }]} numberOfLines={1}>
- Socket : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
- {socketError ? ` — ${socketError}` : ""}
- </Text>
+ {/* STRATÉGIE (bouton) */}
+ <TouchableOpacity style={styles.card} onPress={() => navigation.navigate("Strategy" as never)} activeOpacity={0.85}>
+ <View style={styles.rowBetween}>
+ <Text style={styles.title}>Stratégie</Text>
+ <Text style={styles.muted}>Configurer</Text>
</View>
+
+ <Text style={styles.muted} numberOfLines={2}>
+ (L’écran Stratégie reste inchangé — ici on ne fait que naviguer.)
+ </Text>
</TouchableOpacity>
- {/* 4) PRIX BTC */}
- <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("History" as never)}>
- <View style={[ui.card, compact && styles.cardCompact]}>
- <View style={styles.priceHeaderRow}>
- <View style={styles.headerRow}>
- <Text style={[ui.title, compact && styles.titleCompact]}>Prix BTC</Text>
- <Chevron />
- </View>
+ {/* ACTIONS (Acheter / Vendre) */}
+ <View style={styles.card}>
+ <Text style={styles.title}>Actions</Text>
- <TouchableOpacity
- style={[
- styles.refreshBtn,
- refreshing && styles.refreshBtnDisabled,
- compact && styles.refreshBtnCompact,
- ]}
- onPress={handleManualRefresh}
- disabled={refreshing}
- >
- <Text style={styles.refreshBtnText}>{refreshing ? "…" : "Actualiser"}</Text>
- </TouchableOpacity>
- </View>
+ <View style={styles.buySellRow}>
+ <TouchableOpacity style={styles.buyBtn} onPress={() => openTrade("BUY")}>
+ <Text style={styles.buySellText}>Acheter</Text>
+ </TouchableOpacity>
- <Text style={ui.muted} numberOfLines={1}>
- Dernière maj : {lastRefreshMs ? new Date(lastRefreshMs).toLocaleTimeString() : "—"}
- </Text>
+ <TouchableOpacity style={styles.sellBtn} onPress={() => openTrade("SELL")}>
+ <Text style={styles.buySellText}>Vendre</Text>
+ </TouchableOpacity>
+ </View>
- <View style={[styles.priceCard, compact && styles.priceCardCompact]}>
- <View style={ui.rowBetween}>
- <Text style={ui.value}>Prix BTC</Text>
- <Text style={styles.priceBig}>
- {summary.price.toFixed(2)} {settings.currency}
+ <Text style={[styles.muted, { marginTop: 10 }]} numberOfLines={2}>
+ Note : Acheter/Vendre = simulation (registre local). Pas de trading réel.
+ </Text>
+ </View>
+
+ {/* MODAL BUY/SELL */}
+ <Modal visible={tradeOpen} transparent animationType="fade" onRequestClose={() => setTradeOpen(false)}>
+ <View style={styles.modalBackdrop}>
+ <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined} style={styles.modalWrap}>
+ <View style={styles.modalCard}>
+ <Text style={styles.modalTitle}>
+ {tradeSide === "BUY" ? "Acheter" : "Vendre"} {selectedCrypto}
</Text>
+
+ <Text style={styles.muted}>
+ Prix : {(currentPrice ?? 0).toFixed(2)} {currency}
+ </Text>
+
+ <Text style={[styles.muted, { marginTop: 12 }]}>Quantité</Text>
+ <TextInput
+ value={tradeQty}
+ onChangeText={(t) => {
+ setTradeInfo(null);
+ setTradeQty(t);
+ }}
+ keyboardType="decimal-pad"
+ placeholder="0.01"
+ style={styles.input}
+ />
+
+ {!!tradeInfo && <Text style={styles.modalError}>{tradeInfo}</Text>}
+
+ <View style={styles.modalButtonsRow}>
+ <TouchableOpacity style={[styles.modalBtn, styles.modalBtnSecondary]} onPress={() => setTradeOpen(false)}>
+ <Text style={styles.modalBtnSecondaryText}>Annuler</Text>
+ </TouchableOpacity>
+
+ <TouchableOpacity style={[styles.modalBtn, styles.modalBtnPrimary]} onPress={() => void confirmTrade()}>
+ <Text style={styles.modalBtnPrimaryText}>Confirmer</Text>
+ </TouchableOpacity>
+ </View>
</View>
- </View>
+ </KeyboardAvoidingView>
</View>
+ </Modal>
+
+ {/* Bouton refresh global */}
+ <TouchableOpacity style={[styles.secondaryButton, { marginTop: 4 }]} onPress={() => void refreshAll()}>
+ <Text style={styles.secondaryButtonText}>Tout actualiser</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
}
const styles = StyleSheet.create({
- safeArea: {
- flex: 1,
- backgroundColor: ui.screen.backgroundColor,
- },
+ safeArea: { flex: 1, backgroundColor: "#0b1220" },
+ container: { padding: 16, paddingBottom: 28 },
- containerCompact: {
- padding: 12,
- },
+ centered: { flex: 1, alignItems: "center", justifyContent: "center", backgroundColor: "#0b1220" },
- cardCompact: {
+ banner: {
+ borderWidth: 1,
+ borderColor: "#f59e0b",
+ backgroundColor: "rgba(245,158,11,0.12)",
padding: 12,
- marginBottom: 10,
+ borderRadius: 12,
+ marginBottom: 12,
},
+ bannerText: { color: "#f59e0b", fontWeight: "800" },
- titleCompact: {
- marginBottom: 6,
- },
-
- bigCompact: {
- fontSize: 24,
- marginVertical: 4,
- },
-
- bannerWarning: {
- borderColor: "#ca8a04",
- },
- bannerWarningText: {
- color: "#ca8a04",
- },
- bannerCompact: {
- padding: 10,
- marginBottom: 10,
+ card: {
+ backgroundColor: "#ffffff",
+ borderRadius: 16,
+ padding: 14,
+ marginBottom: 12,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
},
- errorText: {
- color: "#dc2626",
- fontWeight: "900",
- },
+ rowBetween: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
+ rowGap: { flexDirection: "row", gap: 12, marginBottom: 12 },
+ half: { flex: 1, marginBottom: 0 },
- centerText: {
- textAlign: "center",
- },
+ title: { fontSize: 16, fontWeight: "900", color: "#0f172a" },
+ muted: { marginTop: 6, color: "#475569", fontWeight: "600" },
+ bold: { fontWeight: "900", color: "#0f172a" },
- boldInline: {
- fontWeight: "900",
- color: "#0f172a",
- },
+ bigValue: { marginTop: 10, fontSize: 22, fontWeight: "900", color: "#0f172a" },
+ signalAction: { marginTop: 10, fontSize: 24, fontWeight: "900", color: "#0f172a" },
- fullButton: {
- flexGrow: 0,
- flexBasis: "auto",
- width: "100%",
- marginTop: 12,
- },
- buttonCompact: {
+ cryptoRow: { flexDirection: "row", gap: 10, marginTop: 10 },
+ cryptoBtn: {
+ flex: 1,
paddingVertical: 10,
- marginTop: 10,
- },
-
- headerRow: {
- flexDirection: "row",
- justifyContent: "space-between",
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
alignItems: "center",
+ backgroundColor: "#fff",
},
+ cryptoBtnActive: { borderColor: "#0f172a" },
+ cryptoText: { fontWeight: "900", color: "#0f172a", opacity: 0.7 },
+ cryptoTextActive: { opacity: 1 },
- urgentBox: {
+ input: {
borderWidth: 1,
borderColor: "#e5e7eb",
- borderRadius: 10,
- padding: 10,
- backgroundColor: "#ffffff",
- },
- urgentBoxCompact: {
- padding: 8,
- },
- urgentTitle: {
- fontWeight: "900",
+ borderRadius: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ marginTop: 8,
+ backgroundColor: "#fff",
color: "#0f172a",
},
- priceHeaderRow: {
- flexDirection: "row",
- justifyContent: "space-between",
+ secondaryButton: {
+ marginTop: 10,
+ paddingVertical: 12,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
alignItems: "center",
- marginBottom: 8,
+ backgroundColor: "#fff",
},
+ secondaryButtonText: { fontWeight: "900", color: "#0f172a", opacity: 0.9 },
refreshBtn: {
paddingHorizontal: 12,
paddingVertical: 8,
- borderRadius: 10,
+ borderRadius: 12,
borderWidth: 1,
borderColor: "#e5e7eb",
backgroundColor: "#fff",
},
- refreshBtnCompact: {
- paddingVertical: 6,
- },
- refreshBtnDisabled: {
- opacity: 0.6,
- },
- refreshBtnText: {
- fontWeight: "900",
- color: "#0f172a",
- },
-
- priceCard: {
- marginTop: 10,
- borderWidth: 1,
- borderColor: "#e5e7eb",
- borderRadius: 10,
- padding: 12,
- backgroundColor: "#ffffff",
- },
- priceCardCompact: {
- paddingVertical: 10,
- },
- priceBig: {
- fontSize: 22,
- fontWeight: "900",
- color: "#0f172a",
- },
+ refreshText: { fontWeight: "900", color: "#0f172a" },
+
+ buySellRow: { flexDirection: "row", gap: 10, marginTop: 10 },
+ buyBtn: { flex: 1, backgroundColor: "#16a34a", paddingVertical: 12, borderRadius: 12, alignItems: "center" },
+ sellBtn: { flex: 1, backgroundColor: "#dc2626", paddingVertical: 12, borderRadius: 12, alignItems: "center" },
+ buySellText: { color: "#fff", fontWeight: "900" },
+
+ modalBackdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.35)", justifyContent: "center", padding: 16 },
+ modalWrap: { width: "100%" },
+ modalCard: { backgroundColor: "#fff", borderRadius: 16, padding: 16, borderWidth: 1, borderColor: "#e5e7eb" },
+ modalTitle: { fontSize: 18, fontWeight: "900", color: "#0f172a" },
+ modalError: { marginTop: 10, fontWeight: "800", color: "#dc2626" },
+
+ modalButtonsRow: { flexDirection: "row", gap: 10, marginTop: 14 },
+ modalBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: "center" },
+ modalBtnSecondary: { backgroundColor: "#fff", borderWidth: 1, borderColor: "#e5e7eb" },
+ modalBtnSecondaryText: { fontWeight: "900", color: "#0f172a" },
+ modalBtnPrimary: { backgroundColor: "#0f172a" },
+ modalBtnPrimaryText: { fontWeight: "900", color: "#fff" },
});
\ No newline at end of file