import { alertStore } from "../services/alertStore";
import { showAlertNotification } from "../services/notificationService";
-import { loadWallet } from "../utils/walletStorage";
-import type { WalletState } from "../models/Wallet";
+// ✅ Step 3 portfolio
+import { loadPortfolio } from "../utils/portfolioStorage";
+import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
+import { getMockPrice } from "../mocks/prices.mock";
/**
- * DashboardScreen (WF-01) — Responsive + No-scroll goal
- * ----------------------------------------------------
+ * DashboardScreen (WF-01) — Step 3
+ * --------------------------------
+ * - Portefeuille multi-cryptos (portfolioStep3, AsyncStorage)
+ * - Valeur globale = somme(quantity * prix mock)
* - Cartes cliquables (chevron subtil) :
* * Portefeuille -> Wallet
* * Urgence -> Alertes
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
- const [wallet, setWallet] = useState<WalletState | null>(null);
+
+ // ✅ Step 3 portfolio
+ const [portfolio, setPortfolio] = useState<PortfolioState | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
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;
setError(null);
setLoading(true);
- const [dashboardData, userSettings, walletData] = await Promise.all([
+ const [dashboardData, userSettings, portfolioData] = await Promise.all([
fetchDashboardSummary(),
loadSettings(),
- loadWallet(),
+ loadPortfolio(),
]);
if (!isActive) return;
setSummary(dashboardData);
setSettings(userSettings);
- setWallet(walletData);
+ setPortfolio(portfolioData);
+
setLastRefreshMs(Date.now());
} catch {
if (isActive) setError("Impossible de charger le dashboard.");
}, [])
);
+ /**
+ * Refresh auto (si activé)
+ */
useEffect(() => {
if (!settings) return;
if (settings.refreshMode !== "auto") return;
};
}, [settings]);
+ /**
+ * Refresh manuel
+ */
const handleManualRefresh = async () => {
try {
setRefreshing(true);
}
};
+ /**
+ * Socket.IO (non bloquant)
+ * Step 1/2/3 : userId de test
+ */
useEffect(() => {
if (!settings) return;
};
}, [settings]);
+ /**
+ * Urgence : 1 seule alerte, la plus importante.
+ */
const urgentAlert: Alert | null = useMemo(() => {
if (liveAlerts.length === 0) return null;
return liveAlerts[0];
}, [liveAlerts]);
- const walletTotalValue = useMemo(() => {
- if (!wallet || !summary) return null;
- return wallet.quantity * summary.price;
- }, [wallet, summary]);
+ /**
+ * Valeur globale du portefeuille (Step 3)
+ * total = somme(quantity * prix(mock))
+ */
+ const portfolioTotalValue = useMemo(() => {
+ if (!portfolio) return 0;
+
+ return portfolio.assets.reduce((sum, a) => {
+ const price = getMockPrice(a.symbol);
+ if (price === null) return sum;
+ return sum + a.quantity * price;
+ }, 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]);
+
+ const remainingCount = useMemo(() => {
+ if (!portfolio) return 0;
+ return Math.max(0, portfolio.assets.length - topAssets.length);
+ }, [portfolio, topAssets]);
+
+ // Loading / erreurs bloquantes
if (loading) {
return (
<View style={ui.centered}>
);
}
- if (!summary || !settings || !wallet) {
+ if (!summary || !settings || !portfolio) {
return (
<View style={ui.centered}>
<Text>Initialisation…</Text>
}
const Chevron = () => (
- <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
+ <Ionicons
+ name="chevron-forward-outline"
+ size={18}
+ color="#0f172a"
+ style={{ opacity: 0.35 }}
+ />
);
return (
</TouchableOpacity>
</View>
- {/* 2) PORTEFEUILLE — cliquable => Wallet */}
+ {/* 2) PORTEFEUILLE — cliquable => Wallet (Step 3) */}
<TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Wallet" as never)}>
<View style={[ui.card, compact && styles.cardCompact]}>
<View style={styles.headerRow}>
</View>
<View style={ui.rowBetween}>
- <Text style={ui.value}>Quantité BTC :</Text>
- <Text style={ui.valueBold}>{wallet.quantity.toFixed(6)} BTC</Text>
- </View>
-
- <View style={[ui.rowBetween, { marginTop: 6 }]}>
- <Text style={ui.value}>Valeur Totale :</Text>
+ <Text style={ui.value}>Valeur globale :</Text>
<Text style={ui.valueBold}>
- {walletTotalValue !== null ? `${walletTotalValue.toFixed(2)} ${settings.currency}` : "—"}
+ {portfolioTotalValue.toFixed(2)} {settings.currency}
</Text>
</View>
- <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
- BTC @ {summary.price.toFixed(2)} {settings.currency}
+ {/* Résumé assets */}
+ {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>
+ )}
+
+ {/* Ligne informative BTC @ prix (mock) */}
+ <Text style={[ui.muted, { marginTop: 8 }]} numberOfLines={1}>
+ BTC @ {summary.price.toFixed(2)} {settings.currency} (source mock)
</Text>
</View>
</TouchableOpacity>
</View>
<TouchableOpacity
- style={[styles.refreshBtn, refreshing && styles.refreshBtnDisabled, compact && styles.refreshBtnCompact]}
+ style={[
+ styles.refreshBtn,
+ refreshing && styles.refreshBtnDisabled,
+ compact && styles.refreshBtnCompact,
+ ]}
onPress={handleManualRefresh}
disabled={refreshing}
>
marginTop: 10,
},
- // ✅ header row + chevron icon
headerRow: {
flexDirection: "row",
justifyContent: "space-between",
-import { View, Text, StyleSheet, TouchableOpacity, TextInput, Alert as RNAlert } from "react-native";
-import { useMemo, useState } from "react";
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ TextInput,
+ Alert as RNAlert,
+ FlatList,
+} from "react-native";
+import { useCallback, 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 type { PortfolioAsset, PortfolioState } from "../models/Portfolio";
+import { loadPortfolio, savePortfolio, clearPortfolio } from "../utils/portfolioStorage";
+import { getMockPrice } from "../mocks/prices.mock";
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
+ * WalletScreen (Step 3)
+ * ---------------------
+ * Mono-user / multi-cryptos
+ * - liste d'assets (BTC, ETH, SOL...)
+ * - quantité par asset
+ * - valeur globale du portefeuille
+ *
+ * Aujourd'hui : prix mock
+ * Demain : prix via GET /api/price/current?pair=XXX/EUR
*/
export default function WalletScreen() {
- const [wallet, setWallet] = useState<WalletState | null>(null);
+ const [portfolio, setPortfolio] = useState<PortfolioState | 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)
+ // Ajout asset
+ const [symbolInput, setSymbolInput] = useState<string>("BTC");
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(),
- ]);
-
+ const [p, s] = await Promise.all([loadPortfolio(), loadSettings()]);
if (!active) return;
- setWallet(w);
+ setPortfolio(p);
setSettings(s);
- setBtcPrice(dash.price);
-
- setQtyInput(String(w.quantity));
}
init();
}, [])
);
+ const lastUpdatedLabel = useMemo(() => {
+ if (!portfolio) return "—";
+ return new Date(portfolio.updatedAtMs).toLocaleString();
+ }, [portfolio]);
+
const parsedQty = useMemo(() => {
const normalized = qtyInput.replace(",", ".").trim();
const val = Number(normalized);
return val;
}, [qtyInput]);
+ const normalizedSymbol = useMemo(() => symbolInput.toUpperCase().trim(), [symbolInput]);
+
const totalValue = useMemo(() => {
- if (parsedQty === null || btcPrice === null) return null;
- return parsedQty * btcPrice;
- }, [parsedQty, btcPrice]);
+ if (!portfolio) return 0;
- const lastUpdatedLabel = useMemo(() => {
- if (!wallet) return "—";
- return new Date(wallet.updatedAtMs).toLocaleString();
- }, [wallet]);
+ return portfolio.assets.reduce((sum, a) => {
+ const price = getMockPrice(a.symbol);
+ if (price === null) return sum;
+ return sum + a.quantity * price;
+ }, 0);
+ }, [portfolio]);
- const handleSave = async () => {
- if (!wallet) return;
+ const handleAddOrUpdate = async () => {
+ if (!portfolio) return;
+
+ setInfo(null);
+
+ if (!normalizedSymbol || normalizedSymbol.length < 2) {
+ setInfo("Symbole invalide (ex: BTC).");
+ return;
+ }
if (parsedQty === null) {
setInfo("Quantité invalide. Exemple : 0.25");
return;
}
- const updated: WalletState = {
- ...wallet,
- quantity: parsedQty,
+ const price = getMockPrice(normalizedSymbol);
+ if (price === null) {
+ setInfo("Prix inconnu (mock). Essayez: BTC, ETH, SOL, ADA.");
+ return;
+ }
+
+ const existingIndex = portfolio.assets.findIndex((a) => a.symbol === normalizedSymbol);
+
+ let updatedAssets: PortfolioAsset[];
+ if (existingIndex >= 0) {
+ // update
+ updatedAssets = portfolio.assets.map((a) =>
+ a.symbol === normalizedSymbol ? { ...a, quantity: parsedQty } : a
+ );
+ } else {
+ // add
+ updatedAssets = [...portfolio.assets, { symbol: normalizedSymbol, quantity: parsedQty }];
+ }
+
+ const updated: PortfolioState = {
+ assets: updatedAssets,
updatedAtMs: Date.now(),
};
- await saveWallet(updated);
- setWallet(updated);
- setInfo("Portefeuille sauvegardé ✅");
+ await savePortfolio(updated);
+ setPortfolio(updated);
+
+ setInfo(existingIndex >= 0 ? "Asset mis à jour ✅" : "Asset ajouté ✅");
+ };
+
+ const handleDelete = (symbol: string) => {
+ if (!portfolio) return;
+
+ RNAlert.alert(
+ `Supprimer ${symbol} ?`,
+ "Cette action retire l’asset du portefeuille local.",
+ [
+ { text: "Annuler", style: "cancel" },
+ {
+ text: "Supprimer",
+ style: "destructive",
+ onPress: async () => {
+ const updated: PortfolioState = {
+ assets: portfolio.assets.filter((a) => a.symbol !== symbol),
+ updatedAtMs: Date.now(),
+ };
+ await savePortfolio(updated);
+ setPortfolio(updated);
+ setInfo(`${symbol} supprimé ✅`);
+ },
+ },
+ ]
+ );
};
const handleClear = () => {
RNAlert.alert(
"Réinitialiser le portefeuille ?",
- "Cela remet la quantité BTC à 0 (stockage local).",
+ "Cela supprime tous les assets du stockage local.",
[
{ text: "Annuler", style: "cancel" },
{
text: "Réinitialiser",
style: "destructive",
onPress: async () => {
- await clearWallet();
- const fresh = await loadWallet();
- setWallet(fresh);
- setQtyInput("0");
+ await clearPortfolio();
+ const fresh = await loadPortfolio();
+ setPortfolio(fresh);
setInfo("Portefeuille réinitialisé ✅");
},
},
);
};
- if (!wallet || !settings) {
+ if (!portfolio || !settings) {
return (
<View style={ui.centered}>
<Text>Chargement du portefeuille…</Text>
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}` : "—"}
+ <FlatList
+ contentContainerStyle={ui.container}
+ data={portfolio.assets}
+ keyExtractor={(it) => it.symbol}
+ ListHeaderComponent={
+ <View>
+ <Text style={styles.screenTitle}>Portefeuille</Text>
+
+ {/* Résumé global */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Résumé</Text>
+
+ <View style={ui.rowBetween}>
+ <Text style={ui.value}>Valeur globale :</Text>
+ <Text style={ui.valueBold}>
+ {totalValue.toFixed(2)} {settings.currency}
+ </Text>
+ </View>
+
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Dernière mise à jour :{" "}
+ <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
+ </Text>
+
+ <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
+ <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
+ </TouchableOpacity>
+ </View>
+
+ {/* Ajouter / modifier */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Ajouter / Modifier un asset</Text>
+
+ <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL, ADA)</Text>
+ <TextInput
+ value={symbolInput}
+ onChangeText={setSymbolInput}
+ autoCapitalize="characters"
+ placeholder="BTC"
+ style={styles.input}
+ />
+
+ <Text style={[ui.muted, { marginTop: 10 }]}>Quantité</Text>
+ <TextInput
+ value={qtyInput}
+ onChangeText={setQtyInput}
+ keyboardType="decimal-pad"
+ placeholder="0.25"
+ style={styles.input}
+ />
+
+ <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleAddOrUpdate}>
+ <Text style={ui.buttonText}>Enregistrer</Text>
+ </TouchableOpacity>
+
+ {!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
+ <Text style={[ui.muted, { marginTop: 10 }]}>
+ Prix utilisés = mock pour Step 3 (API à brancher plus tard).
+ </Text>
+ </View>
+
+ <Text style={[ui.muted, { marginBottom: 10 }]}>
+ Liste des assets :
</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>
+ </View>
+ }
+ ListEmptyComponent={
+ <View style={ui.card}>
+ <Text style={ui.title}>Aucun asset</Text>
+ <Text style={ui.muted}>Ajoutez BTC/ETH/SOL… pour commencer.</Text>
+ </View>
+ }
+ renderItem={({ item }) => {
+ const price = getMockPrice(item.symbol);
+ const value = price !== null ? item.quantity * price : null;
+
+ return (
+ <View style={ui.card}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.valueBold}>{item.symbol}</Text>
+ <TouchableOpacity onPress={() => handleDelete(item.symbol)}>
+ <Text style={styles.deleteText}>Supprimer</Text>
+ </TouchableOpacity>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 8 }]}>
+ <Text style={ui.value}>Quantité</Text>
+ <Text style={ui.valueBold}>{item.quantity.toFixed(6)}</Text>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Prix (mock)</Text>
+ <Text style={ui.valueBold}>
+ {price !== null ? `${price.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Valeur</Text>
+ <Text style={ui.valueBold}>
+ {value !== null ? `${value.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </View>
+ </View>
+ );
+ }}
+ />
</SafeAreaView>
);
}
fontWeight: "900",
color: "#dc2626",
},
+
+ deleteText: {
+ fontWeight: "900",
+ color: "#dc2626",
+ },
});
\ No newline at end of file