]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Update mobile from thibaud-mobile
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Fri, 27 Feb 2026 21:43:26 +0000 (22:43 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Fri, 27 Feb 2026 21:43:26 +0000 (22:43 +0100)
22 files changed:
Wallette/mobile/.expo/README.md [deleted file]
Wallette/mobile/.expo/devices.json [deleted file]
Wallette/mobile/App.tsx
Wallette/mobile/package-lock.json
Wallette/mobile/package.json
Wallette/mobile/src/config/api.ts [new file with mode: 0644]
Wallette/mobile/src/config/env.ts
Wallette/mobile/src/models/AccountProfile.ts [deleted file]
Wallette/mobile/src/models/AuthUser.ts [new file with mode: 0644]
Wallette/mobile/src/screens/AboutScreen.tsx
Wallette/mobile/src/screens/AccountScreen.tsx
Wallette/mobile/src/screens/AuthScreen.tsx
Wallette/mobile/src/screens/SettingsScreen.tsx
Wallette/mobile/src/screens/TutorialScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/services/api/alertsApi.ts [new file with mode: 0644]
Wallette/mobile/src/services/api/authApi.ts [new file with mode: 0644]
Wallette/mobile/src/services/api/priceApi.ts [new file with mode: 0644]
Wallette/mobile/src/services/api/signalApi.ts [new file with mode: 0644]
Wallette/mobile/src/services/api/strategyApi.ts [new file with mode: 0644]
Wallette/mobile/src/utils/accountStorage.ts [deleted file]
Wallette/mobile/src/utils/authUsersStorage.ts [new file with mode: 0644]
Wallette/mobile/src/utils/tutorialStorage.ts [new file with mode: 0644]

diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md
deleted file mode 100644 (file)
index ce8c4b6..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-> Why do I have a folder named ".expo" in my project?
-
-The ".expo" folder is created when an Expo project is started using "expo start" command.
-
-> What do the files contain?
-
-- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
-- "settings.json": contains the server configuration that is used to serve the application manifest.
-
-> Should I commit the ".expo" folder?
-
-No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
-Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
diff --git a/Wallette/mobile/.expo/devices.json b/Wallette/mobile/.expo/devices.json
deleted file mode 100644 (file)
index 5efff6c..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "devices": []
-}
index d33c4c3850c8c3c8a2aeaf1733701fca46f32d82..1eb69817cce3bd5424b462a3db81323a8cf9c97a 100644 (file)
@@ -1,4 +1,7 @@
-import { NavigationContainer, createNavigationContainerRef } from "@react-navigation/native";
+import {
+  NavigationContainer,
+  createNavigationContainerRef,
+} from "@react-navigation/native";
 import { createNativeStackNavigator } from "@react-navigation/native-stack";
 import { TouchableOpacity, View, Text, Alert as RNAlert } from "react-native";
 import { Ionicons } from "@expo/vector-icons";
@@ -10,13 +13,18 @@ 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 TutorialScreen from "./src/screens/TutorialScreen";
+
 import AccountScreen from "./src/screens/AccountScreen";
 import AboutScreen from "./src/screens/AboutScreen";
 
-import { loadSession, clearSession } from "./src/utils/sessionStorage";
 import AccountMenu from "./src/components/AccountMenu";
 
+import { loadSession, clearSession } from "./src/utils/sessionStorage";
+import { hasSeenTutorial } from "./src/utils/tutorialStorage";
+
 export type RootStackParamList = {
   Dashboard: undefined;
   Settings: undefined;
@@ -30,15 +38,21 @@ export type RootStackParamList = {
 
 const Stack = createNativeStackNavigator<RootStackParamList>();
 
-// ✅ navigationRef (permet de naviguer hors des screens, ex: depuis un Modal)
+// ✅ navigationRef (navigate depuis le Modal AccountMenu)
 export const navigationRef = createNavigationContainerRef<RootStackParamList>();
 
 export default function App() {
   const [ready, setReady] = useState(false);
+
+  // Auth state
   const [isAuthed, setIsAuthed] = useState(false);
+  const [sessionEmail, setSessionEmail] = useState<string>("—");
+
+  // Tutorial gate
+  const [needsTutorial, setNeedsTutorial] = useState(false);
 
+  // Account menu
   const [menuVisible, setMenuVisible] = useState(false);
-  const [sessionEmail, setSessionEmail] = useState<string>("—");
 
   useEffect(() => {
     let active = true;
@@ -47,8 +61,18 @@ export default function App() {
       const s = await loadSession();
       if (!active) return;
 
-      setIsAuthed(!!s);
+      const authed = !!s;
+      setIsAuthed(authed);
       setSessionEmail(s?.email ?? "—");
+
+      if (authed) {
+        const seen = await hasSeenTutorial();
+        if (!active) return;
+        setNeedsTutorial(!seen);
+      } else {
+        setNeedsTutorial(false);
+      }
+
       setReady(true);
     }
 
@@ -69,13 +93,13 @@ export default function App() {
           await clearSession();
           setIsAuthed(false);
           setSessionEmail("—");
+          setNeedsTutorial(false);
         },
       },
     ]);
   };
 
   const go = (route: keyof RootStackParamList) => {
-    // ✅ safe guard : navigation prête ?
     if (!navigationRef.isReady()) return;
     navigationRef.navigate(route);
   };
@@ -88,7 +112,7 @@ export default function App() {
     );
   }
 
-  // Pas connecté -> écran Auth
+  // 1) Pas connecté -> Auth (hors stack)
   if (!isAuthed) {
     return (
       <AuthScreen
@@ -96,14 +120,29 @@ export default function App() {
           const s = await loadSession();
           setSessionEmail(s?.email ?? "—");
           setIsAuthed(true);
+
+          // Après login, on vérifie le tutoriel
+          const seen = await hasSeenTutorial();
+          setNeedsTutorial(!seen);
+        }}
+      />
+    );
+  }
+
+  // 2) Connecté mais tuto pas vu -> Tutoriel (hors stack)
+  if (needsTutorial) {
+    return (
+      <TutorialScreen
+        onDone={() => {
+          setNeedsTutorial(false);
         }}
       />
     );
   }
 
+  // 3) Connecté + tuto vu -> App normale
   return (
     <NavigationContainer ref={navigationRef}>
-      {/* ✅ Modal menu Compte (navigue grâce au navigationRef) */}
       <AccountMenu
         visible={menuVisible}
         email={sessionEmail}
@@ -123,27 +162,70 @@ export default function App() {
               <View style={{ flexDirection: "row", gap: 12 }}>
                 {/* 👤 Menu Compte */}
                 <TouchableOpacity onPress={() => setMenuVisible(true)}>
-                  <Ionicons name="person-circle-outline" size={24} color="#0f172a" />
+                  <Ionicons
+                    name="person-circle-outline"
+                    size={24}
+                    color="#0f172a"
+                  />
                 </TouchableOpacity>
 
                 {/* ⚙️ Paramètres */}
-                <TouchableOpacity onPress={() => navigation.navigate("Settings")}>
-                  <Ionicons name="settings-outline" size={22} color="#0f172a" />
+                <TouchableOpacity
+                  onPress={() => navigation.navigate("Settings")}
+                >
+                  <Ionicons
+                    name="settings-outline"
+                    size={22}
+                    color="#0f172a"
+                  />
                 </TouchableOpacity>
               </View>
             ),
           })}
         />
 
-        <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="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.Screen name="Settings" options={{ title: "Paramètres" }}>
+        {() => (
+        <SettingsScreen
+            onRequestTutorial={() => {
+            // Force le tuto à s'afficher hors stack
+            setNeedsTutorial(true);
+          }}
+         />
+        )}
+        </Stack.Screen>
 
-        <Stack.Screen name="Account" component={AccountScreen} options={{ title: "Compte" }} />
-        <Stack.Screen name="About" component={AboutScreen} options={{ title: "À propos" }} />
+        <Stack.Screen
+          name="Account"
+          component={AccountScreen}
+          options={{ title: "Compte" }}
+        />
+        <Stack.Screen
+          name="About"
+          component={AboutScreen}
+          options={{ title: "À propos" }}
+        />
       </Stack.Navigator>
     </NavigationContainer>
   );
index fd361d289845da2662cb77af19fe9f45703b1cc8..6377cc91b295926e087aa4190b13fe103d5d5b55 100644 (file)
@@ -13,6 +13,7 @@
         "@react-navigation/native": "^7.1.28",
         "@react-navigation/native-stack": "^7.13.0",
         "expo": "~54.0.33",
+        "expo-crypto": "~15.0.8",
         "expo-notifications": "~0.32.16",
         "expo-status-bar": "~3.0.9",
         "react": "19.1.0",
         "react-native": "*"
       }
     },
+    "node_modules/expo-crypto": {
+      "version": "15.0.8",
+      "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz",
+      "integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.0"
+      },
+      "peerDependencies": {
+        "expo": "*"
+      }
+    },
     "node_modules/expo-font": {
       "version": "14.0.11",
       "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
index 0db1739d0bb07caebe29b336277c1a21de80481c..813958d9d5e1e46ee372f1a125bd0b261b9b0e92 100644 (file)
@@ -14,6 +14,7 @@
     "@react-navigation/native": "^7.1.28",
     "@react-navigation/native-stack": "^7.13.0",
     "expo": "~54.0.33",
+    "expo-crypto": "~15.0.8",
     "expo-notifications": "~0.32.16",
     "expo-status-bar": "~3.0.9",
     "react": "19.1.0",
diff --git a/Wallette/mobile/src/config/api.ts b/Wallette/mobile/src/config/api.ts
new file mode 100644 (file)
index 0000000..19f8d94
--- /dev/null
@@ -0,0 +1,41 @@
+import { Platform } from "react-native";
+
+/**
+ * API Gateway (microservices)
+ * ---------------------------
+ * - Téléphone physique : utiliser l'IP du PC sur le réseau local
+ * - Android émulateur : 10.0.2.2
+ * - iOS simulateur : localhost (souvent OK)
+ *
+ * IMPORTANT : le mobile doit appeler UNIQUEMENT le gateway:
+ *   http://<HOST>:3000/api/...
+ * Socket.IO passe aussi via le gateway:
+ *   http://<HOST>:3000   (proxy /socket.io/*)
+ */
+
+// ✅ Change uniquement cette valeur pour ton PC
+const DEV_LAN_IP = "192.168.129.121";
+
+export function getGatewayHost(): string {
+  if (__DEV__) {
+    // Téléphone physique / LAN
+    return DEV_LAN_IP;
+  }
+  // En prod (ou build), tu mettras un vrai domaine/IP si nécessaire
+  return DEV_LAN_IP;
+}
+
+export function getGatewayBaseUrl(): string {
+  const host =
+    Platform.OS === "android" && !__DEV__
+      ? "10.0.2.2"
+      : getGatewayHost();
+
+  return `http://${host}:3000`;
+}
+
+// REST base
+export const API_BASE_URL = `${getGatewayBaseUrl()}/api`;
+
+// Socket base (pas /api)
+export const SOCKET_BASE_URL = `${getGatewayBaseUrl()}`;
\ No newline at end of file
index 36a9d9a6cb7ed054187e118e8374bce18a5e640e..e394a9ba3e90d26318efd6c22ef74920439c9a77 100644 (file)
@@ -1,3 +1,37 @@
-// ⚠️ En dev téléphone réel : URL = IP du PC sur le même réseau
-export const SERVER_URL = "http://192.168.129.121:3000";
-export const WS_URL = "ws://192.168.129.121:3000"; // optionnel, Socket.IO peut aussi utiliser http(s)
\ No newline at end of file
+import { Platform } from "react-native";
+
+/**
+ * env.ts
+ * ------
+ * Objectif : centraliser les URLs réseau pour le mobile.
+ *
+ * IMPORTANT (microservices + gateway) :
+ * - Le mobile parle UNIQUEMENT au gateway : http://<HOST>:3000
+ * - REST = http://<HOST>:3000/api/...
+ * - Socket.IO = http://<HOST>:3000  (proxy /socket.io/* via gateway)
+ *
+ * Téléphone physique :
+ * - <HOST> = IP du PC sur le Wi-Fi (ex: 192.168.x.x)
+ *
+ * Émulateurs (si un jour) :
+ * - Android: 10.0.2.2
+ * - iOS: localhost
+ */
+
+const DEV_LAN_IP = "192.168.129.121";
+
+function resolveHost(): string {
+  // On part sur téléphone physique (ton cas).
+  // Si un jour tu testes sur émulateur Android, tu peux mettre une condition:
+  // if (Platform.OS === "android" && __DEV__ && isEmulator) return "10.0.2.2";
+  return DEV_LAN_IP;
+}
+
+// Base gateway (HTTP)
+export const GATEWAY_BASE_URL = `http://${resolveHost()}:3000`;
+
+// REST (via gateway)
+export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
+
+// Socket.IO (via gateway)
+export const SERVER_URL = GATEWAY_BASE_URL;
\ No newline at end of file
diff --git a/Wallette/mobile/src/models/AccountProfile.ts b/Wallette/mobile/src/models/AccountProfile.ts
deleted file mode 100644 (file)
index 93ebefb..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface AccountProfile {
-  displayName: string;
-  email: string;
-  // On ne stocke JAMAIS un mot de passe en clair.
-  // Ici on simule seulement un champ pour l'UI (Step 4 sans API).
-  updatedAtMs: number;
-}
\ No newline at end of file
diff --git a/Wallette/mobile/src/models/AuthUser.ts b/Wallette/mobile/src/models/AuthUser.ts
new file mode 100644 (file)
index 0000000..1e13ac9
--- /dev/null
@@ -0,0 +1,14 @@
+export interface AuthUser {
+  userId: string;
+
+  email: string;
+  username: string;
+
+  // Optionnel : nom/prénom ou nom affiché
+  displayName?: string;
+
+  // On ne stocke JAMAIS le mot de passe en clair
+  passwordHash: string;
+
+  createdAtMs: number;
+}
\ No newline at end of file
index ab72035298716abd25a4a15cc61e264a2f659872..02e683ecc58cb9e77a067f859c5eec1ac59d9b34 100644 (file)
-import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert as RNAlert } from "react-native";
-import { useEffect, useState } from "react";
+import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
 import { SafeAreaView } from "react-native-safe-area-context";
+import { Ionicons } from "@expo/vector-icons";
+import { useEffect, useState } from "react";
 
 import { ui } from "../components/ui/uiStyles";
 import { loadSession } from "../utils/sessionStorage";
-import { loadAccountProfile, saveAccountProfile } from "../utils/accountStorage";
-import type { AccountProfile } from "../models/AccountProfile";
+import { findUserById } from "../utils/authUsersStorage";
 
 /**
- * AccountScreen (Step 4 - sans API)
- * --------------------------------
- * UI prête pour email + mot de passe.
- * En réalité :
- * - on ne stocke pas le mdp
- * - on sauvegarde seulement displayName + email (local)
- *
- * Quand l'API arrive : POST /auth/update-profile + update password.
+ * AboutScreen 
+ * ------------------------
+ * "Détails du compte" + sections À propos / Support / Version.
+ * Les infos affichées viennent du compte local (AuthUsersStorage) :
+ * - displayName (optionnel)
+ * - username
+ * - email
  */
-export default function AccountScreen() {
-  const [profile, setProfile] = useState<AccountProfile | null>(null);
-  const [userId, setUserId] = useState<string>("");
-
-  const [displayName, setDisplayName] = useState<string>("");
-  const [email, setEmail] = useState<string>("");
-
-  // Champs UI (non persistés) pour mot de passe
-  const [password, setPassword] = useState<string>("");
-  const [password2, setPassword2] = useState<string>("");
-
-  const [info, setInfo] = useState<string | null>(null);
+export default function AboutScreen() {
+  const [email, setEmail] = useState<string>("—");
+  const [displayName, setDisplayName] = useState<string>("Utilisateur");
+  const [username, setUsername] = useState<string>("—");
+  const [userId, setUserId] = useState<string>("—");
 
   useEffect(() => {
-    async function init() {
-      const [session, p] = await Promise.all([loadSession(), loadAccountProfile()]);
-      setUserId(session?.userId ?? "");
-      setProfile(p);
-      setDisplayName(p.displayName);
-      setEmail(p.email);
-    }
-    init();
-  }, []);
+    let active = true;
 
-  if (!profile) {
-    return (
-      <View style={ui.centered}>
-        <Text>Chargement du compte…</Text>
-      </View>
-    );
-  }
+    async function init() {
+      const session = await loadSession();
+      if (!active) return;
+
+      if (!session) {
+        setEmail("—");
+        setDisplayName("Utilisateur");
+        setUsername("—");
+        setUserId("—");
+        return;
+      }
 
-  const handleSave = async () => {
-    setInfo(null);
+      setUserId(session.userId);
+      setEmail(session.email);
 
-    const normalizedEmail = email.trim().toLowerCase();
-    const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail);
+      const user = await findUserById(session.userId);
+      if (!active) return;
 
-    if (!isValidEmail) {
-      setInfo("Email invalide.");
-      return;
+      setUsername(user?.username ?? session.userId.replace("user_", ""));
+      setDisplayName(user?.displayName ?? "Utilisateur");
+      setEmail(user?.email ?? session.email);
     }
 
-    // Mot de passe : UI prête, mais pas stocké sans API
-    if (password || password2) {
-      if (password.length < 6) {
-        setInfo("Mot de passe trop court (min 6).");
-        return;
-      }
-      if (password !== password2) {
-        setInfo("Les mots de passe ne correspondent pas.");
-        return;
-      }
-
-      // Sans API : on ne peut pas réellement changer le mdp
-      RNAlert.alert(
-        "Mot de passe",
-        "Sans API, ce changement n’est pas appliqué (UI prête).",
-        [{ text: "OK" }]
-      );
-    }
+    init();
 
-    const updated: AccountProfile = {
-      displayName: displayName.trim() || "Utilisateur",
-      email: normalizedEmail,
-      updatedAtMs: Date.now(),
+    return () => {
+      active = false;
     };
+  }, []);
 
-    await saveAccountProfile(updated);
-    setProfile(updated);
-    setPassword("");
-    setPassword2("");
-    setInfo("Profil sauvegardé ✅");
-  };
+  const Row = ({
+    icon,
+    title,
+    subtitle,
+  }: {
+    icon: any;
+    title: string;
+    subtitle?: string;
+  }) => (
+    <View style={styles.row}>
+      <Ionicons name={icon} size={18} color="#0f172a" style={{ opacity: 0.75 }} />
+      <View style={{ flex: 1 }}>
+        <Text style={styles.rowTitle}>{title}</Text>
+        {!!subtitle && <Text style={ui.muted}>{subtitle}</Text>}
+      </View>
+      <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.25 }} />
+    </View>
+  );
 
   return (
     <SafeAreaView style={styles.safeArea}>
       <View style={ui.container}>
-        <Text style={styles.title}>Modification du compte</Text>
+        <Text style={styles.title}>Détails du compte</Text>
 
+        {/* Carte profil (style WF-09) */}
         <View style={ui.card}>
-          <Text style={ui.title}>Informations</Text>
-
-          <Text style={ui.muted}>UserId (session)</Text>
-          <Text style={styles.mono}>{userId || "—"}</Text>
+          <View style={styles.profileRow}>
+            <Ionicons
+              name="person-circle-outline"
+              size={52}
+              color="#0f172a"
+              style={{ opacity: 0.75 }}
+            />
+            <View style={{ flex: 1 }}>
+              <Text style={styles.profileName}>{displayName}</Text>
+              <Text style={ui.muted} numberOfLines={1}>{email}</Text>
+              <Text style={[ui.muted, { marginTop: 2 }]} numberOfLines={1}>
+                @{username}
+              </Text>
+            </View>
+          </View>
+
+          <View style={styles.metaBox}>
+            <Text style={styles.metaLabel}>UserId</Text>
+            <Text style={styles.metaValue} numberOfLines={1}>{userId}</Text>
+          </View>
+        </View>
 
-          <Text style={[ui.muted, { marginTop: 10 }]}>Nom affiché</Text>
-          <TextInput value={displayName} onChangeText={setDisplayName} style={styles.input} />
+        {/* À propos */}
+        <View style={ui.card}>
+          <Text style={ui.title}>À propos</Text>
 
-          <Text style={[ui.muted, { marginTop: 10 }]}>Email</Text>
-          <TextInput
-            value={email}
-            onChangeText={setEmail}
-            autoCapitalize="none"
-            keyboardType="email-address"
-            style={styles.input}
+          <Row icon="sparkles-outline" title="Wall-e-tte" subtitle="Conseiller crypto — projet PDW 2025-2026" />
+          <Row
+            icon="shield-checkmark-outline"
+            title="Avertissement"
+            subtitle="Outil éducatif : ce n’est pas un conseil financier."
           />
         </View>
 
+        {/* Support */}
         <View style={ui.card}>
-          <Text style={ui.title}>Mot de passe (UI prête)</Text>
-          <Text style={ui.muted}>
-            Le changement réel du mot de passe sera branché quand l’API auth sera disponible.
-          </Text>
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
-          <TextInput value={password} onChangeText={setPassword} secureTextEntry style={styles.input} />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
-          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry style={styles.input} />
+          <Text style={ui.title}>Support</Text>
+          <Row icon="help-circle-outline" title="Aide" subtitle="Comprendre les écrans et les signaux." />
+          <Row icon="mail-outline" title="Contact" subtitle="Support de l’équipe (à définir)." />
         </View>
 
-        {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
+        {/* Version */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Version</Text>
+          <Text style={ui.muted}>App : 1.0.0</Text>
+          <Text style={[ui.muted, { marginTop: 6 }]}>Build : Step 4 (sans API)</Text>
+        </View>
 
-        <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
-          <Text style={ui.buttonText}>Sauvegarder</Text>
+        {/* Bouton discret (optionnel) */}
+        <TouchableOpacity style={styles.secondaryButton}>
+          <Text style={styles.secondaryButtonText}>Voir les licences (optionnel)</Text>
         </TouchableOpacity>
       </View>
     </SafeAreaView>
@@ -142,23 +141,46 @@ export default function AccountScreen() {
 const styles = StyleSheet.create({
   safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
   title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
-  fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 },
 
-  mono: {
-    marginTop: 6,
+  profileRow: { flexDirection: "row", alignItems: "center", gap: 12 },
+  profileName: { fontSize: 18, fontWeight: "900", color: "#0f172a" },
+
+  metaBox: {
+    marginTop: 12,
+    borderTopWidth: 1,
+    borderTopColor: "#e5e7eb",
+    paddingTop: 10,
+  },
+  metaLabel: {
+    fontWeight: "900",
+    color: "#0f172a",
+    opacity: 0.7,
+    marginBottom: 4,
+  },
+  metaValue: {
     fontFamily: "monospace",
     color: "#0f172a",
-    opacity: 0.8,
+    opacity: 0.85,
   },
 
-  input: {
+  row: {
+    flexDirection: "row",
+    alignItems: "center",
+    gap: 10,
+    paddingVertical: 12,
+    borderTopWidth: 1,
+    borderTopColor: "#e5e7eb",
+  },
+  rowTitle: { fontWeight: "900", color: "#0f172a" },
+
+  secondaryButton: {
+    marginTop: 6,
+    paddingVertical: 12,
+    borderRadius: 12,
     borderWidth: 1,
     borderColor: "#e5e7eb",
-    borderRadius: 10,
-    paddingHorizontal: 12,
-    paddingVertical: 10,
-    marginTop: 8,
+    alignItems: "center",
     backgroundColor: "#fff",
-    color: "#0f172a",
   },
+  secondaryButtonText: { fontWeight: "900", color: "#0f172a", opacity: 0.8 },
 });
\ No newline at end of file
index ab72035298716abd25a4a15cc61e264a2f659872..d7be82965aa1e71cef344427cb864c449a2e10ac 100644 (file)
@@ -1,47 +1,68 @@
-import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert as RNAlert } from "react-native";
+import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
 import { useEffect, useState } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
 
 import { ui } from "../components/ui/uiStyles";
 import { loadSession } from "../utils/sessionStorage";
-import { loadAccountProfile, saveAccountProfile } from "../utils/accountStorage";
-import type { AccountProfile } from "../models/AccountProfile";
+import { findUserById, updateCurrentUserProfile } from "../utils/authUsersStorage";
 
 /**
  * AccountScreen (Step 4 - sans API)
  * --------------------------------
- * UI prête pour email + mot de passe.
- * En réalité :
- * - on ne stocke pas le mdp
- * - on sauvegarde seulement displayName + email (local)
+ * - Email : affiché mais NON modifiable (important)
+ * - Username : affiché mais NON modifiable (sinon userId local casse)
+ * - DisplayName : modifiable
+ * - Mot de passe : modifiable (hash local)
  *
- * Quand l'API arrive : POST /auth/update-profile + update password.
+ * Plus tard : remplacement par une route API (DB).
  */
 export default function AccountScreen() {
-  const [profile, setProfile] = useState<AccountProfile | null>(null);
+  const [loading, setLoading] = useState(true);
+
   const [userId, setUserId] = useState<string>("");
 
+  const [email, setEmail] = useState<string>(""); // affichage only
+  const [username, setUsername] = useState<string>(""); // affichage only
   const [displayName, setDisplayName] = useState<string>("");
-  const [email, setEmail] = useState<string>("");
 
-  // Champs UI (non persistés) pour mot de passe
   const [password, setPassword] = useState<string>("");
   const [password2, setPassword2] = useState<string>("");
 
   const [info, setInfo] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
 
   useEffect(() => {
+    let active = true;
+
     async function init() {
-      const [session, p] = await Promise.all([loadSession(), loadAccountProfile()]);
-      setUserId(session?.userId ?? "");
-      setProfile(p);
-      setDisplayName(p.displayName);
-      setEmail(p.email);
+      const session = await loadSession();
+      if (!session) {
+        if (!active) return;
+        setError("Session absente.");
+        setLoading(false);
+        return;
+      }
+
+      const u = await findUserById(session.userId);
+
+      if (!active) return;
+
+      setUserId(session.userId);
+      setEmail(u?.email ?? session.email);
+      setUsername(u?.username ?? session.userId.replace("user_", ""));
+      setDisplayName(u?.displayName ?? "");
+
+      setLoading(false);
     }
+
     init();
+
+    return () => {
+      active = false;
+    };
   }, []);
 
-  if (!profile) {
+  if (loading) {
     return (
       <View style={ui.centered}>
         <Text>Chargement du compte…</Text>
@@ -51,45 +72,37 @@ export default function AccountScreen() {
 
   const handleSave = async () => {
     setInfo(null);
+    setError(null);
 
-    const normalizedEmail = email.trim().toLowerCase();
-    const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail);
-
-    if (!isValidEmail) {
-      setInfo("Email invalide.");
-      return;
-    }
+    // Mot de passe : optionnel, mais si rempli => validations
+    const wantsPasswordChange = password.trim().length > 0 || password2.trim().length > 0;
 
-    // Mot de passe : UI prête, mais pas stocké sans API
-    if (password || password2) {
-      if (password.length < 6) {
-        setInfo("Mot de passe trop court (min 6).");
+    if (wantsPasswordChange) {
+      if (password.trim().length < 6) {
+        setError("Mot de passe trop court (min 6).");
         return;
       }
       if (password !== password2) {
-        setInfo("Les mots de passe ne correspondent pas.");
+        setError("Les mots de passe ne correspondent pas.");
         return;
       }
-
-      // Sans API : on ne peut pas réellement changer le mdp
-      RNAlert.alert(
-        "Mot de passe",
-        "Sans API, ce changement n’est pas appliqué (UI prête).",
-        [{ text: "OK" }]
-      );
     }
 
-    const updated: AccountProfile = {
-      displayName: displayName.trim() || "Utilisateur",
-      email: normalizedEmail,
-      updatedAtMs: Date.now(),
-    };
+    const res = await updateCurrentUserProfile({
+      displayName,
+      newPassword: wantsPasswordChange ? password : undefined,
+    });
 
-    await saveAccountProfile(updated);
-    setProfile(updated);
+    if (!res.ok) {
+      setError(res.message);
+      return;
+    }
+
+    // reset password fields
     setPassword("");
     setPassword2("");
-    setInfo("Profil sauvegardé ✅");
+
+    setInfo("Profil sauvegardé ✅ (local, en attendant l’API).");
   };
 
   return (
@@ -100,35 +113,37 @@ export default function AccountScreen() {
         <View style={ui.card}>
           <Text style={ui.title}>Informations</Text>
 
-          <Text style={ui.muted}>UserId (session)</Text>
+          <Text style={ui.muted}>UserId</Text>
           <Text style={styles.mono}>{userId || "—"}</Text>
 
+          <Text style={[ui.muted, { marginTop: 10 }]}>Email (non modifiable)</Text>
+          <TextInput value={email} editable={false} style={[styles.input, styles.inputDisabled]} />
+
+          <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur (non modifiable)</Text>
+          <TextInput value={username} editable={false} style={[styles.input, styles.inputDisabled]} />
+
           <Text style={[ui.muted, { marginTop: 10 }]}>Nom affiché</Text>
-          <TextInput value={displayName} onChangeText={setDisplayName} style={styles.input} />
-
-          <Text style={[ui.muted, { marginTop: 10 }]}>Email</Text>
-          <TextInput
-            value={email}
-            onChangeText={setEmail}
-            autoCapitalize="none"
-            keyboardType="email-address"
-            style={styles.input}
-          />
+          <TextInput value={displayName} onChangeText={setDisplayName} placeholder="ex: Thibaud M." style={styles.input} />
+
+          <Text style={[ui.muted, { marginTop: 12 }]}>
+            ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation).
+          </Text>
         </View>
 
         <View style={ui.card}>
-          <Text style={ui.title}>Mot de passe (UI prête)</Text>
+          <Text style={ui.title}>Mot de passe</Text>
           <Text style={ui.muted}>
-            Le changement réel du mot de passe sera branché quand l’API auth sera disponible.
+            Change optionnel. (Local pour l’instant, sera relié à l’API plus tard.)
           </Text>
 
           <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
-          <TextInput value={password} onChangeText={setPassword} secureTextEntry style={styles.input} />
+          <TextInput value={password} onChangeText={setPassword} secureTextEntry placeholder="min 6 caractères" style={styles.input} />
 
           <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
-          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry style={styles.input} />
+          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry placeholder="retaper le mot de passe" style={styles.input} />
         </View>
 
+        {!!error && <Text style={styles.errorText}>{error}</Text>}
         {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
 
         <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
@@ -141,8 +156,8 @@ export default function AccountScreen() {
 
 const styles = StyleSheet.create({
   safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
+
   title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
-  fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 },
 
   mono: {
     marginTop: 6,
@@ -161,4 +176,14 @@ const styles = StyleSheet.create({
     backgroundColor: "#fff",
     color: "#0f172a",
   },
+
+  inputDisabled: {
+    backgroundColor: "#f3f4f6",
+    color: "#111827",
+    opacity: 0.8,
+  },
+
+  fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginBottom: 10 },
+
+  errorText: { color: "#dc2626", fontWeight: "900", marginBottom: 10 },
 });
\ No newline at end of file
index 8e15dac9348e0dbdd561fc5517377d3813537560..8ac998653cf639a4185778fd385440232115d168 100644 (file)
@@ -1,44 +1,87 @@
 import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
-import { useMemo, useState } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
+import { useMemo, useState } from "react";
 
 import { ui } from "../components/ui/uiStyles";
 import { saveSession } from "../utils/sessionStorage";
+import { createUser, verifyLogin } from "../utils/authUsersStorage";
 
 /**
- * AuthScreen (Step 4 - sans API)
- * ------------------------------
- * Simule un login local :
- * - Email -> userId stable
- * - Session stockée en AsyncStorage
+ * AuthScreen (Step 4 - local, sans API)
+ * ------------------------------------
+ * Mode Connexion + Mode Création de compte.
+ * - Identifiant: email OU username
+ * - Mot de passe obligatoire
  *
- * Plus tard : on remplace par une auth REST réelle.
+ * Plus tard : on remplacera createUser/verifyLogin par des appels REST.
  */
 export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => void }) {
-  const [email, setEmail] = useState<string>("demo@example.com");
+  const [mode, setMode] = useState<"login" | "register">("login");
   const [error, setError] = useState<string | null>(null);
 
-  const normalizedEmail = useMemo(() => email.trim().toLowerCase(), [email]);
+  // Connexion
+  const [login, setLogin] = useState("demo@example.com");
+  const [password, setPassword] = useState("");
+
+  // Register
+  const [email, setEmail] = useState("demo@example.com");
+  const [username, setUsername] = useState("demo");
+  const [displayName, setDisplayName] = useState("");
+  const [password2, setPassword2] = useState("");
 
-  const isValidEmail = useMemo(() => {
-    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail);
-  }, [normalizedEmail]);
+  const isValidEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val.trim().toLowerCase());
+  const isValidUsername = (val: string) => /^[a-zA-Z0-9_]{3,20}$/.test(val.trim());
 
-  const makeUserId = (mail: string) => `user_${mail.replace(/[^a-z0-9]/g, "_")}`;
+  const title = useMemo(() => (mode === "login" ? "Connexion" : "Créer un compte"), [mode]);
 
   const handleLogin = async () => {
     setError(null);
 
-    if (!isValidEmail) {
-      setError("Email invalide.");
-      return;
-    }
+    const l = login.trim();
+    const p = password;
 
-    const userId = makeUserId(normalizedEmail);
+    if (!l) return setError("Veuillez entrer un email ou un nom d’utilisateur.");
+    if (!p || p.length < 6) return setError("Mot de passe invalide (min 6).");
+
+    const res = await verifyLogin({ login: l, password: p });
+    if (!res.ok) return setError(res.message);
 
     await saveSession({
-      userId,
-      email: normalizedEmail,
+      userId: res.user.userId,
+      email: res.user.email,
+      createdAtMs: Date.now(),
+    });
+
+    onAuthenticated();
+  };
+
+  const handleRegister = async () => {
+    setError(null);
+
+    const e = email.trim().toLowerCase();
+    const u = username.trim();
+    const d = displayName.trim();
+    const p1 = password;
+    const p2 = password2;
+
+    if (!isValidEmail(e)) return setError("Email invalide.");
+    if (!isValidUsername(u)) return setError("Username invalide (3-20, lettres/chiffres/_).");
+    if (!p1 || p1.length < 6) return setError("Mot de passe trop court (min 6).");
+    if (p1 !== p2) return setError("Les mots de passe ne correspondent pas.");
+
+    const res = await createUser({
+      email: e,
+      username: u,
+      displayName: d || undefined,
+      password: p1,
+    });
+
+    if (!res.ok) return setError(res.message);
+
+    // auto-login après création
+    await saveSession({
+      userId: res.user.userId,
+      email: res.user.email,
       createdAtMs: Date.now(),
     });
 
@@ -48,29 +91,124 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () =>
   return (
     <SafeAreaView style={styles.safeArea}>
       <View style={ui.container}>
-        <Text style={styles.title}>Connexion</Text>
+        <Text style={styles.title}>{title}</Text>
+
+        {/* Tabs */}
+        <View style={styles.tabsRow}>
+          <TouchableOpacity
+            style={[styles.tab, mode === "login" && styles.tabActive]}
+            onPress={() => {
+              setMode("login");
+              setError(null);
+            }}
+          >
+            <Text style={[styles.tabText, mode === "login" && styles.tabTextActive]}>
+              Connexion
+            </Text>
+          </TouchableOpacity>
+
+          <TouchableOpacity
+            style={[styles.tab, mode === "register" && styles.tabActive]}
+            onPress={() => {
+              setMode("register");
+              setError(null);
+            }}
+          >
+            <Text style={[styles.tabText, mode === "register" && styles.tabTextActive]}>
+              Créer un compte
+            </Text>
+          </TouchableOpacity>
+        </View>
 
         <View style={ui.card}>
-          <Text style={ui.title}>Email</Text>
+          {mode === "login" ? (
+            <>
+              <Text style={ui.muted}>Email ou nom d’utilisateur</Text>
+              <TextInput
+                value={login}
+                onChangeText={setLogin}
+                autoCapitalize="none"
+                placeholder="ex: demo@example.com ou demo"
+                style={styles.input}
+              />
 
-          <TextInput
-            value={email}
-            onChangeText={setEmail}
-            autoCapitalize="none"
-            keyboardType="email-address"
-            placeholder="ex: demo@example.com"
-            style={styles.input}
-          />
+              <Text style={[ui.muted, { marginTop: 10 }]}>Mot de passe</Text>
+              <TextInput
+                value={password}
+                onChangeText={setPassword}
+                secureTextEntry
+                placeholder="min 6 caractères"
+                style={styles.input}
+              />
 
-          {!!error && <Text style={styles.errorText}>{error}</Text>}
+              {!!error && <Text style={styles.errorText}>{error}</Text>}
 
-          <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleLogin}>
-            <Text style={ui.buttonText}>Se connecter</Text>
-          </TouchableOpacity>
+              <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleLogin}>
+                <Text style={ui.buttonText}>Se connecter</Text>
+              </TouchableOpacity>
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>
+                (Mode local sans API : destiné au développement / démo.)
+              </Text>
+            </>
+          ) : (
+            <>
+              <Text style={ui.muted}>Email</Text>
+              <TextInput
+                value={email}
+                onChangeText={setEmail}
+                autoCapitalize="none"
+                keyboardType="email-address"
+                placeholder="ex: demo@example.com"
+                style={styles.input}
+              />
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur</Text>
+              <TextInput
+                value={username}
+                onChangeText={setUsername}
+                autoCapitalize="none"
+                placeholder="ex: demo_123"
+                style={styles.input}
+              />
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>Nom / Prénom (optionnel)</Text>
+              <TextInput
+                value={displayName}
+                onChangeText={setDisplayName}
+                placeholder="ex: Thibaud M."
+                style={styles.input}
+              />
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>Mot de passe</Text>
+              <TextInput
+                value={password}
+                onChangeText={setPassword}
+                secureTextEntry
+                placeholder="min 6 caractères"
+                style={styles.input}
+              />
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>Confirmer</Text>
+              <TextInput
+                value={password2}
+                onChangeText={setPassword2}
+                secureTextEntry
+                placeholder="retaper le mot de passe"
+                style={styles.input}
+              />
 
-          <Text style={[ui.muted, { marginTop: 10 }]}>
-            Step 4 (sans API) : session locale. Plus tard, auth réelle via serveur.
-          </Text>
+              {!!error && <Text style={styles.errorText}>{error}</Text>}
+
+              <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleRegister}>
+                <Text style={ui.buttonText}>Créer le compte</Text>
+              </TouchableOpacity>
+
+              <Text style={[ui.muted, { marginTop: 10 }]}>
+                (Sans API : stockage local + hash SHA-256 pour éviter le mot de passe en clair.)
+              </Text>
+            </>
+          )}
         </View>
       </View>
     </SafeAreaView>
@@ -78,16 +216,30 @@ export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () =>
 }
 
 const styles = StyleSheet.create({
-  safeArea: {
+  safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
+
+  title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
+
+  tabsRow: {
+    flexDirection: "row",
+    gap: 10,
+    marginBottom: 12,
+  },
+  tab: {
     flex: 1,
-    backgroundColor: ui.screen.backgroundColor,
+    paddingVertical: 10,
+    borderRadius: 12,
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    backgroundColor: "#fff",
+    alignItems: "center",
   },
-  title: {
-    fontSize: 22,
-    fontWeight: "900",
-    marginBottom: 12,
-    color: "#0f172a",
+  tabActive: {
+    borderColor: "#0f172a",
   },
+  tabText: { fontWeight: "900", color: "#0f172a", opacity: 0.7 },
+  tabTextActive: { opacity: 1 },
+
   input: {
     borderWidth: 1,
     borderColor: "#e5e7eb",
@@ -98,15 +250,8 @@ const styles = StyleSheet.create({
     backgroundColor: "#fff",
     color: "#0f172a",
   },
-  fullButton: {
-    flexGrow: 0,
-    flexBasis: "auto",
-    width: "100%",
-    marginTop: 12,
-  },
-  errorText: {
-    marginTop: 10,
-    color: "#dc2626",
-    fontWeight: "900",
-  },
+
+  fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginTop: 12 },
+
+  errorText: { marginTop: 10, color: "#dc2626", fontWeight: "900" },
 });
\ No newline at end of file
index 4f753fb66eb8640676ba003bbe1cafad8b316cbc..aa1540a601aaa3edc5fea563cadb1d2e8c3c9f59 100644 (file)
@@ -1,4 +1,4 @@
-import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
+import { View, Text, StyleSheet, TouchableOpacity, Alert as RNAlert } from "react-native";
 import { useEffect, useState } from "react";
 import { SafeAreaView } from "react-native-safe-area-context";
 
@@ -6,26 +6,29 @@ 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";
+import { setSeenTutorial } from "../utils/tutorialStorage";
 
 /**
- * SettingsScreen (Step 4)
- * ----------------------
- * Paramètres stockés par userId (via settingsStorage).
- * Ajout : bouton Déconnexion.
+ * SettingsScreen
+ * --------------
+ * - Settings par userId (via settingsStorage)
+ * - Notifications (local)
+ * - Revoir le tutoriel (remet hasSeenTutorial à false)
  */
-export default function SettingsScreen({ onLogout }: { onLogout?: () => void }) {
+export default function SettingsScreen({
+  onRequestTutorial,
+}: {
+  onRequestTutorial?: () => void;
+}) {
   const [settings, setSettings] = useState<UserSettings | null>(null);
   const [infoMessage, setInfoMessage] = useState<string | null>(null);
-  const [sessionLabel, setSessionLabel] = useState<string>("");
 
   useEffect(() => {
     async function init() {
-      const [data, session] = await Promise.all([loadSettings(), loadSession()]);
+      const data = await loadSettings();
       setSettings(data);
-      setSessionLabel(session?.email ?? "—");
     }
     init();
   }, []);
@@ -53,19 +56,19 @@ export default function SettingsScreen({ onLogout }: { onLogout?: () => void })
 
     if (settings.notificationsEnabled) {
       setSettings({ ...settings, notificationsEnabled: false });
+      setInfoMessage("Notifications désactivées (pense à sauvegarder).");
       return;
     }
 
     const granted = await requestNotificationPermission();
-
     if (!granted) {
-      setInfoMessage("Permission refusée : notifications désactivées.");
       setSettings({ ...settings, notificationsEnabled: false });
+      setInfoMessage("Permission refusée : notifications désactivées.");
       return;
     }
 
-    setInfoMessage("Notifications activées ✅ (pense à sauvegarder)");
     setSettings({ ...settings, notificationsEnabled: true });
+    setInfoMessage("Notifications activées ✅ (pense à sauvegarder).");
   };
 
   const handleSave = async () => {
@@ -73,9 +76,21 @@ export default function SettingsScreen({ onLogout }: { onLogout?: () => void })
     setInfoMessage("Paramètres sauvegardés ✅");
   };
 
-  const handleLogout = async () => {
-    await clearSession();
-    onLogout?.(); // App.tsx repasse sur Auth
+  const handleReplayTutorial = () => {
+    RNAlert.alert(
+      "Revoir le tutoriel ?",
+      "Le tutoriel sera relancé maintenant (comme au premier lancement).",
+      [
+        { text: "Annuler", style: "cancel" },
+        {
+          text: "Continuer",
+          onPress: async () => {
+            await setSeenTutorial(false);
+            onRequestTutorial?.();
+          },
+        },
+      ]
+    );
   };
 
   return (
@@ -83,16 +98,6 @@ export default function SettingsScreen({ onLogout }: { onLogout?: () => void })
       <View style={ui.container}>
         <Text style={styles.screenTitle}>Paramètres</Text>
 
-        {/* Carte session */}
-        <View style={ui.card}>
-          <Text style={ui.title}>Compte</Text>
-          <Text style={ui.muted}>Connecté : <Text style={styles.boldInline}>{sessionLabel}</Text></Text>
-
-          <TouchableOpacity style={styles.secondaryButton} onPress={handleLogout}>
-            <Text style={styles.secondaryButtonText}>Se déconnecter</Text>
-          </TouchableOpacity>
-        </View>
-
         {/* Carte : Devise */}
         <View style={ui.card}>
           <Text style={ui.title}>Devise</Text>
@@ -127,6 +132,18 @@ export default function SettingsScreen({ onLogout }: { onLogout?: () => void })
           {!!infoMessage && <Text style={[ui.muted, { marginTop: 10 }]}>{infoMessage}</Text>}
         </View>
 
+        {/* Carte : Tutoriel */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Tutoriel</Text>
+          <Text style={ui.muted}>
+            Le tutoriel explique la crypto, l’objectif de l’app et les réglages principaux.
+          </Text>
+
+          <TouchableOpacity style={[styles.secondaryButton]} onPress={handleReplayTutorial}>
+            <Text style={styles.secondaryButtonText}>Revoir le tutoriel</Text>
+          </TouchableOpacity>
+        </View>
+
         {/* Bouton Save */}
         <TouchableOpacity style={[ui.button, styles.fullButton, styles.saveButton]} onPress={handleSave}>
           <Text style={ui.buttonText}>Sauvegarder</Text>
@@ -147,11 +164,6 @@ const styles = StyleSheet.create({
     marginBottom: 12,
     color: "#0f172a",
   },
-  boldInline: {
-    fontWeight: "900",
-    color: "#0f172a",
-  },
-
   fullButton: {
     flexGrow: 0,
     flexBasis: "auto",
@@ -162,11 +174,10 @@ const styles = StyleSheet.create({
     marginTop: 4,
     marginBottom: 10,
   },
-
   secondaryButton: {
     marginTop: 10,
-    paddingVertical: 10,
-    borderRadius: 10,
+    paddingVertical: 12,
+    borderRadius: 12,
     borderWidth: 1,
     borderColor: "#e5e7eb",
     alignItems: "center",
@@ -174,6 +185,7 @@ const styles = StyleSheet.create({
   },
   secondaryButtonText: {
     fontWeight: "900",
-    color: "#dc2626",
+    color: "#0f172a",
+    opacity: 0.85,
   },
 });
\ No newline at end of file
diff --git a/Wallette/mobile/src/screens/TutorialScreen.tsx b/Wallette/mobile/src/screens/TutorialScreen.tsx
new file mode 100644 (file)
index 0000000..3f5f36c
--- /dev/null
@@ -0,0 +1,230 @@
+import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useMemo, useState } from "react";
+import { Ionicons } from "@expo/vector-icons";
+
+import { ui } from "../components/ui/uiStyles";
+import { setSeenTutorial } from "../utils/tutorialStorage";
+
+type Slide = {
+  key: string;
+  title: string;
+  text: string;
+  icon: any;
+};
+
+export default function TutorialScreen({ onDone }: { onDone: () => void }) {
+  const slides: Slide[] = useMemo(
+    () => [
+      {
+        key: "intro",
+        title: "La crypto, c’est quoi ?",
+        text:
+          "Une cryptomonnaie est une monnaie numérique. Son prix varie selon l’offre et la demande, et peut bouger très vite.",
+        icon: "sparkles-outline",
+      },
+      {
+        key: "types",
+        title: "Types de cryptos",
+        text:
+          "Exemples :\n• BTC : la plus connue\n• ETH : écosystème de contrats\n• Stablecoins (USDT/USDC) : valeur ≈ 1 USD\n\nLes stablecoins servent souvent d’intermédiaire pour convertir.",
+        icon: "layers-outline",
+      },
+      {
+        key: "role",
+        title: "Rôle de Wall-e-tte",
+        text:
+          "Wall-e-tte est un assistant : il aide à suivre le marché, les signaux et les alertes.\n\nCe n’est PAS un conseiller financier.",
+        icon: "shield-checkmark-outline",
+      },
+      {
+        key: "app",
+        title: "Comment utiliser l’app",
+        text:
+          "• Dashboard : résumé (prix, stratégie, urgence)\n• Portefeuille : ajouter des cryptos + quantités\n• Alertes : tri + filtres (CRITICAL en priorité)\n• Historique : signaux récents",
+        icon: "apps-outline",
+      },
+      {
+        key: "settings",
+        title: "Paramètres importants",
+        text:
+          "• Stratégie : choisir la méthode d’analyse\n• Notifications : recevoir les alertes\n• Devise : EUR/USD\n\nLe tutoriel reste accessible dans Paramètres.",
+        icon: "settings-outline",
+      },
+    ],
+    []
+  );
+
+  const [index, setIndex] = useState(0);
+  const isFirst = index === 0;
+  const isLast = index === slides.length - 1;
+  const slide = slides[index];
+
+  const finish = async () => {
+    await setSeenTutorial(true);
+    onDone();
+  };
+
+  const next = () => {
+    if (!isLast) setIndex((i) => i + 1);
+  };
+
+  const prev = () => {
+    if (!isFirst) setIndex((i) => i - 1);
+  };
+
+  const skip = () => {
+    void finish();
+  };
+
+  return (
+    <SafeAreaView style={styles.safeArea} edges={["top", "bottom"]}>
+      <View style={styles.page}>
+        {/* TOP BAR */}
+        <View style={[styles.topBar, ui.container]}>
+          <Text style={styles.small}>
+            {index + 1}/{slides.length}
+          </Text>
+
+          <TouchableOpacity onPress={skip}>
+            <Text style={styles.skip}>Passer</Text>
+          </TouchableOpacity>
+        </View>
+
+        {/* CONTENT (on laisse de la place en bas pour la barre fixe) */}
+        <View style={[styles.content, ui.container]}>
+          <View style={styles.iconWrap}>
+            <Ionicons name={slide.icon} size={38} color="#0f172a" style={{ opacity: 0.85 }} />
+          </View>
+
+          <Text style={styles.title}>{slide.title}</Text>
+          <Text style={styles.text}>{slide.text}</Text>
+        </View>
+
+        {/* BOTTOM BAR FIXE */}
+        <View style={styles.bottomFixed}>
+          <View style={styles.dotsRow}>
+            {slides.map((_, i) => (
+              <View key={i} style={[styles.dot, i === index && styles.dotActive]} />
+            ))}
+          </View>
+
+          <View style={styles.bottomRow}>
+            <TouchableOpacity
+              style={[styles.secondaryBtn, isFirst && styles.disabledBtn]}
+              onPress={prev}
+              disabled={isFirst}
+            >
+              <Text style={[styles.secondaryText, isFirst && styles.disabledText]}>
+                Précédent
+              </Text>
+            </TouchableOpacity>
+
+            <TouchableOpacity
+              style={[styles.primaryBtn]}
+              onPress={isLast ? finish : next}
+            >
+              <Text style={styles.primaryText}>
+                {isLast ? "Terminer" : "Suivant"}
+              </Text>
+            </TouchableOpacity>
+          </View>
+        </View>
+      </View>
+    </SafeAreaView>
+  );
+}
+
+const styles = StyleSheet.create({
+  safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
+  page: { flex: 1 },
+
+  topBar: {
+    flexDirection: "row",
+    justifyContent: "space-between",
+    alignItems: "center",
+    paddingTop: 6,
+    paddingBottom: 8,
+  },
+
+  small: { fontWeight: "900", color: "#0f172a", opacity: 0.6 },
+  skip: { fontWeight: "900", color: "#0f172a", opacity: 0.75 },
+
+  // On réserve de la place pour la barre fixe
+  content: {
+    flex: 1,
+    alignItems: "center",
+    justifyContent: "center",
+    paddingBottom: 140, // ✅ réserve l’espace pour les boutons fixes
+  },
+
+  iconWrap: {
+    width: 76,
+    height: 76,
+    borderRadius: 20,
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    backgroundColor: "#fff",
+    alignItems: "center",
+    justifyContent: "center",
+    marginBottom: 14,
+  },
+
+  title: { fontSize: 22, fontWeight: "900", color: "#0f172a", textAlign: "center" },
+  text: {
+    marginTop: 10,
+    fontSize: 14,
+    color: "#0f172a",
+    opacity: 0.75,
+    textAlign: "center",
+    lineHeight: 20,
+  },
+
+  // ✅ barre du bas fixée
+  bottomFixed: {
+    position: "absolute",
+    left: 0,
+    right: 0,
+    bottom: 0,
+    paddingHorizontal: 16,
+    paddingTop: 10,
+    paddingBottom: 16,
+    backgroundColor: ui.screen.backgroundColor,
+    borderTopWidth: 1,
+    borderTopColor: "#e5e7eb",
+  },
+
+  dotsRow: {
+    flexDirection: "row",
+    justifyContent: "center",
+    gap: 8,
+    marginBottom: 10,
+  },
+  dot: { width: 8, height: 8, borderRadius: 8, backgroundColor: "#0f172a", opacity: 0.15 },
+  dotActive: { opacity: 0.55 },
+
+  bottomRow: { flexDirection: "row", gap: 10, alignItems: "center" },
+
+  secondaryBtn: {
+    paddingVertical: 12,
+    paddingHorizontal: 14,
+    borderRadius: 12,
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    backgroundColor: "#fff",
+  },
+  secondaryText: { fontWeight: "900", color: "#0f172a", opacity: 0.8 },
+
+  primaryBtn: {
+    flex: 1,
+    borderRadius: 12,
+    paddingVertical: 12,
+    alignItems: "center",
+    justifyContent: "center",
+    backgroundColor: "#16a34a", // ✅ un vert "crypto friendly"
+  },
+  primaryText: { color: "#fff", fontWeight: "900" },
+
+  disabledBtn: { opacity: 0.45 },
+  disabledText: { opacity: 0.6 },
+});
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/alertsApi.ts b/Wallette/mobile/src/services/api/alertsApi.ts
new file mode 100644 (file)
index 0000000..3c9e7d2
--- /dev/null
@@ -0,0 +1,21 @@
+import type { Alert } from "../../types/Alert";
+import { alertStore } from "../alertStore";
+
+/**
+ * alertsApi
+ * ---------
+ * Contrat futur (gateway):
+ * - GET /api/alerts/events?userId=...&limit=10
+ * - POST /api/alerts
+ * - POST /api/alerts/:id/toggle
+ *
+ * Pour l'instant : on lit ce qu'on a en local (alertStore alimenté par Socket).
+ */
+export async function getRecentAlerts(limit = 10): Promise<Alert[]> {
+  const all = alertStore.getAll?.() ?? [];
+  return all.slice(0, limit);
+}
+
+export async function clearLocalAlerts(): Promise<void> {
+  alertStore.clear?.();
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/authApi.ts b/Wallette/mobile/src/services/api/authApi.ts
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Wallette/mobile/src/services/api/priceApi.ts b/Wallette/mobile/src/services/api/priceApi.ts
new file mode 100644 (file)
index 0000000..224660d
--- /dev/null
@@ -0,0 +1,15 @@
+import type { DashboardSummary } from "../../types/DashboardSummary";
+import { fetchDashboardSummary } from "../dashboardService";
+
+/**
+ * priceApi
+ * --------
+ * Contrat futur (gateway):
+ * - GET /api/price/current?pair=BTC/EUR
+ *
+ * Pour l'instant : on renvoie la donnée mock déjà utilisée par le Dashboard.
+ */
+export async function getCurrentPriceForDashboard(): Promise<Pick<DashboardSummary, "pair" | "price" | "timestamp">> {
+  const d = await fetchDashboardSummary();
+  return { pair: d.pair, price: d.price, timestamp: d.timestamp };
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/signalApi.ts b/Wallette/mobile/src/services/api/signalApi.ts
new file mode 100644 (file)
index 0000000..8a2d075
--- /dev/null
@@ -0,0 +1,15 @@
+import type { Signal } from "../../types/Signal";
+import { fetchRecentSignals } from "../signalService";
+
+/**
+ * signalApi
+ * ---------
+ * Contrat futur (gateway):
+ * - GET /api/signal/current?userId=...&pair=BTC/EUR
+ * - (option) GET /api/signal/recent?userId=...&limit=20
+ *
+ * Pour l'instant : on utilise les mocks existants.
+ */
+export async function getRecentSignals(limit = 20): Promise<Signal[]> {
+  return await fetchRecentSignals(limit);
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/services/api/strategyApi.ts b/Wallette/mobile/src/services/api/strategyApi.ts
new file mode 100644 (file)
index 0000000..5f5b366
--- /dev/null
@@ -0,0 +1,17 @@
+import { loadSettings, saveSettings } from "../../utils/settingsStorage";
+import type { UserSettings } from "../../models/UserSettings";
+
+/**
+ * strategyApi
+ * -----------
+ * Contrat futur (gateway):
+ * - POST /api/strategy/select   { userId, pair, strategyKey, params... }
+ *
+ * Pour l'instant : on sauvegarde localement dans settingsStorage.
+ */
+export async function selectStrategy(strategyKey: string): Promise<UserSettings> {
+  const s = await loadSettings();
+  const next: UserSettings = { ...s, selectedStrategyKey: strategyKey };
+  await saveSettings(next);
+  return next;
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/utils/accountStorage.ts b/Wallette/mobile/src/utils/accountStorage.ts
deleted file mode 100644 (file)
index cf15063..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-import AsyncStorage from "@react-native-async-storage/async-storage";
-import type { AccountProfile } from "../models/AccountProfile";
-import { loadSession } from "./sessionStorage";
-
-function keyFor(userId: string) {
-  return `accountProfile:${userId}`;
-}
-
-const DEFAULT_PROFILE: AccountProfile = {
-  displayName: "Utilisateur",
-  email: "demo@example.com",
-  updatedAtMs: Date.now(),
-};
-
-export async function loadAccountProfile(): Promise<AccountProfile> {
-  const session = await loadSession();
-  if (!session) return DEFAULT_PROFILE;
-
-  const raw = await AsyncStorage.getItem(keyFor(session.userId));
-  if (!raw) {
-    // Par défaut, on reprend l’email de session
-    return { ...DEFAULT_PROFILE, email: session.email };
-  }
-
-  try {
-    const parsed = JSON.parse(raw) as Partial<AccountProfile>;
-    return {
-      ...DEFAULT_PROFILE,
-      ...parsed,
-      email: String(parsed.email ?? session.email),
-      displayName: String(parsed.displayName ?? DEFAULT_PROFILE.displayName),
-      updatedAtMs: Number(parsed.updatedAtMs ?? Date.now()),
-    };
-  } catch {
-    return { ...DEFAULT_PROFILE, email: session.email };
-  }
-}
-
-export async function saveAccountProfile(profile: AccountProfile): Promise<void> {
-  const session = await loadSession();
-  if (!session) return;
-
-  const safe: AccountProfile = {
-    displayName: profile.displayName.trim() || "Utilisateur",
-    email: profile.email.trim().toLowerCase() || session.email,
-    updatedAtMs: Date.now(),
-  };
-
-  await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(safe));
-}
\ No newline at end of file
diff --git a/Wallette/mobile/src/utils/authUsersStorage.ts b/Wallette/mobile/src/utils/authUsersStorage.ts
new file mode 100644 (file)
index 0000000..0ab6c42
--- /dev/null
@@ -0,0 +1,143 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import * as Crypto from "expo-crypto";
+import type { AuthUser } from "../models/AuthUser";
+import { loadSession } from "./sessionStorage";
+
+const KEY = "authUsers";
+const LOCAL_SALT = "wall-e-tte_local_salt_v1";
+
+async function sha256(input: string): Promise<string> {
+  return await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, input);
+}
+
+export async function hashPassword(password: string): Promise<string> {
+  const normalized = password.trim();
+  return await sha256(`${LOCAL_SALT}:${normalized}`);
+}
+
+export async function loadUsers(): Promise<AuthUser[]> {
+  const raw = await AsyncStorage.getItem(KEY);
+  if (!raw) return [];
+
+  try {
+    const parsed = JSON.parse(raw) as AuthUser[];
+    return Array.isArray(parsed) ? parsed : [];
+  } catch {
+    return [];
+  }
+}
+
+async function saveUsers(users: AuthUser[]): Promise<void> {
+  await AsyncStorage.setItem(KEY, JSON.stringify(users));
+}
+
+export async function findUserByLogin(login: string): Promise<AuthUser | null> {
+  const users = await loadUsers();
+  const key = login.trim().toLowerCase();
+
+  const found = users.find(
+    (u) => u.email.toLowerCase() === key || u.username.toLowerCase() === key
+  );
+
+  return found ?? null;
+}
+
+export async function findUserById(userId: string): Promise<AuthUser | null> {
+  const users = await loadUsers();
+  return users.find((u) => u.userId === userId) ?? null;
+}
+
+export async function createUser(params: {
+  email: string;
+  username: string;
+  displayName?: string;
+  password: string;
+}): Promise<{ ok: true; user: AuthUser } | { ok: false; message: string }> {
+  const email = params.email.trim().toLowerCase();
+  const username = params.username.trim().toLowerCase();
+  const displayName = (params.displayName ?? "").trim();
+
+  const users = await loadUsers();
+
+  if (users.some((u) => u.email.toLowerCase() === email)) {
+    return { ok: false, message: "Cet email est déjà utilisé." };
+  }
+  if (users.some((u) => u.username.toLowerCase() === username)) {
+    return { ok: false, message: "Ce nom d’utilisateur est déjà utilisé." };
+  }
+
+  const passwordHash = await hashPassword(params.password);
+
+  const user: AuthUser = {
+    userId: `user_${username}`,
+    email,
+    username,
+    displayName: displayName || undefined,
+    passwordHash,
+    createdAtMs: Date.now(),
+  };
+
+  await saveUsers([...users, user]);
+  return { ok: true, user };
+}
+
+export async function verifyLogin(params: {
+  login: string;
+  password: string;
+}): Promise<{ ok: true; user: AuthUser } | { ok: false; message: string }> {
+  const user = await findUserByLogin(params.login);
+
+  if (!user) return { ok: false, message: "Compte introuvable." };
+
+  const hash = await hashPassword(params.password);
+
+  if (hash !== user.passwordHash) {
+    return { ok: false, message: "Mot de passe incorrect." };
+  }
+
+  return { ok: true, user };
+}
+
+/**
+ * Update profil (local, Step 4 sans API)
+ * - email NON modifiable ici (on le garde tel quel)
+ * - username NON modifiable ici (sinon ça casse userId local)
+ * - displayName modifiable
+ * - password modifiable (hashé)
+ *
+ * Plus tard : remplacer par un appel REST vers le serveur.
+ */
+export async function updateCurrentUserProfile(params: {
+  displayName?: string;
+  newPassword?: string;
+}): Promise<{ ok: true; user: AuthUser } | { ok: false; message: string }> {
+  const session = await loadSession();
+  if (!session) return { ok: false, message: "Session absente." };
+
+  const users = await loadUsers();
+  const idx = users.findIndex((u) => u.userId === session.userId);
+  if (idx < 0) return { ok: false, message: "Utilisateur introuvable." };
+
+  const current = users[idx];
+
+  let next: AuthUser = { ...current };
+
+  // displayName (optionnel)
+  if (typeof params.displayName === "string") {
+    const dn = params.displayName.trim();
+    next.displayName = dn ? dn : undefined;
+  }
+
+  // mot de passe (optionnel)
+  if (params.newPassword) {
+    if (params.newPassword.trim().length < 6) {
+      return { ok: false, message: "Mot de passe trop court (min 6)." };
+    }
+    next.passwordHash = await hashPassword(params.newPassword);
+  }
+
+  users[idx] = next;
+  await saveUsers(users);
+
+  return { ok: true, user: next };
+}
\ No newline at end of file
diff --git a/Wallette/mobile/src/utils/tutorialStorage.ts b/Wallette/mobile/src/utils/tutorialStorage.ts
new file mode 100644 (file)
index 0000000..a828708
--- /dev/null
@@ -0,0 +1,26 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { loadSession } from "./sessionStorage";
+
+/**
+ * Tutoriel par utilisateur (Step 4)
+ * - clé = hasSeenTutorial:<userId>
+ * - permet d'afficher le tuto au premier lancement ET par compte
+ */
+function keyFor(userId: string) {
+  return `hasSeenTutorial:${userId}`;
+}
+
+export async function hasSeenTutorial(): Promise<boolean> {
+  const session = await loadSession();
+  if (!session) return true; // sécurité : si pas de session, on ne bloque pas l'app
+
+  const raw = await AsyncStorage.getItem(keyFor(session.userId));
+  return raw === "1";
+}
+
+export async function setSeenTutorial(value: boolean): Promise<void> {
+  const session = await loadSession();
+  if (!session) return;
+
+  await AsyncStorage.setItem(keyFor(session.userId), value ? "1" : "0");
+}
\ No newline at end of file