From: Thibaud Moustier Date: Fri, 27 Feb 2026 21:43:26 +0000 (+0100) Subject: Mobile : Update mobile from thibaud-mobile X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=fd5b8e22d29370ee6fa953195a396d03090b60e0;p=pdw25-26 Mobile : Update mobile from thibaud-mobile --- diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md deleted file mode 100644 index ce8c4b6..0000000 --- a/Wallette/mobile/.expo/README.md +++ /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 index 5efff6c..0000000 --- a/Wallette/mobile/.expo/devices.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "devices": [] -} diff --git a/Wallette/mobile/App.tsx b/Wallette/mobile/App.tsx index d33c4c3..1eb6981 100644 --- a/Wallette/mobile/App.tsx +++ b/Wallette/mobile/App.tsx @@ -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(); -// ✅ navigationRef (permet de naviguer hors des screens, ex: depuis un Modal) +// ✅ navigationRef (navigate depuis le Modal AccountMenu) export const navigationRef = createNavigationContainerRef(); export default function App() { const [ready, setReady] = useState(false); + + // Auth state const [isAuthed, setIsAuthed] = useState(false); + const [sessionEmail, setSessionEmail] = useState("—"); + + // Tutorial gate + const [needsTutorial, setNeedsTutorial] = useState(false); + // Account menu const [menuVisible, setMenuVisible] = useState(false); - const [sessionEmail, setSessionEmail] = useState("—"); 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 ( + ); + } + + // 2) Connecté mais tuto pas vu -> Tutoriel (hors stack) + if (needsTutorial) { + return ( + { + setNeedsTutorial(false); }} /> ); } + // 3) Connecté + tuto vu -> App normale return ( - {/* ✅ Modal menu Compte (navigue grâce au navigationRef) */} {/* 👤 Menu Compte */} setMenuVisible(true)}> - + {/* ⚙️ Paramètres */} - navigation.navigate("Settings")}> - + navigation.navigate("Settings")} + > + ), })} /> - - - - + + + + - + + {() => ( + { + // Force le tuto à s'afficher hors stack + setNeedsTutorial(true); + }} + /> + )} + - - + + ); diff --git a/Wallette/mobile/package-lock.json b/Wallette/mobile/package-lock.json index fd361d2..6377cc9 100644 --- a/Wallette/mobile/package-lock.json +++ b/Wallette/mobile/package-lock.json @@ -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", @@ -4745,6 +4746,18 @@ "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", diff --git a/Wallette/mobile/package.json b/Wallette/mobile/package.json index 0db1739..813958d 100644 --- a/Wallette/mobile/package.json +++ b/Wallette/mobile/package.json @@ -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 index 0000000..19f8d94 --- /dev/null +++ b/Wallette/mobile/src/config/api.ts @@ -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://:3000/api/... + * Socket.IO passe aussi via le gateway: + * http://: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 diff --git a/Wallette/mobile/src/config/env.ts b/Wallette/mobile/src/config/env.ts index 36a9d9a..e394a9b 100644 --- a/Wallette/mobile/src/config/env.ts +++ b/Wallette/mobile/src/config/env.ts @@ -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://:3000 + * - REST = http://:3000/api/... + * - Socket.IO = http://:3000 (proxy /socket.io/* via gateway) + * + * Téléphone physique : + * - = 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 index 93ebefb..0000000 --- a/Wallette/mobile/src/models/AccountProfile.ts +++ /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 index 0000000..1e13ac9 --- /dev/null +++ b/Wallette/mobile/src/models/AuthUser.ts @@ -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 diff --git a/Wallette/mobile/src/screens/AboutScreen.tsx b/Wallette/mobile/src/screens/AboutScreen.tsx index ab72035..02e683e 100644 --- a/Wallette/mobile/src/screens/AboutScreen.tsx +++ b/Wallette/mobile/src/screens/AboutScreen.tsx @@ -1,138 +1,137 @@ -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(null); - const [userId, setUserId] = useState(""); - - const [displayName, setDisplayName] = useState(""); - const [email, setEmail] = useState(""); - - // Champs UI (non persistés) pour mot de passe - const [password, setPassword] = useState(""); - const [password2, setPassword2] = useState(""); - - const [info, setInfo] = useState(null); +export default function AboutScreen() { + const [email, setEmail] = useState("—"); + const [displayName, setDisplayName] = useState("Utilisateur"); + const [username, setUsername] = useState("—"); + const [userId, setUserId] = useState("—"); 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 ( - - Chargement du compte… - - ); - } + 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; + }) => ( + + + + {title} + {!!subtitle && {subtitle}} + + + + ); return ( - Modification du compte + Détails du compte + {/* Carte profil (style WF-09) */} - Informations - - UserId (session) - {userId || "—"} + + + + {displayName} + {email} + + @{username} + + + + + + UserId + {userId} + + - Nom affiché - + {/* À propos */} + + À propos - Email - + + {/* Support */} - Mot de passe (UI prête) - - Le changement réel du mot de passe sera branché quand l’API auth sera disponible. - - - Nouveau mot de passe - - - Confirmation - + Support + + - {!!info && {info}} + {/* Version */} + + Version + App : 1.0.0 + Build : Step 4 (sans API) + - - Sauvegarder + {/* Bouton discret (optionnel) */} + + Voir les licences (optionnel) @@ -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 diff --git a/Wallette/mobile/src/screens/AccountScreen.tsx b/Wallette/mobile/src/screens/AccountScreen.tsx index ab72035..d7be829 100644 --- a/Wallette/mobile/src/screens/AccountScreen.tsx +++ b/Wallette/mobile/src/screens/AccountScreen.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [userId, setUserId] = useState(""); + const [email, setEmail] = useState(""); // affichage only + const [username, setUsername] = useState(""); // affichage only const [displayName, setDisplayName] = useState(""); - const [email, setEmail] = useState(""); - // Champs UI (non persistés) pour mot de passe const [password, setPassword] = useState(""); const [password2, setPassword2] = useState(""); const [info, setInfo] = useState(null); + const [error, setError] = useState(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 ( Chargement du compte… @@ -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() { Informations - UserId (session) + UserId {userId || "—"} + Email (non modifiable) + + + Nom d’utilisateur (non modifiable) + + Nom affiché - - - Email - + + + + ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation). + - Mot de passe (UI prête) + Mot de passe - 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.) Nouveau mot de passe - + Confirmation - + + {!!error && {error}} {!!info && {info}} @@ -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 diff --git a/Wallette/mobile/src/screens/AuthScreen.tsx b/Wallette/mobile/src/screens/AuthScreen.tsx index 8e15dac..8ac9986 100644 --- a/Wallette/mobile/src/screens/AuthScreen.tsx +++ b/Wallette/mobile/src/screens/AuthScreen.tsx @@ -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("demo@example.com"); + const [mode, setMode] = useState<"login" | "register">("login"); const [error, setError] = useState(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 ( - Connexion + {title} + + {/* Tabs */} + + { + setMode("login"); + setError(null); + }} + > + + Connexion + + + + { + setMode("register"); + setError(null); + }} + > + + Créer un compte + + + - Email + {mode === "login" ? ( + <> + Email ou nom d’utilisateur + - + Mot de passe + - {!!error && {error}} + {!!error && {error}} - - Se connecter - + + Se connecter + + + + (Mode local sans API : destiné au développement / démo.) + + + ) : ( + <> + Email + + + Nom d’utilisateur + + + Nom / Prénom (optionnel) + + + Mot de passe + + + Confirmer + - - Step 4 (sans API) : session locale. Plus tard, auth réelle via serveur. - + {!!error && {error}} + + + Créer le compte + + + + (Sans API : stockage local + hash SHA-256 pour éviter le mot de passe en clair.) + + + )} @@ -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 diff --git a/Wallette/mobile/src/screens/SettingsScreen.tsx b/Wallette/mobile/src/screens/SettingsScreen.tsx index 4f753fb..aa1540a 100644 --- a/Wallette/mobile/src/screens/SettingsScreen.tsx +++ b/Wallette/mobile/src/screens/SettingsScreen.tsx @@ -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(null); const [infoMessage, setInfoMessage] = useState(null); - const [sessionLabel, setSessionLabel] = useState(""); 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 }) Paramètres - {/* Carte session */} - - Compte - Connecté : {sessionLabel} - - - Se déconnecter - - - {/* Carte : Devise */} Devise @@ -127,6 +132,18 @@ export default function SettingsScreen({ onLogout }: { onLogout?: () => void }) {!!infoMessage && {infoMessage}} + {/* Carte : Tutoriel */} + + Tutoriel + + Le tutoriel explique la crypto, l’objectif de l’app et les réglages principaux. + + + + Revoir le tutoriel + + + {/* Bouton Save */} Sauvegarder @@ -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 index 0000000..3f5f36c --- /dev/null +++ b/Wallette/mobile/src/screens/TutorialScreen.tsx @@ -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 ( + + + {/* TOP BAR */} + + + {index + 1}/{slides.length} + + + + Passer + + + + {/* CONTENT (on laisse de la place en bas pour la barre fixe) */} + + + + + + {slide.title} + {slide.text} + + + {/* BOTTOM BAR FIXE */} + + + {slides.map((_, i) => ( + + ))} + + + + + + Précédent + + + + + + {isLast ? "Terminer" : "Suivant"} + + + + + + + ); +} + +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 index 0000000..3c9e7d2 --- /dev/null +++ b/Wallette/mobile/src/services/api/alertsApi.ts @@ -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 { + const all = alertStore.getAll?.() ?? []; + return all.slice(0, limit); +} + +export async function clearLocalAlerts(): Promise { + 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 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 index 0000000..224660d --- /dev/null +++ b/Wallette/mobile/src/services/api/priceApi.ts @@ -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> { + 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 index 0000000..8a2d075 --- /dev/null +++ b/Wallette/mobile/src/services/api/signalApi.ts @@ -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 { + 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 index 0000000..5f5b366 --- /dev/null +++ b/Wallette/mobile/src/services/api/strategyApi.ts @@ -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 { + 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 index cf15063..0000000 --- a/Wallette/mobile/src/utils/accountStorage.ts +++ /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 { - 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; - 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 { - 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 index 0000000..0ab6c42 --- /dev/null +++ b/Wallette/mobile/src/utils/authUsersStorage.ts @@ -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 { + return await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, input); +} + +export async function hashPassword(password: string): Promise { + const normalized = password.trim(); + return await sha256(`${LOCAL_SALT}:${normalized}`); +} + +export async function loadUsers(): Promise { + 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 { + await AsyncStorage.setItem(KEY, JSON.stringify(users)); +} + +export async function findUserByLogin(login: string): Promise { + 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 { + 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 index 0000000..a828708 --- /dev/null +++ b/Wallette/mobile/src/utils/tutorialStorage.ts @@ -0,0 +1,26 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { loadSession } from "./sessionStorage"; + +/** + * Tutoriel par utilisateur (Step 4) + * - clé = hasSeenTutorial: + * - permet d'afficher le tuto au premier lancement ET par compte + */ +function keyFor(userId: string) { + return `hasSeenTutorial:${userId}`; +} + +export async function hasSeenTutorial(): Promise { + 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 { + const session = await loadSession(); + if (!session) return; + + await AsyncStorage.setItem(keyFor(session.userId), value ? "1" : "0"); +} \ No newline at end of file