]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : creation fenetre Wallet
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 15:30:44 +0000 (16:30 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 15:30:44 +0000 (16:30 +0100)
Wallette/mobile/App.tsx
Wallette/mobile/src/models/Wallet.ts [new file with mode: 0644]
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/screens/WalletScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/utils/walletStorage.ts [new file with mode: 0644]

index c1f7b6fade61c3ff27e11f8518b968fe67e5a3b6..d662a45dccb0d51db56acb374f8a1f684fda82db 100644 (file)
@@ -8,6 +8,7 @@ import SettingsScreen from "./src/screens/SettingsScreen";
 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;
@@ -15,6 +16,7 @@ export type RootStackParamList = {
   History: undefined;
   Alerts: undefined;
   Strategy: undefined;
+  Wallet: undefined;
 };
 
 const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -28,7 +30,6 @@ export default function App() {
           component={DashboardScreen}
           options={({ navigation }) => ({
             title: "Dashboard",
-            // ✅ icônes header à droite
             headerRight: () => (
               <View style={{ flexDirection: "row", gap: 12 }}>
                 <TouchableOpacity onPress={() => navigation.navigate("Alerts")}>
@@ -47,29 +48,11 @@ export default function App() {
           })}
         />
 
-        <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>
   );
diff --git a/Wallette/mobile/src/models/Wallet.ts b/Wallette/mobile/src/models/Wallet.ts
new file mode 100644 (file)
index 0000000..f36769f
--- /dev/null
@@ -0,0 +1,11 @@
+/**
+ * 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
index 61905ddbacf1f018b7020f5073b829b580c38f54..1efa64830d506bbb22016ef041160b075baa654b 100644 (file)
@@ -23,12 +23,24 @@ import type { Alert } from "../types/Alert";
 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();
@@ -36,6 +48,7 @@ export default function DashboardScreen() {
 
   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);
@@ -59,15 +72,17 @@ export default function DashboardScreen() {
           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.");
@@ -174,6 +189,11 @@ export default function DashboardScreen() {
     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}>
@@ -190,7 +210,7 @@ export default function DashboardScreen() {
     );
   }
 
-  if (!summary || !settings) {
+  if (!summary || !settings || !wallet) {
     return (
       <View style={ui.centered}>
         <Text>Initialisation…</Text>
@@ -234,27 +254,32 @@ export default function DashboardScreen() {
           </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>
 
@@ -264,8 +289,7 @@ export default function DashboardScreen() {
                   {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>
             ) : (
@@ -284,20 +308,13 @@ export default function DashboardScreen() {
         </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}
               >
@@ -306,8 +323,7 @@ export default function DashboardScreen() {
             </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]}>
diff --git a/Wallette/mobile/src/screens/WalletScreen.tsx b/Wallette/mobile/src/screens/WalletScreen.tsx
new file mode 100644 (file)
index 0000000..0768379
--- /dev/null
@@ -0,0 +1,240 @@
+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
diff --git a/Wallette/mobile/src/utils/walletStorage.ts b/Wallette/mobile/src/utils/walletStorage.ts
new file mode 100644 (file)
index 0000000..f74b2aa
--- /dev/null
@@ -0,0 +1,39 @@
+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