import HistoryScreen from "./src/screens/HistoryScreen";
import AlertsScreen from "./src/screens/AlertsScreen";
import StrategyScreen from "./src/screens/StrategyScreen";
+import WalletScreen from "./src/screens/WalletScreen";
export type RootStackParamList = {
Dashboard: undefined;
History: undefined;
Alerts: undefined;
Strategy: undefined;
+ Wallet: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
component={DashboardScreen}
options={({ navigation }) => ({
title: "Dashboard",
- // ✅ icônes header à droite
headerRight: () => (
<View style={{ flexDirection: "row", gap: 12 }}>
<TouchableOpacity onPress={() => navigation.navigate("Alerts")}>
})}
/>
- <Stack.Screen
- name="Settings"
- component={SettingsScreen}
- options={{ title: "Paramètres" }}
- />
-
- <Stack.Screen
- name="History"
- component={HistoryScreen}
- options={{ title: "Historique" }}
- />
-
- <Stack.Screen
- name="Alerts"
- component={AlertsScreen}
- options={{ title: "Alertes" }}
- />
-
- <Stack.Screen
- name="Strategy"
- component={StrategyScreen}
- options={{ title: "Stratégie" }}
- />
+ <Stack.Screen name="Wallet" component={WalletScreen} options={{ title: "Portefeuille" }} />
+ <Stack.Screen name="Strategy" component={StrategyScreen} options={{ title: "Stratégie" }} />
+ <Stack.Screen name="Alerts" component={AlertsScreen} options={{ title: "Alertes" }} />
+ <Stack.Screen name="History" component={HistoryScreen} options={{ title: "Historique" }} />
+ <Stack.Screen name="Settings" component={SettingsScreen} options={{ title: "Paramètres" }} />
</Stack.Navigator>
</NavigationContainer>
);
--- /dev/null
+/**
+ * Wallet (Step 1)
+ * ---------------
+ * Mono-utilisateur, mono-crypto : on stocke seulement BTC.
+ * (Step 3 : on passera à plusieurs cryptos + valeur globale)
+ */
+export interface WalletState {
+ assetSymbol: "BTC";
+ quantity: number; // ex: 0.25
+ updatedAtMs: number;
+}
\ No newline at end of file
import { alertStore } from "../services/alertStore";
import { showAlertNotification } from "../services/notificationService";
+import { loadWallet } from "../utils/walletStorage";
+import type { WalletState } from "../models/Wallet";
+
/**
- * DashboardScreen (WF-01) — Responsive
- * -----------------------------------
- * Objectif : coller au mockup, éviter le scroll.
- * - On enlève le gros bloc "Actions" (navigation via header icons)
- * - Certaines cartes deviennent cliquables (tap-to-open)
+ * DashboardScreen (WF-01) — Responsive + No-scroll goal
+ * ----------------------------------------------------
+ * - Navigation via header icons (Alerts / History / Settings) dans App.tsx
+ * - Cartes cliquables :
+ * * Portefeuille -> Wallet
+ * * Urgence -> Alertes
+ * * Prix BTC -> Historique
+ * - Conseiller : bouton -> Stratégie
+ * - Socket.IO non bloquant + notifications locales
+ *
+ * Portefeuille (Step 1):
+ * - Quantité BTC stockée localement (AsyncStorage)
+ * - Valeur totale = quantity * prix BTC
+ * - Ligne info: "BTC @ prix actuel"
*/
export default function DashboardScreen() {
const { height } = useWindowDimensions();
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
+ const [wallet, setWallet] = useState<WalletState | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
setError(null);
setLoading(true);
- const [dashboardData, userSettings] = await Promise.all([
+ const [dashboardData, userSettings, walletData] = await Promise.all([
fetchDashboardSummary(),
loadSettings(),
+ loadWallet(),
]);
if (!isActive) return;
setSummary(dashboardData);
setSettings(userSettings);
+ setWallet(walletData);
setLastRefreshMs(Date.now());
} catch {
if (isActive) setError("Impossible de charger le dashboard.");
return liveAlerts[0];
}, [liveAlerts]);
+ const walletTotalValue = useMemo(() => {
+ if (!wallet || !summary) return null;
+ return wallet.quantity * summary.price;
+ }, [wallet, summary]);
+
if (loading) {
return (
<View style={ui.centered}>
);
}
- if (!summary || !settings) {
+ if (!summary || !settings || !wallet) {
return (
<View style={ui.centered}>
<Text>Initialisation…</Text>
</TouchableOpacity>
</View>
- {/* 2) PORTEFEUILLE (pas encore cliquable car pas d’écran Wallet dédié) */}
- <View style={[ui.card, compact && styles.cardCompact]}>
- <Text style={[ui.title, compact && styles.titleCompact]}>Portefeuille</Text>
+ {/* 2) PORTEFEUILLE — cliquable => Wallet */}
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Wallet" as never)}>
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Portefeuille</Text>
- <View style={ui.rowBetween}>
- <Text style={ui.value}>Valeur Totale :</Text>
- <Text style={ui.valueBold}>10 000 {settings.currency}</Text>
- </View>
+ <View style={ui.rowBetween}>
+ <Text style={ui.value}>Quantité BTC :</Text>
+ <Text style={ui.valueBold}>{wallet.quantity.toFixed(6)} BTC</Text>
+ </View>
- {!compact && (
- <Text style={[ui.muted, { marginTop: 6 }]}>
- Step 1 : mono-utilisateur / mono-crypto
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Valeur Totale :</Text>
+ <Text style={ui.valueBold}>
+ {walletTotalValue !== null ? `${walletTotalValue.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </View>
+
+ {/* ✅ Ligne informative : BTC @ prix actuel */}
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
+ BTC @ {summary.price.toFixed(2)} {settings.currency}
</Text>
- )}
- </View>
+ </View>
+ </TouchableOpacity>
{/* 3) URGENCE — cliquable => Alertes */}
- <TouchableOpacity
- activeOpacity={0.85}
- onPress={() => navigation.navigate("Alerts" as never)}
- >
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Alerts" as never)}>
<View style={[ui.card, compact && styles.cardCompact]}>
<Text style={[ui.title, compact && styles.titleCompact]}>Urgence</Text>
{urgentAlert.alertLevel} : {urgentAlert.reason}
</Text>
<Text style={ui.muted} numberOfLines={1}>
- {urgentAlert.action} {urgentAlert.pair} —{" "}
- {(urgentAlert.confidence * 100).toFixed(0)}%
+ {urgentAlert.action} {urgentAlert.pair} — {(urgentAlert.confidence * 100).toFixed(0)}%
</Text>
</View>
) : (
</TouchableOpacity>
{/* 4) PRIX BTC — cliquable => Historique */}
- <TouchableOpacity
- activeOpacity={0.85}
- onPress={() => navigation.navigate("History" as never)}
- >
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("History" as never)}>
<View style={[ui.card, compact && styles.cardCompact]}>
<View style={styles.priceHeaderRow}>
<Text style={[ui.title, compact && styles.titleCompact]}>Prix BTC</Text>
<TouchableOpacity
- style={[
- styles.refreshBtn,
- refreshing && styles.refreshBtnDisabled,
- compact && styles.refreshBtnCompact,
- ]}
+ style={[styles.refreshBtn, refreshing && styles.refreshBtnDisabled, compact && styles.refreshBtnCompact]}
onPress={handleManualRefresh}
disabled={refreshing}
>
</View>
<Text style={ui.muted} numberOfLines={1}>
- Dernière maj :{" "}
- {lastRefreshMs ? new Date(lastRefreshMs).toLocaleTimeString() : "—"}
+ Dernière maj : {lastRefreshMs ? new Date(lastRefreshMs).toLocaleTimeString() : "—"}
</Text>
<View style={[styles.priceCard, compact && styles.priceCardCompact]}>
--- /dev/null
+import { View, Text, StyleSheet, TouchableOpacity, TextInput, Alert as RNAlert } from "react-native";
+import { useMemo, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useFocusEffect } from "@react-navigation/native";
+import { useCallback } from "react";
+
+import { ui } from "../components/ui/uiStyles";
+import { loadWallet, saveWallet, clearWallet } from "../utils/walletStorage";
+import type { WalletState } from "../models/Wallet";
+import { fetchDashboardSummary } from "../services/dashboardService";
+import { loadSettings } from "../utils/settingsStorage";
+import type { UserSettings } from "../models/UserSettings";
+
+/**
+ * WalletScreen (WF-03 Step 1)
+ * ---------------------------
+ * Mono-utilisateur / mono-crypto : BTC uniquement.
+ * - L'utilisateur encode la quantité de BTC qu'il possède
+ * - On calcule la valeur estimée via le prix BTC du dashboard
+ * - Stockage local (AsyncStorage) pour ne pas dépendre de l'API
+ */
+export default function WalletScreen() {
+ const [wallet, setWallet] = useState<WalletState | null>(null);
+ const [settings, setSettings] = useState<UserSettings | null>(null);
+
+ // Prix BTC actuel (mock aujourd'hui, API demain)
+ const [btcPrice, setBtcPrice] = useState<number | null>(null);
+
+ // input texte (évite les bugs de virgule/points)
+ const [qtyInput, setQtyInput] = useState<string>("0");
+
+ const [info, setInfo] = useState<string | null>(null);
+
+ // Recharge quand l’écran reprend le focus (retour depuis autre page)
+ useFocusEffect(
+ useCallback(() => {
+ let active = true;
+
+ async function init() {
+ setInfo(null);
+
+ const [w, s, dash] = await Promise.all([
+ loadWallet(),
+ loadSettings(),
+ fetchDashboardSummary(),
+ ]);
+
+ if (!active) return;
+
+ setWallet(w);
+ setSettings(s);
+ setBtcPrice(dash.price);
+
+ setQtyInput(String(w.quantity));
+ }
+
+ init();
+
+ return () => {
+ active = false;
+ };
+ }, [])
+ );
+
+ const parsedQty = useMemo(() => {
+ const normalized = qtyInput.replace(",", ".").trim();
+ const val = Number(normalized);
+ if (!Number.isFinite(val)) return null;
+ if (val < 0) return null;
+ return val;
+ }, [qtyInput]);
+
+ const totalValue = useMemo(() => {
+ if (parsedQty === null || btcPrice === null) return null;
+ return parsedQty * btcPrice;
+ }, [parsedQty, btcPrice]);
+
+ const lastUpdatedLabel = useMemo(() => {
+ if (!wallet) return "—";
+ return new Date(wallet.updatedAtMs).toLocaleString();
+ }, [wallet]);
+
+ const handleSave = async () => {
+ if (!wallet) return;
+
+ if (parsedQty === null) {
+ setInfo("Quantité invalide. Exemple : 0.25");
+ return;
+ }
+
+ const updated: WalletState = {
+ ...wallet,
+ quantity: parsedQty,
+ updatedAtMs: Date.now(),
+ };
+
+ await saveWallet(updated);
+ setWallet(updated);
+ setInfo("Portefeuille sauvegardé ✅");
+ };
+
+ const handleClear = () => {
+ RNAlert.alert(
+ "Réinitialiser le portefeuille ?",
+ "Cela remet la quantité BTC à 0 (stockage local).",
+ [
+ { text: "Annuler", style: "cancel" },
+ {
+ text: "Réinitialiser",
+ style: "destructive",
+ onPress: async () => {
+ await clearWallet();
+ const fresh = await loadWallet();
+ setWallet(fresh);
+ setQtyInput("0");
+ setInfo("Portefeuille réinitialisé ✅");
+ },
+ },
+ ]
+ );
+ };
+
+ if (!wallet || !settings) {
+ return (
+ <View style={ui.centered}>
+ <Text>Chargement du portefeuille…</Text>
+ </View>
+ );
+ }
+
+ return (
+ <SafeAreaView style={styles.safeArea}>
+ <View style={ui.container}>
+ <Text style={styles.screenTitle}>Portefeuille</Text>
+
+ {/* Carte BTC */}
+ <View style={ui.card}>
+ <Text style={ui.title}>BTC</Text>
+
+ <Text style={ui.muted}>Quantité détenue</Text>
+
+ <TextInput
+ value={qtyInput}
+ onChangeText={setQtyInput}
+ keyboardType="decimal-pad"
+ placeholder="ex: 0.25"
+ style={styles.input}
+ />
+
+ <Text style={[ui.muted, { marginTop: 10 }]}>
+ Prix BTC actuel :{" "}
+ <Text style={styles.boldInline}>
+ {btcPrice !== null ? `${btcPrice.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </Text>
+
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Valeur estimée :{" "}
+ <Text style={styles.boldInline}>
+ {totalValue !== null ? `${totalValue.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </Text>
+
+ {/* ✅ Dernière mise à jour */}
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Dernière mise à jour :{" "}
+ <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
+ </Text>
+
+ <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
+ <Text style={ui.buttonText}>Enregistrer</Text>
+ </TouchableOpacity>
+
+ <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
+ <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
+ </TouchableOpacity>
+
+ {!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
+ </View>
+
+ {/* Carte info Step */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Step 1</Text>
+ <Text style={ui.muted}>
+ Mono-utilisateur / mono-crypto (BTC). Step 3 : portefeuille multi-cryptos + valeur globale.
+ </Text>
+ </View>
+ </View>
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: ui.screen.backgroundColor,
+ },
+ screenTitle: {
+ fontSize: 22,
+ fontWeight: "900",
+ marginBottom: 12,
+ color: "#0f172a",
+ },
+ boldInline: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+
+ input: {
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ borderRadius: 10,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ marginTop: 8,
+ backgroundColor: "#fff",
+ color: "#0f172a",
+ },
+
+ fullButton: {
+ flexGrow: 0,
+ flexBasis: "auto",
+ width: "100%",
+ marginTop: 12,
+ },
+
+ secondaryButton: {
+ marginTop: 10,
+ paddingVertical: 10,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ alignItems: "center",
+ backgroundColor: "#fff",
+ },
+ secondaryButtonText: {
+ fontWeight: "900",
+ color: "#dc2626",
+ },
+});
\ No newline at end of file
--- /dev/null
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import type { WalletState } from "../models/Wallet";
+
+const KEY = "walletStep1";
+
+/**
+ * Valeurs par défaut : wallet vide.
+ */
+const DEFAULT_WALLET: WalletState = {
+ assetSymbol: "BTC",
+ quantity: 0,
+ updatedAtMs: Date.now(),
+};
+
+export async function loadWallet(): Promise<WalletState> {
+ const raw = await AsyncStorage.getItem(KEY);
+ if (!raw) return DEFAULT_WALLET;
+
+ try {
+ const parsed = JSON.parse(raw) as Partial<WalletState>;
+ return { ...DEFAULT_WALLET, ...parsed };
+ } catch {
+ return DEFAULT_WALLET;
+ }
+}
+
+export async function saveWallet(wallet: WalletState): Promise<void> {
+ const safe: WalletState = {
+ assetSymbol: "BTC",
+ quantity: Number.isFinite(wallet.quantity) ? wallet.quantity : 0,
+ updatedAtMs: Date.now(),
+ };
+
+ await AsyncStorage.setItem(KEY, JSON.stringify(safe));
+}
+
+export async function clearWallet(): Promise<void> {
+ await AsyncStorage.removeItem(KEY);
+}
\ No newline at end of file