createNavigationContainerRef,
} from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
-import { TouchableOpacity, View, Text, Alert as RNAlert } from "react-native";
+import { TouchableOpacity, View, Text } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
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 AccountMenu from "./src/components/AccountMenu";
-import { loadSession } from "./src/utils/sessionStorage";
+import { loadSession, saveSession, type Session } from "./src/utils/sessionStorage";
import { hasSeenTutorial } from "./src/utils/tutorialStorage";
-import { logout as authLogout } from "./src/services/api/authApi";
export type RootStackParamList = {
Dashboard: undefined;
Alerts: undefined;
Strategy: undefined;
Wallet: undefined;
- Account: undefined;
About: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
-
-// ✅ navigationRef (navigate depuis le Modal AccountMenu)
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
+const SERVER_USER_ID = "user-123";
+
export default function App() {
const [ready, setReady] = useState(false);
- // Auth state
- const [isAuthed, setIsAuthed] = useState(false);
- const [sessionEmail, setSessionEmail] = useState<string>("—");
+ // affichage simple dans le menu
+ const [sessionLabel, setSessionLabel] = useState<string>(SERVER_USER_ID);
- // Tutorial gate
+ // Tutoriel
const [needsTutorial, setNeedsTutorial] = useState(false);
- // Account menu
+ // Menu
const [menuVisible, setMenuVisible] = useState(false);
useEffect(() => {
let active = true;
async function init() {
+ // ✅ Session fixe (pas d’auth)
const s = await loadSession();
if (!active) return;
- const authed = !!s;
- setIsAuthed(authed);
- setSessionEmail(s?.email ?? "—");
-
- if (authed) {
- const seen = await hasSeenTutorial();
+ if (!s) {
+ const session: Session = {
+ userId: SERVER_USER_ID,
+ email: SERVER_USER_ID,
+ createdAtMs: Date.now(),
+ };
+ await saveSession(session);
if (!active) return;
- setNeedsTutorial(!seen);
+ setSessionLabel(session.userId);
} else {
- setNeedsTutorial(false);
+ setSessionLabel(s.userId ?? SERVER_USER_ID);
}
+ const seen = await hasSeenTutorial();
+ if (!active) return;
+ setNeedsTutorial(!seen);
+
setReady(true);
}
void init();
-
return () => {
active = false;
};
}, []);
- const doLogout = () => {
- RNAlert.alert("Déconnexion", "Se déconnecter du compte ?", [
- { text: "Annuler", style: "cancel" },
- {
- text: "Déconnexion",
- style: "destructive",
- onPress: async () => {
- await authLogout();
- setIsAuthed(false);
- setSessionEmail("—");
- setNeedsTutorial(false);
- setMenuVisible(false);
- },
- },
- ]);
- };
-
const go = (route: keyof RootStackParamList) => {
if (!navigationRef.isReady()) return;
navigationRef.navigate(route);
);
}
- // 1) Pas connecté -> Auth (hors stack)
- if (!isAuthed) {
- return (
- <AuthScreen
- onAuthenticated={async () => {
- 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)
+ // Tutoriel au premier lancement
if (needsTutorial) {
return (
<TutorialScreen
);
}
- // 3) Connecté + tuto vu -> App normale
return (
<NavigationContainer ref={navigationRef}>
<AccountMenu
visible={menuVisible}
- email={sessionEmail}
+ label={sessionLabel}
onClose={() => setMenuVisible(false)}
- onGoAccount={() => go("Account")}
onGoAbout={() => go("About")}
- onLogout={doLogout}
/>
<Stack.Navigator id="MainStack" initialRouteName="Dashboard">
title: "Dashboard",
headerRight: () => (
<View style={{ flexDirection: "row", gap: 12 }}>
- {/* 👤 Menu Compte */}
+ {/* Menu */}
<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"
- />
+ {/* Paramètres */}
+ <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" 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="About" component={AboutScreen} options={{ title: "À propos" }} />
</Stack.Navigator>
</NavigationContainer>
);
-import { Modal, View, Text, StyleSheet, TouchableOpacity, Pressable } from "react-native";
+import { Modal, View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons";
-import { ui } from "./ui/uiStyles";
type Props = {
visible: boolean;
- email: string;
+ label: string; // ex: user-123
onClose: () => void;
-
- onGoAccount: () => void;
onGoAbout: () => void;
- onLogout: () => void;
};
-export default function AccountMenu({
- visible,
- email,
- onClose,
- onGoAccount,
- onGoAbout,
- onLogout,
-}: Props) {
+export default function AccountMenu({ visible, label, onClose, onGoAbout }: Props) {
return (
- <Modal transparent visible={visible} animationType="fade" onRequestClose={onClose}>
- {/* Overlay */}
- <Pressable style={styles.overlay} onPress={onClose}>
- {/* Stop propagation */}
- <Pressable style={styles.card} onPress={() => null}>
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
+ <View style={styles.backdrop}>
+ <View style={styles.card}>
<View style={styles.headerRow}>
- <Ionicons name="person-circle-outline" size={28} color="#0f172a" style={{ opacity: 0.8 }} />
- <View style={{ flex: 1 }}>
- <Text style={styles.title}>Compte</Text>
- <Text style={ui.muted} numberOfLines={1}>{email}</Text>
+ <View style={styles.userRow}>
+ <Ionicons name="person-circle-outline" size={28} color="#0f172a" style={{ opacity: 0.8 }} />
+ <Text style={styles.label}>{label}</Text>
</View>
+
<TouchableOpacity onPress={onClose}>
<Ionicons name="close" size={22} color="#0f172a" style={{ opacity: 0.7 }} />
</TouchableOpacity>
</View>
- <TouchableOpacity style={styles.item} onPress={() => { onClose(); onGoAccount(); }}>
- <Ionicons name="create-outline" size={18} color="#0f172a" style={styles.itemIcon} />
- <Text style={styles.itemText}>Modification du compte</Text>
- <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
- </TouchableOpacity>
-
- <TouchableOpacity style={styles.item} onPress={() => { onClose(); onGoAbout(); }}>
- <Ionicons name="information-circle-outline" size={18} color="#0f172a" style={styles.itemIcon} />
+ <TouchableOpacity
+ style={styles.item}
+ onPress={() => {
+ onClose();
+ onGoAbout();
+ }}
+ >
+ <Ionicons name="information-circle-outline" size={18} color="#0f172a" style={{ opacity: 0.75 }} />
<Text style={styles.itemText}>À propos</Text>
- <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
</TouchableOpacity>
- <TouchableOpacity style={[styles.item, styles.itemDanger]} onPress={() => { onClose(); onLogout(); }}>
- <Ionicons name="log-out-outline" size={18} color="#dc2626" style={styles.itemIcon} />
- <Text style={[styles.itemText, { color: "#dc2626" }]}>Déconnexion</Text>
+ <TouchableOpacity style={[styles.item, styles.itemSecondary]} onPress={onClose}>
+ <Ionicons name="arrow-back-outline" size={18} color="#0f172a" style={{ opacity: 0.75 }} />
+ <Text style={styles.itemText}>Fermer</Text>
</TouchableOpacity>
- </Pressable>
- </Pressable>
+ </View>
+ </View>
</Modal>
);
}
const styles = StyleSheet.create({
- overlay: {
+ backdrop: {
flex: 1,
- backgroundColor: "rgba(15, 23, 42, 0.35)",
- justifyContent: "flex-start",
- paddingTop: 70,
- paddingHorizontal: 16,
+ backgroundColor: "rgba(0,0,0,0.35)",
+ justifyContent: "center",
+ padding: 16,
},
-
card: {
backgroundColor: "#fff",
- borderRadius: 14,
+ borderRadius: 16,
+ padding: 14,
borderWidth: 1,
borderColor: "#e5e7eb",
- padding: 12,
},
-
headerRow: {
flexDirection: "row",
+ justifyContent: "space-between",
alignItems: "center",
- gap: 10,
- paddingBottom: 10,
- borderBottomWidth: 1,
- borderBottomColor: "#e5e7eb",
marginBottom: 10,
},
-
- title: {
- fontSize: 16,
+ userRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ label: {
fontWeight: "900",
color: "#0f172a",
+ opacity: 0.9,
},
-
item: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 12,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ backgroundColor: "#fff",
+ marginTop: 10,
},
-
- itemDanger: {
- marginTop: 6,
- backgroundColor: "#dc262611",
- },
-
- itemIcon: {
- width: 22,
+ itemSecondary: {
opacity: 0.9,
},
-
itemText: {
- flex: 1,
fontWeight: "900",
color: "#0f172a",
+ opacity: 0.85,
},
});
\ No newline at end of file
export const GATEWAY_BASE_URL = PROD_GATEWAY;
// REST (via gateway)
-export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
+export const API_BASE_URL = "https://wallette.duckdns.org/api";
// Socket.IO (via gateway)
export const SERVER_URL = GATEWAY_BASE_URL;
+++ /dev/null
-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 { findUserById, updateCurrentUserProfile } from "../utils/authUsersStorage";
-
-/**
- * AccountScreen (Step 4 - sans API)
- * --------------------------------
- * - Email : affiché mais NON modifiable (important)
- * - Username : affiché mais NON modifiable (sinon userId local casse)
- * - DisplayName : modifiable
- * - Mot de passe : modifiable (hash local)
- *
- * Plus tard : remplacement par une route API (DB).
- */
-export default function AccountScreen() {
- const [loading, setLoading] = useState(true);
-
- const [userId, setUserId] = useState<string>("");
-
- const [email, setEmail] = useState<string>(""); // affichage only
- const [username, setUsername] = useState<string>(""); // affichage only
- const [displayName, setDisplayName] = useState<string>("");
-
- const [password, setPassword] = useState<string>("");
- const [password2, setPassword2] = useState<string>("");
-
- const [info, setInfo] = useState<string | null>(null);
- const [error, setError] = useState<string | null>(null);
-
- useEffect(() => {
- let active = true;
-
- async function init() {
- const session = 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 (loading) {
- return (
- <View style={ui.centered}>
- <Text>Chargement du compte…</Text>
- </View>
- );
- }
-
- const handleSave = async () => {
- setInfo(null);
- setError(null);
-
- // Mot de passe : optionnel, mais si rempli => validations
- const wantsPasswordChange = password.trim().length > 0 || password2.trim().length > 0;
-
- if (wantsPasswordChange) {
- if (password.trim().length < 6) {
- setError("Mot de passe trop court (min 6).");
- return;
- }
- if (password !== password2) {
- setError("Les mots de passe ne correspondent pas.");
- return;
- }
- }
-
- const res = await updateCurrentUserProfile({
- displayName,
- newPassword: wantsPasswordChange ? password : undefined,
- });
-
- if (!res.ok) {
- setError(res.message);
- return;
- }
-
- // reset password fields
- setPassword("");
- setPassword2("");
-
- setInfo("Profil sauvegardé ✅ (local, en attendant l’API).");
- };
-
- return (
- <SafeAreaView style={styles.safeArea}>
- <View style={ui.container}>
- <Text style={styles.title}>Modification du compte</Text>
-
- <View style={ui.card}>
- <Text style={ui.title}>Informations</Text>
-
- <Text style={ui.muted}>UserId</Text>
- <Text style={styles.mono}>{userId || "—"}</Text>
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Email (non modifiable)</Text>
- <TextInput value={email} editable={false} style={[styles.input, styles.inputDisabled]} />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur (non modifiable)</Text>
- <TextInput value={username} editable={false} style={[styles.input, styles.inputDisabled]} />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Nom affiché</Text>
- <TextInput value={displayName} onChangeText={setDisplayName} placeholder="ex: Thibaud M." style={styles.input} />
-
- <Text style={[ui.muted, { marginTop: 12 }]}>
- ⚠️ Email/username seront modifiables uniquement quand l’API serveur/DB sera en place (et avec validation).
- </Text>
- </View>
-
- <View style={ui.card}>
- <Text style={ui.title}>Mot de passe</Text>
- <Text style={ui.muted}>
- Change optionnel. (Local pour l’instant, sera relié à l’API plus tard.)
- </Text>
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Nouveau mot de passe</Text>
- <TextInput value={password} onChangeText={setPassword} secureTextEntry placeholder="min 6 caractères" style={styles.input} />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Confirmation</Text>
- <TextInput value={password2} onChangeText={setPassword2} secureTextEntry placeholder="retaper le mot de passe" style={styles.input} />
- </View>
-
- {!!error && <Text style={styles.errorText}>{error}</Text>}
- {!!info && <Text style={[ui.muted, { marginBottom: 10 }]}>{info}</Text>}
-
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
- <Text style={ui.buttonText}>Sauvegarder</Text>
- </TouchableOpacity>
- </View>
- </SafeAreaView>
- );
-}
-
-const styles = StyleSheet.create({
- safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
-
- title: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
-
- mono: {
- marginTop: 6,
- fontFamily: "monospace",
- color: "#0f172a",
- opacity: 0.8,
- },
-
- input: {
- borderWidth: 1,
- borderColor: "#e5e7eb",
- borderRadius: 10,
- paddingHorizontal: 12,
- paddingVertical: 10,
- marginTop: 8,
- 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
+++ /dev/null
-import { View, Text, StyleSheet, TextInput, TouchableOpacity } from "react-native";
-import { SafeAreaView } from "react-native-safe-area-context";
-import { useMemo, useState } from "react";
-
-import { ui } from "../components/ui/uiStyles";
-import { login as authLogin, register as authRegister } from "../services/api/authApi";
-
-/**
- * AuthScreen
- * ----------
- * Connexion + Création de compte.
- * - Identifiant : email OU nom d’utilisateur
- * - Mot de passe : minimum 6 caractères
- *
- * Note technique (code) :
- * - L’écran appelle authApi (façade).
- * - Le stockage de session est géré côté utils.
- */
-export default function AuthScreen({ onAuthenticated }: { onAuthenticated: () => void }) {
- const [mode, setMode] = useState<"login" | "register">("login");
- const [error, setError] = useState<string | null>(null);
-
- // Connexion
- const [login, setLogin] = useState("");
- const [password, setPassword] = useState("");
-
- // Création compte
- const [email, setEmail] = useState("");
- const [username, setUsername] = useState("");
- const [displayName, setDisplayName] = useState("");
- const [password2, setPassword2] = useState("");
-
- 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 title = useMemo(() => (mode === "login" ? "Connexion" : "Créer un compte"), [mode]);
-
- const handleLogin = async () => {
- setError(null);
-
- const l = login.trim();
- const p = password;
-
- if (!l) return setError("Veuillez entrer un email ou un nom d’utilisateur.");
- if (!p || p.length < 6) return setError("Mot de passe invalide (minimum 6 caractères).");
-
- const res = await authLogin({ login: l, password: p });
- if (!res.ok) return setError(res.message);
-
- 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("Nom d’utilisateur invalide (3 à 20, lettres/chiffres/_).");
- if (!p1 || p1.length < 6) return setError("Mot de passe trop court (minimum 6 caractères).");
- if (p1 !== p2) return setError("Les mots de passe ne correspondent pas.");
-
- const res = await authRegister({
- email: e,
- username: u,
- displayName: d || undefined,
- password: p1,
- });
-
- if (!res.ok) return setError(res.message);
-
- onAuthenticated();
- };
-
- return (
- <SafeAreaView style={styles.safeArea}>
- <View style={ui.container}>
- <Text style={styles.title}>{title}</Text>
-
- {/* Onglets */}
- <View style={styles.tabsRow}>
- <TouchableOpacity
- style={[styles.tab, mode === "login" && styles.tabActive]}
- onPress={() => {
- setMode("login");
- setError(null);
- }}
- >
- <Text style={[styles.tabText, mode === "login" && styles.tabTextActive]}>Connexion</Text>
- </TouchableOpacity>
-
- <TouchableOpacity
- style={[styles.tab, mode === "register" && styles.tabActive]}
- onPress={() => {
- setMode("register");
- setError(null);
- }}
- >
- <Text style={[styles.tabText, mode === "register" && styles.tabTextActive]}>Créer un compte</Text>
- </TouchableOpacity>
- </View>
-
- <View style={ui.card}>
- {mode === "login" ? (
- <>
- <Text style={ui.muted}>Email ou nom d’utilisateur</Text>
- <TextInput
- value={login}
- onChangeText={setLogin}
- autoCapitalize="none"
- placeholder="ex : user@example.com ou pseudo"
- style={styles.input}
- />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Mot de passe</Text>
- <TextInput
- value={password}
- onChangeText={setPassword}
- secureTextEntry
- placeholder="minimum 6 caractères"
- style={styles.input}
- />
-
- {!!error && <Text style={styles.errorText}>{error}</Text>}
-
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleLogin}>
- <Text style={ui.buttonText}>Se connecter</Text>
- </TouchableOpacity>
- </>
- ) : (
- <>
- <Text style={ui.muted}>Email</Text>
- <TextInput
- value={email}
- onChangeText={setEmail}
- autoCapitalize="none"
- keyboardType="email-address"
- placeholder="ex : user@example.com"
- style={styles.input}
- />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Nom d’utilisateur</Text>
- <TextInput
- value={username}
- onChangeText={setUsername}
- autoCapitalize="none"
- placeholder="ex : pseudo_123"
- style={styles.input}
- />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Nom / prénom (optionnel)</Text>
- <TextInput
- value={displayName}
- onChangeText={setDisplayName}
- placeholder="ex : Thibaud"
- style={styles.input}
- />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Mot de passe</Text>
- <TextInput
- value={password}
- onChangeText={setPassword}
- secureTextEntry
- placeholder="minimum 6 caractères"
- style={styles.input}
- />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Confirmer le mot de passe</Text>
- <TextInput
- value={password2}
- onChangeText={setPassword2}
- secureTextEntry
- placeholder="retapez le mot de passe"
- style={styles.input}
- />
-
- {!!error && <Text style={styles.errorText}>{error}</Text>}
-
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleRegister}>
- <Text style={ui.buttonText}>Créer le compte</Text>
- </TouchableOpacity>
- </>
- )}
- </View>
- </View>
- </SafeAreaView>
- );
-}
-
-const styles = StyleSheet.create({
- 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,
- paddingVertical: 10,
- borderRadius: 12,
- borderWidth: 1,
- borderColor: "#e5e7eb",
- backgroundColor: "#fff",
- alignItems: "center",
- },
- tabActive: {
- borderColor: "#0f172a",
- },
- tabText: { fontWeight: "900", color: "#0f172a", opacity: 0.7 },
- tabTextActive: { opacity: 1 },
-
- input: {
- borderWidth: 1,
- borderColor: "#e5e7eb",
- borderRadius: 10,
- paddingHorizontal: 12,
- paddingVertical: 10,
- marginTop: 8,
- backgroundColor: "#fff",
- color: "#0f172a",
- },
-
- fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginTop: 12 },
-
- errorText: { marginTop: 10, color: "#dc2626", fontWeight: "900" },
-});
\ No newline at end of file
// Dashboard summary (prix + signal) => dépend du pair BTC/EUR ou BTC/USDT
try {
- const dash = await getDashboardSummary();
- setSummary(dash);
- } catch {
- setSummary(null);
- setSoftError("Données indisponibles pour le moment. Réessayez dans quelques secondes.");
- }
+ const dash = await getDashboardSummary();
+ setSummary(dash);
+} catch (e: any) {
+ setSummary(null);
+
+ const msg = String(e?.message ?? "");
+ if (msg.includes("401")) {
+ setSoftError("Accès refusé. Vérifiez l’accès protégé du serveur.");
+ } else if (msg.toLowerCase().includes("prix")) {
+ setSoftError(msg);
+ } else {
+ setSoftError("Signal indisponible pour le moment.");
+ }
+}
// Wallet API (source de vérité) -> cache local
if (uid) {
<Text style={ui.muted}>{displayPair}</Text>
<Text style={styles.bigValue}>
- {(summary?.price ?? 0).toFixed(2)} {quote}
+ {summary ? `${summary.price.toFixed(2)} ${quote}` : "—"}
</Text>
{/* Info transparente : prix/signal serveur sont basés sur BTC (DB) */}
+++ /dev/null
-import type { AuthUser } from "../../models/AuthUser";
-import type { Session } from "../../utils/sessionStorage";
-
-import { createUser, verifyLogin, findUserById } from "../../utils/authUsersStorage";
-import { loadSession, saveSession, clearSession } from "../../utils/sessionStorage";
-
-/**
- * authApi.ts
- * ----------
- * Décision projet : PAS d'auth serveur.
- *
- * IMPORTANT :
- * - Les services (alerts/signal/wallets) utilisent un userId FIXE côté serveur : "user-123".
- * - Donc la session côté mobile doit utiliser ce userId pour toutes les routes API.
- *
- * L'auth locale sert uniquement à l'expérience utilisateur (écran de connexion),
- * mais le userId utilisé pour l'API = "user-123" (align serveur).
- */
-
-const SERVER_USER_ID = "user-123";
-
-export type AuthResult =
- | { ok: true; user: AuthUser; session: Session }
- | { ok: false; message: string };
-
-export async function register(params: {
- email: string;
- username: string;
- displayName?: string;
- password: string;
-}): Promise<AuthResult> {
- const res = await createUser(params);
- if (!res.ok) return { ok: false, message: res.message };
-
- // ✅ userId aligné serveur
- const session: Session = {
- userId: SERVER_USER_ID,
- email: res.user.email,
- createdAtMs: Date.now(),
- };
-
- await saveSession(session);
-
- return { ok: true, user: res.user, session };
-}
-
-export async function login(params: {
- login: string; // email OU username
- password: string;
-}): Promise<AuthResult> {
- const res = await verifyLogin(params);
- if (!res.ok) return { ok: false, message: res.message };
-
- // ✅ userId aligné serveur
- const session: Session = {
- userId: SERVER_USER_ID,
- email: res.user.email,
- createdAtMs: Date.now(),
- };
-
- await saveSession(session);
-
- return { ok: true, user: res.user, session };
-}
-
-export async function logout(): Promise<void> {
- await clearSession();
-}
-
-/**
- * Retourne l'utilisateur courant (profil local).
- * (Le userId serveur étant fixe, on conserve le profil local via la recherche par session.email si besoin.)
- */
-export async function getCurrentUser(): Promise<AuthUser | null> {
- const session = await loadSession();
- if (!session) return null;
-
- // On tente par userId local d'abord (si jamais), sinon on retombe sur findUserById.
- // Note : si ton système local lie le profil à un autre userId, on peut améliorer plus tard.
- return await findUserById((session as any).userId ?? SERVER_USER_ID);
-}
-
-export async function getSession(): Promise<Session | null> {
- return await loadSession();
-}
\ No newline at end of file
return "HOLD";
}
+function toNumber(x: any, fallback = 0): number {
+ const n = typeof x === "number" ? x : Number(x);
+ return Number.isFinite(n) ? n : fallback;
+}
+
function confidenceToLevel(conf: number): AlertLevel {
if (conf >= 0.85) return "CRITICAL";
if (conf >= 0.65) return "WARNING";
return "INFO";
}
-function toNumber(x: any, fallback = 0): number {
- const n = typeof x === "number" ? x : Number(x);
- return Number.isFinite(n) ? n : fallback;
-}
-
export async function getDashboardSummary(): Promise<DashboardSummary> {
const session = await loadSession();
const userId = session?.userId;
- if (!userId) throw new Error("Session absente : impossible de charger le dashboard.");
+ if (!userId) throw new Error("Session absente.");
const settings = await loadSettings();
const quote: "EUR" | "USDT" = settings.currency === "USDT" ? "USDT" : "EUR";
const pair = `BTC/${quote}`;
- // 1) Prix (toujours OK)
- const priceRes = await getCurrentPrice(pair);
+ // 1) Prix
+ let priceRes;
+ try {
+ priceRes = await getCurrentPrice(pair);
+ } catch (e: any) {
+ throw new Error(e?.message ?? `Prix indisponible pour ${pair}.`);
+ }
// 2) Signal (peut être null)
- const sig = await apiGet<any>(
- `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
- );
+ let sig: any = null;
+ try {
+ sig = await apiGet<any>(
+ `/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
+ );
+ } catch (e: any) {
+ // Signal indisponible, mais prix OK → on renvoie un “dashboard minimal”
+ return {
+ pair,
+ price: priceRes.price,
+ strategy: settings.selectedStrategyKey,
+ decision: "HOLD",
+ confidence: 0,
+ reason: "Signal indisponible pour le moment.",
+ alertLevel: "INFO",
+ timestamp: priceRes.timestampMs,
+ };
+ }
- // serveur renvoie { ok:true, data: ... } => http.ts unwrap => sig = data
+ // Pas de signal récent
if (!sig) {
- // pas de signal récent : on renvoie un dashboard “HOLD” propre
return {
pair,
price: priceRes.price,
};
}
- // Champs réels observés :
- // action, confidence (string), reason, timestamp_ms, pair_code
const decision = safeDecision(sig.action ?? sig.decision);
const confidence = toNumber(sig.confidence, 0);
const reason = String(sig.reason ?? sig.message ?? "—");
-
const timestamp =
toNumber(sig.timestamp_ms, 0) ||
toNumber(sig.timestamp, 0) ||
priceRes.timestampMs;
- const alertLevel =
- (String(sig.alertLevel ?? sig.criticality ?? "").toUpperCase() as AlertLevel) ||
- confidenceToLevel(confidence);
-
- // pair_code si présent, sinon pair demandé
const pairOut = String(sig.pair_code ?? sig.pair ?? pair);
+ const levelFromServer = String(sig.alertLevel ?? sig.criticality ?? "").toUpperCase();
+
+ const alertLevel: AlertLevel =
+ levelFromServer === "CRITICAL" || levelFromServer === "WARNING" || levelFromServer === "INFO"
+ ? (levelFromServer as AlertLevel)
+ : confidenceToLevel(confidence);
return {
pair: pairOut,
decision,
confidence,
reason,
- alertLevel: alertLevel === "CRITICAL" || alertLevel === "WARNING" || alertLevel === "INFO"
- ? alertLevel
- : confidenceToLevel(confidence),
+ alertLevel,
timestamp,
};
}
\ No newline at end of file
-import { API_BASE_URL } from "../../config/env";
-import { BASIC_AUTH_USER, BASIC_AUTH_PASS } from "../../config/env";
+import { API_BASE_URL, BASIC_AUTH_USER, BASIC_AUTH_PASS } from "../../config/env";
import { Buffer } from "buffer";
-/**
- * HTTP helper (fetch)
- * -------------------
- * - Ajoute Basic Auth si configuré (pour passer la barrière 401)
- * - Compatible avec 2 formats :
- * A) wrap : { ok:true, data: ... } / { ok:false, error:{message} }
- * B) raw : { ... } / [...]
- */
-
function getBasicAuthHeader(): string | null {
const u = (BASIC_AUTH_USER ?? "").trim();
const p = (BASIC_AUTH_PASS ?? "").trim();
source?: string;
};
+function toNumber(x: any): number | null {
+ if (typeof x === "number" && Number.isFinite(x)) return x;
+ if (typeof x === "string") {
+ const n = Number(x);
+ return Number.isFinite(n) ? n : null;
+ }
+ return null;
+}
+
export async function getCurrentPrice(pair: string): Promise<PriceCurrent> {
const data = await apiGet<any>(`/price/current?pair=${encodeURIComponent(pair)}`);
const price =
- (typeof data?.current_price === "number" ? data.current_price : null) ??
- (typeof data?.price === "number" ? data.price : null);
+ toNumber(data?.current_price) ??
+ toNumber(data?.price);
- if (typeof price !== "number" || !Number.isFinite(price)) {
- throw new Error("Prix invalide (API).");
+ if (price === null) {
+ throw new Error(`Prix indisponible pour ${pair}.`);
}
const ts =
- (typeof data?.timestamp_ms === "number" ? data.timestamp_ms : null) ??
- (typeof data?.timestampMs === "number" ? data.timestampMs : null) ??
+ toNumber(data?.timestamp_ms) ??
+ toNumber(data?.timestampMs) ??
Date.now();
return {
pair: String(data?.pair ?? pair),
timestampMs: Number(ts),
- price: Number(price),
+ price,
source: typeof data?.source === "string" ? data.source : undefined,
};
}
\ No newline at end of file
import { Platform } from "react-native";
import type { Alert } from "../types/Alert";
-/**
- * Notification handler (Foreground)
- * ---------------------------------
- * Sur certaines versions, Expo demande aussi shouldShowBanner/shouldShowList.
- * On les met explicitement pour éviter les erreurs TypeScript.
- */
Notifications.setNotificationHandler({
handleNotification: async () => ({
- shouldShowAlert: true,
- shouldPlaySound: true,
- shouldSetBadge: false,
-
- // ✅ champs demandés par les versions récentes
shouldShowBanner: true,
shouldShowList: true,
+ shouldPlaySound: false,
+ shouldSetBadge: false,
}),
});
-/**
- * Initialisation Android (channel)
- * --------------------------------
- * Sur Android, un channel "alerts" permet d'avoir une importance HIGH.
- */
export async function initNotificationChannel(): Promise<void> {
if (Platform.OS !== "android") return;
});
}
+function safeLevel(alert: Alert): string {
+ return String(alert.alertLevel ?? alert.criticality ?? "INFO");
+}
+
+function safeAction(alert: Alert): string {
+ return String(alert.action ?? "HOLD");
+}
+
+function safePair(alert: Alert): string {
+ return String(alert.pair ?? "—");
+}
+
+function safeConfidence(alert: Alert): number {
+ const c = alert.confidence;
+ return typeof c === "number" && Number.isFinite(c) ? c : 0;
+}
+
function formatTitle(alert: Alert): string {
- return `${alert.alertLevel} — ${alert.action} ${alert.pair}`;
+ return `${safeLevel(alert)} — ${safeAction(alert)} ${safePair(alert)}`;
}
function formatBody(alert: Alert): string {
- const conf = Math.round(alert.confidence * 100);
- return `${conf}% — ${alert.reason}`;
+ const confPct = Math.round(safeConfidence(alert) * 100);
+ const reason = String(alert.reason ?? alert.message ?? "—");
+ return `${confPct}% — ${reason}`;
}
-/**
- * Demande de permission
- * ---------------------
- * Retourne true si l'utilisateur accepte.
- */
export async function requestNotificationPermission(): Promise<boolean> {
const current = await Notifications.getPermissionsAsync();
if (current.status === "granted") return true;
return req.status === "granted";
}
-/**
- * Afficher une notification locale immédiate
- * ------------------------------------------
- * data doit être un Record<string, unknown> (pas un objet typé direct).
- */
export async function showAlertNotification(alert: Alert): Promise<void> {
await initNotificationChannel();
const data: Record<string, unknown> = {
- pair: alert.pair,
- action: alert.action,
- alertLevel: alert.alertLevel,
- confidence: alert.confidence,
- reason: alert.reason,
- price: alert.price ?? null,
- timestamp: alert.timestamp ?? Date.now(),
+ pair: alert.pair ?? null,
+ action: alert.action ?? null,
+ alertLevel: alert.alertLevel ?? alert.criticality ?? null,
+ confidence: safeConfidence(alert),
+ reason: alert.reason ?? alert.message ?? null,
+ price: typeof alert.price === "number" ? alert.price : null,
+ timestamp: typeof alert.timestamp === "number" ? alert.timestamp : Date.now(),
};
await Notifications.scheduleNotificationAsync({
data,
sound: "default",
},
- trigger: null, // immédiat
+ trigger: null,
});
}
\ No newline at end of file
this.currentServerUrl = serverUrl;
this.currentUserId = userId;
- const attachHandlers = (sock: Socket) => {
- const emitAuth = () => sock.emit("auth", userId);
-
- sock.on("connect", () => {
- console.log("✅ Socket connecté:", sock.id);
- emitAuth();
- });
-
- sock.on("auth_success", (data: any) => {
- console.log("✅ Auth success:", data?.message ?? data);
- });
-
- sock.on("alert", (alert: Alert) => {
- for (const cb of this.listeners) cb(alert);
- });
-
- sock.on("disconnect", (reason: string) => {
- console.log("⚠️ Socket disconnect:", reason);
- });
-
- sock.on("error", (err: any) => {
- console.log("❌ Socket error:", err?.message ?? err);
- });
- };
-
- // tentative websocket + polling
const sock = io(serverUrl, {
path: "/socket.io",
- transports: ["websocket", "polling"],
+ transports: ["websocket"], // ✅ on force websocket (plus stable en prod)
+ upgrade: false, // ✅ évite switch polling->ws
reconnection: true,
- reconnectionAttempts: 10,
+ reconnectionAttempts: Infinity, // ✅ mais...
+ reconnectionDelay: 1500,
timeout: 10000,
});
this.socket = sock;
- attachHandlers(sock);
+
+ sock.on("connect", () => {
+ console.log("✅ Socket connecté:", sock.id);
+ sock.emit("auth", userId);
+ });
+
+ sock.on("auth_success", (data: any) => {
+ console.log("✅ Auth success:", data?.message ?? data);
+ });
+
+ sock.on("alert", (alert: Alert) => {
+ for (const cb of this.listeners) cb(alert);
+ });
+
+ sock.on("disconnect", (reason: string) => {
+ console.log("⚠️ Socket disconnect:", reason);
+ });
sock.on("connect_error", (err: any) => {
- const msg = String(err?.message ?? err);
- console.log("❌ Socket connect_error:", msg);
-
- // fallback polling-only si websocket échoue
- if (msg.toLowerCase().includes("websocket")) {
- console.log("↩️ Fallback: polling-only");
- this.disconnect();
-
- const sockPolling = io(serverUrl, {
- path: "/socket.io",
- transports: ["polling"],
- upgrade: false,
- reconnection: true,
- reconnectionAttempts: 10,
- timeout: 10000,
- });
-
- this.socket = sockPolling;
- attachHandlers(sockPolling);
-
- sockPolling.on("connect_error", (e: any) => {
- console.log("❌ Socket polling connect_error:", e?.message ?? e);
- });
- }
+ // ✅ log simple, pas de fallback spam
+ console.log("❌ Socket connect_error:", err?.message ?? err);
+ });
+
+ sock.on("error", (err: any) => {
+ console.log("❌ Socket error:", err?.message ?? err);
});
}
import type { StrategyOption, StrategyKey } from "../types/Strategy";
-import { getStrategies } from "../mocks/strategies.mock";
-
-/**
- * strategyService
- * ---------------
- * Aujourd'hui : mock
- * Demain : REST
- *
- * REST attendu (contrat groupe) :
- * - POST /api/strategy/select
- */
+import { apiGet } from "./api/http";
+
+type Risk = "SAFE" | "NORMAL" | "AGGRESSIVE";
+
+function asArray(x: any): any[] {
+ if (Array.isArray(x)) return x;
+ if (Array.isArray(x?.strategies)) return x.strategies;
+ if (Array.isArray(x?.items)) return x.items;
+ if (Array.isArray(x?.data)) return x.data;
+ return [];
+}
+
+function normalizeRisk(raw: any): Risk {
+ const v = String(raw ?? "").toUpperCase();
+ if (v === "SAFE") return "SAFE";
+ if (v === "AGGRESSIVE") return "AGGRESSIVE";
+ if (v === "NORMAL") return "NORMAL";
+ return "NORMAL";
+}
+
+function normalizeStrategy(raw: any): StrategyOption | null {
+ const key = String(raw?.key ?? raw?.mode ?? raw?.strategy_key ?? raw?.strategyKey ?? "").trim();
+ if (!key) return null;
+
+ return {
+ key: key as StrategyKey,
+ label: String(raw?.label ?? raw?.name ?? key),
+ description: String(raw?.description ?? "—"),
+ risk: normalizeRisk(raw?.risk ?? raw?.level ?? raw?.modeRisk),
+ };
+}
+
export async function fetchStrategies(): Promise<StrategyOption[]> {
- return await getStrategies();
+ const candidates = ["/strategy/list", "/strategy/available", "/strategy/modes"];
+
+ for (const path of candidates) {
+ try {
+ const data = await apiGet<any>(path);
+ const arr = asArray(data);
+ const normalized = arr.map(normalizeStrategy).filter(Boolean) as StrategyOption[];
+ if (normalized.length > 0) return normalized;
+ } catch {
+ // continue
+ }
+ }
+
+ // Fallback “propre” (sans mocks)
+ return [
+ {
+ key: "RSI_SIMPLE" as StrategyKey,
+ label: "RSI Simple",
+ description: "Signal basé sur RSI (surachat / survente).",
+ risk: "NORMAL",
+ },
+ {
+ key: "EMA_CROSS" as StrategyKey,
+ label: "EMA Cross",
+ description: "Croisement de moyennes mobiles exponentielles.",
+ risk: "NORMAL",
+ },
+ {
+ key: "ALWAYS_HOLD" as StrategyKey,
+ label: "Always HOLD",
+ description: "Ne déclenche jamais d’achat/vente.",
+ risk: "SAFE",
+ },
+ ];
}
-/**
- * Placeholder : plus tard on fera le POST ici.
- * Pour le moment, la sélection est gérée via AsyncStorage (settings).
- */
export async function selectStrategy(_strategyKey: StrategyKey): Promise<void> {
return;
}
\ No newline at end of file