]> git.digitality.be Git - pdw25-26/commitdiff
"Mobile : Ajout d'un tutoriel user friendly"
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Fri, 27 Feb 2026 19:01:32 +0000 (20:01 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Fri, 27 Feb 2026 19:01:32 +0000 (20:01 +0100)
Wallette/mobile/App.tsx
Wallette/mobile/src/screens/AboutScreen.tsx
Wallette/mobile/src/screens/SettingsScreen.tsx
Wallette/mobile/src/screens/TutorialScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/utils/tutorialStorage.ts [new file with mode: 0644]

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 ab72035298716abd25a4a15cc61e264a2f659872..f6ddadcf05b49dcfe9241e0d44f2006b9693863a 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";
-
-/**
- * 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.
- */
-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>("");
+import { loadAccountProfile } from "../utils/accountStorage";
 
-  // 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");
 
   useEffect(() => {
     async function init() {
-      const [session, p] = await Promise.all([loadSession(), loadAccountProfile()]);
-      setUserId(session?.userId ?? "");
-      setProfile(p);
-      setDisplayName(p.displayName);
-      setEmail(p.email);
+      const [session, profile] = await Promise.all([loadSession(), loadAccountProfile()]);
+      setEmail(profile?.email ?? session?.email ?? "—");
+      setDisplayName(profile?.displayName ?? "Utilisateur");
     }
     init();
   }, []);
 
-  if (!profile) {
-    return (
-      <View style={ui.centered}>
-        <Text>Chargement du compte…</Text>
+  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>
-    );
-  }
-
-  const handleSave = async () => {
-    setInfo(null);
-
-    const normalizedEmail = email.trim().toLowerCase();
-    const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail);
-
-    if (!isValidEmail) {
-      setInfo("Email invalide.");
-      return;
-    }
-
-    // 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" }]
-      );
-    }
-
-    const updated: AccountProfile = {
-      displayName: displayName.trim() || "Utilisateur",
-      email: normalizedEmail,
-      updatedAtMs: Date.now(),
-    };
-
-    await saveAccountProfile(updated);
-    setProfile(updated);
-    setPassword("");
-    setPassword2("");
-    setInfo("Profil sauvegardé ✅");
-  };
+      <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>
-
-          <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}
-          />
+          <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}>{email}</Text>
+            </View>
+          </View>
         </View>
 
+        {/* À propos */}
         <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.title}>À propos</Text>
 
-          <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
-          <TextInput value={password} onChangeText={setPassword} secureTextEntry 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>
 
-          <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
-          <TextInput value={password2} onChangeText={setPassword2} secureTextEntry style={styles.input} />
+        {/* Support */}
+        <View style={ui.card}>
+          <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 */}
+        <TouchableOpacity style={[styles.secondaryButton]}>
+          <Text style={styles.secondaryButtonText}>Voir les licences (optionnel)</Text>
         </TouchableOpacity>
       </View>
     </SafeAreaView>
@@ -142,23 +81,28 @@ 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,
-    fontFamily: "monospace",
-    color: "#0f172a",
-    opacity: 0.8,
+  profileRow: { flexDirection: "row", alignItems: "center", gap: 12 },
+  profileName: { fontSize: 18, fontWeight: "900", color: "#0f172a" },
+
+  row: {
+    flexDirection: "row",
+    alignItems: "center",
+    gap: 10,
+    paddingVertical: 12,
+    borderTopWidth: 1,
+    borderTopColor: "#e5e7eb",
   },
+  rowTitle: { fontWeight: "900", color: "#0f172a" },
 
-  input: {
+  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 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..5ffe6f7
--- /dev/null
@@ -0,0 +1,184 @@
+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}>
+      <View style={[ui.container, { flex: 1 }]}>
+        {/* Top bar */}
+        <View style={styles.topRow}>
+          <Text style={styles.small}>
+            {index + 1}/{slides.length}
+          </Text>
+
+          <TouchableOpacity onPress={skip}>
+            <Text style={styles.skip}>Passer</Text>
+          </TouchableOpacity>
+        </View>
+
+        {/* Slide content (sans swipe) */}
+        <View style={styles.slide}>
+          <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>
+
+        {/* Dots */}
+        <View style={styles.dotsRow}>
+          {slides.map((_, i) => (
+            <View key={i} style={[styles.dot, i === index && styles.dotActive]} />
+          ))}
+        </View>
+
+        {/* Bottom buttons */}
+        <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={[ui.button, styles.primaryBtn]}
+            onPress={isLast ? finish : next}
+          >
+            <Text style={ui.buttonText}>{isLast ? "Terminer" : "Suivant"}</Text>
+          </TouchableOpacity>
+        </View>
+      </View>
+    </SafeAreaView>
+  );
+}
+
+const styles = StyleSheet.create({
+  safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
+
+  topRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
+  small: { fontWeight: "900", color: "#0f172a", opacity: 0.6 },
+  skip: { fontWeight: "900", color: "#0f172a", opacity: 0.75 },
+
+  slide: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 22 },
+  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,
+  },
+
+  dotsRow: { flexDirection: "row", justifyContent: "center", gap: 8, marginBottom: 12, marginTop: 6 },
+  dot: { width: 8, height: 8, borderRadius: 8, backgroundColor: "#0f172a", opacity: 0.15 },
+  dotActive: { opacity: 0.55 },
+
+  bottomRow: { flexDirection: "row", gap: 10, alignItems: "center", paddingBottom: 8 },
+
+  secondaryBtn: {
+    paddingVertical: 12,
+    paddingHorizontal: 14,
+    borderRadius: 12,
+    borderWidth: 1,
+    borderColor: "#e5e7eb",
+    backgroundColor: "#fff",
+  },
+  secondaryText: { fontWeight: "900", color: "#0f172a", opacity: 0.8 },
+
+  primaryBtn: { flex: 1, flexGrow: 0, flexBasis: "auto" },
+
+  disabledBtn: { opacity: 0.45 },
+  disabledText: { opacity: 0.6 },
+});
\ 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