-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";
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;
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;
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);
}
await clearSession();
setIsAuthed(false);
setSessionEmail("—");
+ setNeedsTutorial(false);
},
},
]);
};
const go = (route: keyof RootStackParamList) => {
- // ✅ safe guard : navigation prête ?
if (!navigationRef.isReady()) return;
navigationRef.navigate(route);
};
);
}
- // Pas connecté -> écran Auth
+ // 1) Pas connecté -> Auth (hors stack)
if (!isAuthed) {
return (
<AuthScreen
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}
<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>
);
-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>
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
-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";
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();
}, []);
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 () => {
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 (
<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>
{!!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>
marginBottom: 12,
color: "#0f172a",
},
- boldInline: {
- fontWeight: "900",
- color: "#0f172a",
- },
-
fullButton: {
flexGrow: 0,
flexBasis: "auto",
marginTop: 4,
marginBottom: 10,
},
-
secondaryButton: {
marginTop: 10,
- paddingVertical: 10,
- borderRadius: 10,
+ paddingVertical: 12,
+ borderRadius: 12,
borderWidth: 1,
borderColor: "#e5e7eb",
alignItems: "center",
},
secondaryButtonText: {
fontWeight: "900",
- color: "#dc2626",
+ color: "#0f172a",
+ opacity: 0.85,
},
});
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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