-/**
- * UserSettings
- * ------------
- * Paramètres utilisateur stockés localement (AsyncStorage).
- * Step 1/2/3 : pas d'auth obligatoire, donc userId peut être absent.
- */
-export interface UserSettings {
- // (optionnel) utile pour Socket.IO et futur Step 4 (auth)
- userId?: string;
+export type Currency = "EUR" | "USDT";
- // Paramètres UI / usage
- currency: "EUR" | "USD";
+export interface UserSettings {
+ currency: Currency;
favoriteSymbol: string;
-
- // Préférences alertes
alertPreference: "critical" | "all";
-
- // Rafraîchissement dashboard
refreshMode: "manual" | "auto";
-
- // Notifications locales (Expo)
notificationsEnabled: boolean;
-
- // Stratégie choisie (persistée)
- // Exemple: "RSI_SIMPLE"
selectedStrategyKey: string;
}
\ No newline at end of file
import { findUserById } from "../utils/authUsersStorage";
/**
- * AboutScreen
- * ------------------------
- * "Détails du compte" + sections À propos / Support / Version.
- * Les infos affichées viennent du compte local (AuthUsersStorage) :
+ * AboutScreen
+ * -----------
+ * Détails du compte + sections À propos / Support / Version.
+ * Les infos affichées viennent du compte local :
* - displayName (optionnel)
* - username
* - email
setEmail(user?.email ?? session.email);
}
- init();
+ void init();
return () => {
active = false;
<Text style={styles.rowTitle}>{title}</Text>
{!!subtitle && <Text style={ui.muted}>{subtitle}</Text>}
</View>
- <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.25 }} />
+ <Ionicons
+ name="chevron-forward-outline"
+ size={18}
+ color="#0f172a"
+ style={{ opacity: 0.25 }}
+ />
</View>
);
<View style={ui.container}>
<Text style={styles.title}>Détails du compte</Text>
- {/* Carte profil (style WF-09) */}
+ {/* Carte profil */}
<View style={ui.card}>
<View style={styles.profileRow}>
<Ionicons
/>
<View style={{ flex: 1 }}>
<Text style={styles.profileName}>{displayName}</Text>
- <Text style={ui.muted} numberOfLines={1}>{email}</Text>
+ <Text style={ui.muted} numberOfLines={1}>
+ {email}
+ </Text>
<Text style={[ui.muted, { marginTop: 2 }]} numberOfLines={1}>
@{username}
</Text>
<View style={styles.metaBox}>
<Text style={styles.metaLabel}>UserId</Text>
- <Text style={styles.metaValue} numberOfLines={1}>{userId}</Text>
+ <Text style={styles.metaValue} numberOfLines={1}>
+ {userId}
+ </Text>
</View>
</View>
<View style={ui.card}>
<Text style={ui.title}>À propos</Text>
- <Row icon="sparkles-outline" title="Wall-e-tte" subtitle="Conseiller crypto — projet PDW 2025-2026" />
+ <Row
+ icon="sparkles-outline"
+ title="Wallette"
+ subtitle="Conseiller crypto — projet PDW 2025-2026"
+ />
<Row
icon="shield-checkmark-outline"
title="Avertissement"
- subtitle="Outil éducatif : ce n’est pas un conseil financier."
+ subtitle="Outil éducatif : ceci n’est pas un conseil financier."
/>
</View>
<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)." />
+ <Row icon="mail-outline" title="Contact" subtitle="Contactez l’équipe du projet." />
</View>
{/* 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>
{/* Bouton discret (optionnel) */}
import { ui } from "../components/ui/uiStyles";
import type { Alert } from "../types/Alert";
import { alertStore } from "../services/alertStore";
-
-/**
- * ✅ API-ready (façade)
- * Aujourd'hui : clear local store
- * Demain : REST /api/alerts/events ... etc.
- */
import { clearAlertsLocal } from "../services/api/alertsApi";
-/**
- * AlertsScreen
- * -----------
- * Liste des alertes reçues (Socket.IO) stockées dans alertStore.
- *
- * Objectifs :
- * - Affichage clair des enums (CRITICAL/WARNING/INFO, BUY/SELL/HOLD/STOP_LOSS)
- * - Tri : CRITICAL > WARNING > INFO, puis récent -> ancien
- * - Filtre (par défaut : CRITICAL)
- * - Clear avec confirmation
- */
type Filter = "CRITICAL" | "WARNING" | "INFO" | "ALL";
-function severityRank(level: Alert["alertLevel"]): number {
+function severityRank(level?: Alert["alertLevel"]): number {
switch (level) {
case "CRITICAL":
return 3;
}
}
-function actionColor(action: Alert["action"]): string {
+function actionColor(action?: Alert["action"]): string {
switch (action) {
case "BUY":
return "#16a34a";
}
}
-function levelColor(level: Alert["alertLevel"]): string {
+function levelColor(level?: Alert["alertLevel"]): string {
switch (level) {
case "CRITICAL":
return "#b91c1c";
}
}
-function formatDate(ms: number): string {
- return new Date(ms).toLocaleString();
+function formatDate(ms?: number): string {
+ const t = typeof ms === "number" && Number.isFinite(ms) ? ms : Date.now();
+ return new Date(t).toLocaleString();
}
export default function AlertsScreen() {
const [filter, setFilter] = useState<Filter>("CRITICAL");
useEffect(() => {
- // Load initial
setItems(alertStore.getAll?.() ?? []);
- // Live updates if subscribe exists
const unsub = alertStore.subscribe?.((all: Alert[]) => {
setItems(all);
});
const filteredSorted = useMemo(() => {
const base =
- filter === "ALL" ? items : items.filter((a) => a.alertLevel === filter);
+ filter === "ALL" ? items : items.filter((a) => (a.alertLevel ?? "INFO") === filter);
return [...base].sort((a, b) => {
- // 1) severity
const r = severityRank(b.alertLevel) - severityRank(a.alertLevel);
if (r !== 0) return r;
- // 2) timestamp desc
return (b.timestamp ?? 0) - (a.timestamp ?? 0);
});
}, [items, filter]);
const handleClear = useCallback(() => {
RNAlert.alert(
"Supprimer les alertes ?",
- "Cette action supprime les alertes stockées localement (alertStore).",
+ "Cette action supprime les alertes stockées sur l’app.",
[
{ text: "Annuler", style: "cancel" },
{
onPress={() => setFilter(value)}
style={[styles.filterBtn, active && styles.filterBtnActive]}
>
- <Text style={[styles.filterText, active && styles.filterTextActive]}>
- {label}
- </Text>
+ <Text style={[styles.filterText, active && styles.filterTextActive]}>{label}</Text>
</TouchableOpacity>
);
};
contentContainerStyle={ui.container}
data={filteredSorted}
keyExtractor={(it, idx) =>
- // Alert n'a pas forcément d'id => fallback stable
(it as any).id ??
- `${it.timestamp}-${it.pair}-${it.action}-${it.alertLevel}-${idx}`
+ `${it.timestamp ?? "0"}-${it.pair ?? ""}-${it.action ?? ""}-${it.alertLevel ?? ""}-${idx}`
}
ListHeaderComponent={
<View style={{ marginBottom: 12 }}>
<Text style={styles.screenTitle}>Alertes</Text>
<TouchableOpacity onPress={handleClear} style={styles.clearBtn}>
- <Text style={styles.clearBtnText}>Clear</Text>
+ <Text style={styles.clearBtnText}>Supprimer</Text>
</TouchableOpacity>
</View>
<View style={styles.filtersRow}>
<FilterButton value="CRITICAL" label="Critiques" />
- <FilterButton value="WARNING" label="Warnings" />
- <FilterButton value="INFO" label="Info" />
+ <FilterButton value="WARNING" label="Avertissements" />
+ <FilterButton value="INFO" label="Informations" />
<FilterButton value="ALL" label="Toutes" />
</View>
<Text style={ui.muted}>
- Tri : CRITICAL → WARNING → INFO, puis récent → ancien.
+ Tri : critique → avertissement → info, puis du plus récent au plus ancien.
</Text>
</View>
}
const aColor = actionColor(item.action);
const lColor = levelColor(item.alertLevel);
+ const confidence =
+ typeof item.confidence === "number" && Number.isFinite(item.confidence)
+ ? item.confidence
+ : 0;
+
return (
<View style={ui.card}>
<View style={ui.rowBetween}>
- <Text style={ui.valueBold}>{item.pair}</Text>
+ <Text style={ui.valueBold}>{item.pair ?? "—"}</Text>
<Text style={ui.muted}>{formatDate(item.timestamp)}</Text>
</View>
<View style={styles.badgesRow}>
<View style={[ui.badge, { backgroundColor: `${aColor}22`, marginTop: 0 }]}>
- <Text style={[ui.badgeText, { color: aColor }]}>{item.action}</Text>
+ <Text style={[ui.badgeText, { color: aColor }]}>{item.action ?? "HOLD"}</Text>
</View>
<View style={[ui.badge, { backgroundColor: `${lColor}22`, marginTop: 0 }]}>
- <Text style={[ui.badgeText, { color: lColor }]}>{item.alertLevel}</Text>
+ <Text style={[ui.badgeText, { color: lColor }]}>{item.alertLevel ?? "INFO"}</Text>
</View>
</View>
<View style={[ui.rowBetween, { marginTop: 10 }]}>
<Text style={ui.value}>Confiance</Text>
- <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(0)}%</Text>
+ <Text style={ui.valueBold}>{Math.round(confidence * 100)}%</Text>
</View>
- <Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
+ <Text style={[ui.muted, { marginTop: 10 }]} numberOfLines={4}>
+ {item.reason ?? item.message ?? "—"}
+ </Text>
- {!!item.price && (
+ {typeof item.price === "number" && Number.isFinite(item.price) && (
<Text style={[ui.muted, { marginTop: 6 }]}>
- Prix (optionnel) : {item.price.toFixed(2)}
+ Prix : {item.price.toFixed(2)}
</Text>
)}
</View>
import { login as authLogin, register as authRegister } from "../services/api/authApi";
/**
- * AuthScreen (Step local, sans API serveur)
- * ----------------------------------------
- * Mode Connexion + Mode Création de compte.
- * - Identifiant: email OU username
- * - Mot de passe obligatoire
+ * AuthScreen
+ * ----------
+ * Connexion + Création de compte.
+ * - Identifiant : email OU nom d’utilisateur
+ * - Mot de passe : minimum 6 caractères
*
- * IMPORTANT :
- * - L'écran appelle authApi (façade).
- * - Plus tard, authApi sera remplacé par des appels REST,
- * sans modifier cet écran.
+ * 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("demo@example.com");
+ const [login, setLogin] = useState("");
const [password, setPassword] = useState("");
- // Register
- const [email, setEmail] = useState("demo@example.com");
- const [username, setUsername] = useState("demo");
+ // Création compte
+ const [email, setEmail] = useState("");
+ const [username, setUsername] = useState("");
const [displayName, setDisplayName] = useState("");
const [password2, setPassword2] = useState("");
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 (min 6).");
+ 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);
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 (min 6).");
+ 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({
<View style={ui.container}>
<Text style={styles.title}>{title}</Text>
- {/* Tabs */}
+ {/* Onglets */}
<View style={styles.tabsRow}>
<TouchableOpacity
style={[styles.tab, mode === "login" && styles.tabActive]}
value={login}
onChangeText={setLogin}
autoCapitalize="none"
- placeholder="ex: demo@example.com ou demo"
+ placeholder="ex : user@example.com ou pseudo"
style={styles.input}
/>
value={password}
onChangeText={setPassword}
secureTextEntry
- placeholder="min 6 caractères"
+ placeholder="minimum 6 caractères"
style={styles.input}
/>
<TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleLogin}>
<Text style={ui.buttonText}>Se connecter</Text>
</TouchableOpacity>
-
- <Text style={[ui.muted, { marginTop: 10 }]}>
- (Mode local sans API serveur : destiné au développement / démo.)
- </Text>
</>
) : (
<>
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
- placeholder="ex: demo@example.com"
+ placeholder="ex : user@example.com"
style={styles.input}
/>
value={username}
onChangeText={setUsername}
autoCapitalize="none"
- placeholder="ex: demo_123"
+ placeholder="ex : pseudo_123"
style={styles.input}
/>
- <Text style={[ui.muted, { marginTop: 10 }]}>Nom / Prénom (optionnel)</Text>
+ <Text style={[ui.muted, { marginTop: 10 }]}>Nom / prénom (optionnel)</Text>
<TextInput
value={displayName}
onChangeText={setDisplayName}
- placeholder="ex: Thibaud M."
+ placeholder="ex : Thibaud"
style={styles.input}
/>
value={password}
onChangeText={setPassword}
secureTextEntry
- placeholder="min 6 caractères"
+ placeholder="minimum 6 caractères"
style={styles.input}
/>
- <Text style={[ui.muted, { marginTop: 10 }]}>Confirmer</Text>
+ <Text style={[ui.muted, { marginTop: 10 }]}>Confirmer le mot de passe</Text>
<TextInput
value={password2}
onChangeText={setPassword2}
secureTextEntry
- placeholder="retaper le mot de passe"
+ placeholder="retapez le mot de passe"
style={styles.input}
/>
<TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleRegister}>
<Text style={ui.buttonText}>Créer le compte</Text>
</TouchableOpacity>
-
- <Text style={[ui.muted, { marginTop: 10 }]}>
- (Sans API serveur : stockage local + hash SHA-256, pas de mot de passe en clair.)
- </Text>
</>
)}
</View>
import { loadSettings } from "../utils/settingsStorage";
import { loadPortfolio, savePortfolio } from "../utils/portfolioStorage";
-import { SERVER_URL, API_BASE_URL, ENV_MODE } from "../config/env";
+import { SERVER_URL } from "../config/env";
import { socketService } from "../services/socketService";
import { alertStore } from "../services/alertStore";
import { showAlertNotification } from "../services/notificationService";
type CryptoSymbol = "BTC" | "ETH" | "LTC";
type TradeSide = "BUY" | "SELL";
+
const CRYPTOS: CryptoSymbol[] = ["BTC", "ETH", "LTC"];
function walletAddressKey(userId: string) {
});
const [selectedCrypto, setSelectedCrypto] = useState<CryptoSymbol>("BTC");
-
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [walletAddress, setWalletAddress] = useState("");
const [tradeQty, setTradeQty] = useState("0.01");
const [tradeInfo, setTradeInfo] = useState<string | null>(null);
- const currency: "EUR" | "USD" = settings?.currency === "USD" ? "USD" : "EUR";
- const pair = useMemo(() => `${selectedCrypto}/${currency}`, [selectedCrypto, currency]);
+ // ✅ Currency alignée DB/serveur : EUR ou USDT
+ const quote: "EUR" | "USDT" = settings?.currency === "USDT" ? "USDT" : "EUR";
+
+ // ✅ Pair affiché et utilisé côté serveur (DB ne connaît que BTC/EUR et BTC/USDT)
+ const serverPair = useMemo(() => `BTC/${quote}`, [quote]);
+
+ // UI pair : on peut afficher la crypto sélectionnée, mais sans inventer des paires serveur
+ const displayPair = useMemo(() => `${selectedCrypto}/${quote}`, [selectedCrypto, quote]);
const selectedQty = useMemo(() => {
const a = portfolio.assets.find((x) => normalizeSymbol(x.symbol) === selectedCrypto);
setWalletAddress("");
}
- // Dashboard summary (prix+signal)
+ // Dashboard summary (prix + signal) => dépend du pair BTC/EUR ou BTC/USDT
try {
const dash = await getDashboardSummary();
setSummary(dash);
} catch {
setSummary(null);
- setSoftError(`Signal/Prix indisponibles (API). DEV=${ENV_MODE}. Base REST: ${API_BASE_URL}`);
+ setSoftError("Données indisponibles pour le moment. Réessayez dans quelques secondes.");
}
- // Wallet API (si dispo) -> fusion
+ // Wallet API (source de vérité) -> cache local
if (uid) {
try {
const apiPortfolio = await getPortfolioFromApi();
}, [refreshAll])
);
- // Socket live (✅ connect seulement si userId existe)
+ // Socket live (connect uniquement si session)
useEffect(() => {
let unsub: null | (() => void) = null;
let alive = true;
setSocketConnected(true);
} catch {
if (!alive) return;
- setSocketInfo("Socket indisponible (URL / serveur).");
+ setSocketInfo("Socket indisponible pour le moment.");
return;
}
const trimmed = walletAddress.trim();
if (trimmed.length < 6) {
- setWalletAddressInfo("Adresse trop courte. Vérifie la saisie.");
+ setWalletAddressInfo("Adresse trop courte. Vérifiez la saisie.");
return;
}
const qty = Number(tradeQty.replace(",", ".").trim());
if (!Number.isFinite(qty) || qty <= 0) {
- setTradeInfo("Quantité invalide (ex: 0.01).");
+ setTradeInfo("Quantité invalide (ex : 0.01).");
return;
}
if (tradeSide === "BUY") nextQty = currentQty + qty;
else {
if (qty > currentQty) {
- setTradeInfo(`Vente impossible : tu n'as que ${currentQty.toFixed(6)} ${symbol}.`);
+ setTradeInfo(`Vente impossible : vous n'avez que ${currentQty.toFixed(6)} ${symbol}.`);
return;
}
nextQty = currentQty - qty;
{/* Crypto + adresse */}
<View style={ui.card}>
- <Text style={ui.title}>Choisir une cryptomonnaie</Text>
+ <Text style={ui.title}>Choix de cryptomonnaie</Text>
<View style={styles.cryptoRow}>
{CRYPTOS.map((c) => {
setWalletAddressInfo(null);
setWalletAddress(t);
}}
- placeholder="Entrez une adresse crypto"
+ placeholder="Entrez une adresse"
style={styles.input}
autoCapitalize="none"
autoCorrect={false}
<Text style={styles.secondaryButtonText}>Enregistrer l’adresse</Text>
</TouchableOpacity>
- {!!walletAddressInfo && <Text style={[ui.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>}
+ {!!walletAddressInfo && (
+ <Text style={[ui.muted, { marginTop: 8 }]}>{walletAddressInfo}</Text>
+ )}
</View>
{/* Solde */}
<Text style={styles.bigValue}>
{selectedQty.toFixed(6)} {selectedCrypto}
</Text>
- <Text style={ui.muted}>Portefeuille local (+ sync serveur si dispo)</Text>
+ <Text style={ui.muted}>Synchronisé avec le serveur quand disponible.</Text>
</View>
{/* Prix */}
<View style={ui.card}>
<Text style={ui.title}>Prix</Text>
- <Text style={ui.muted}>{pair}</Text>
+ <Text style={ui.muted}>{displayPair}</Text>
+
<Text style={styles.bigValue}>
- {(summary?.price ?? 0).toFixed(2)} {currency}
+ {(summary?.price ?? 0).toFixed(2)} {quote}
</Text>
+ {/* Info transparente : prix/signal serveur sont basés sur BTC (DB) */}
+ {selectedCrypto !== "BTC" && (
+ <Text style={[ui.muted, { marginTop: 8 }]}>
+ Note : les données serveur (prix/signal) sont disponibles uniquement pour BTC.
+ </Text>
+ )}
+
<TouchableOpacity style={[ui.button, { marginTop: 10 }]} onPress={() => void refreshAll()}>
<Text style={ui.buttonText}>Actualiser</Text>
</TouchableOpacity>
<Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={3}>
{summary.reason}
</Text>
+ <Text style={[ui.muted, { marginTop: 6 }]}>{serverPair}</Text>
</>
) : (
<Text style={ui.muted}>Aucune donnée pour le moment.</Text>
</TouchableOpacity>
</View>
<Text style={[ui.muted, { marginTop: 10 }]} numberOfLines={2}>
- Note : Acheter/Vendre = simulation (registre local). Pas de trading réel.
+ Note : acheter/vendre enregistre une opération locale. (Pas de trading réel)
</Text>
</View>
</Text>
<Text style={ui.muted}>
- Prix : {(summary?.price ?? 0).toFixed(2)} {currency}
+ Prix (serveur) : {(summary?.price ?? 0).toFixed(2)} {quote}
</Text>
<Text style={[ui.muted, { marginTop: 12 }]}>Quantité</Text>
import { SafeAreaView } from "react-native-safe-area-context";
import type { Signal, SignalAction, SignalCriticality } from "../types/Signal";
-
-/**
- * ✅ API-ready (façade)
- */
import { getRecentSignals } from "../services/api/signalApi";
-
import { ui } from "../components/ui/uiStyles";
function getActionColor(action: SignalAction): string {
return new Date(ms).toLocaleString();
}
+function quoteFromPair(pair: string): string {
+ const p = String(pair ?? "");
+ const parts = p.split("/");
+ return parts.length === 2 ? parts[1] : "";
+}
+
export default function HistoryScreen() {
const [items, setItems] = useState<Signal[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const data = await getRecentSignals(20);
if (active) setItems(data);
} catch {
- if (active) setError("Impossible de charger l'historique.");
+ if (active) setError("Impossible de charger l’historique pour le moment.");
} finally {
if (active) setLoading(false);
}
}
- load();
+ void load();
return () => {
active = false;
if (loading) {
return (
<View style={ui.centered}>
- <Text>Chargement de l'historique…</Text>
+ <Text>Chargement de l’historique…</Text>
</View>
);
}
<FlatList
contentContainerStyle={ui.container}
data={items}
- keyExtractor={(it) => it.signalId}
+ keyExtractor={(it, idx) => String((it as any).signalId ?? (it as any).id ?? idx)}
ListEmptyComponent={
<View style={ui.card}>
<Text style={ui.title}>Aucun signal</Text>
renderItem={({ item }) => {
const actionColor = getActionColor(item.action);
const critColor = getCriticalityColor(item.criticality);
+ const quote = quoteFromPair(item.pair);
return (
<View style={ui.card}>
<Text style={ui.muted}>{formatDate(item.timestamp)}</Text>
</View>
- {/* Badges */}
<View
style={{
flexDirection: "row",
<View style={[ui.rowBetween, { marginTop: 6 }]}>
<Text style={ui.value}>Prix au signal</Text>
- <Text style={ui.valueBold}>{item.priceAtSignal.toFixed(2)}</Text>
+ <Text style={ui.valueBold}>
+ {item.priceAtSignal.toFixed(2)} {quote}
+ </Text>
</View>
<Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
import { ui } from "../components/ui/uiStyles";
import { setSeenTutorial } from "../utils/tutorialStorage";
-/**
- * SettingsScreen
- * --------------
- * - Settings par userId (via settingsStorage)
- * - Notifications (local)
- * - Revoir le tutoriel (remet hasSeenTutorial à false)
- *
- * Note "API-ready" :
- * - Aujourd'hui : AsyncStorage (local)
- * - Demain : les mêmes actions pourront appeler une API (sans changer l'UI)
- */
export default function SettingsScreen({
onRequestTutorial,
}: {
);
}
- /**
- * Sauvegarde "safe"
- * - évite les doubles clics
- * - affiche un message simple
- */
const persist = async (next: UserSettings, msg?: string) => {
try {
setSaving(true);
}
};
+ // EUR <-> USDT (aligné DB/serveur)
const toggleCurrency = async () => {
setInfoMessage(null);
- const newCurrency = settings.currency === "EUR" ? "USD" : "EUR";
+ const newCurrency = settings.currency === "EUR" ? "USDT" : "EUR";
await persist({ ...settings, currency: newCurrency }, `Devise : ${newCurrency} ✅`);
};
const toggleNotifications = async () => {
setInfoMessage(null);
- // OFF -> ON : on demande la permission
if (!settings.notificationsEnabled) {
const granted = await requestNotificationPermission();
if (!granted) {
return;
}
- await persist(
- { ...settings, notificationsEnabled: true },
- "Notifications activées ✅"
- );
+ await persist({ ...settings, notificationsEnabled: true }, "Notifications activées ✅");
return;
}
- // ON -> OFF
- await persist(
- { ...settings, notificationsEnabled: false },
- "Notifications désactivées ✅"
- );
+ await persist({ ...settings, notificationsEnabled: false }, "Notifications désactivées ✅");
};
- /**
- * Bouton "Sauvegarder" : garde-fou
- * (utile si on ajoute d'autres réglages plus tard)
- */
const handleSave = async () => {
setInfoMessage(null);
await persist(settings, "Paramètres sauvegardés ✅");
<View style={ui.container}>
<Text style={styles.screenTitle}>Paramètres</Text>
- {/* Carte : Devise */}
<View style={ui.card}>
<Text style={ui.title}>Devise</Text>
<Text style={ui.value}>Actuelle : {settings.currency}</Text>
</TouchableOpacity>
</View>
- {/* Carte : Refresh */}
<View style={ui.card}>
<Text style={ui.title}>Rafraîchissement</Text>
<Text style={ui.value}>Mode : {settings.refreshMode}</Text>
</TouchableOpacity>
</View>
- {/* Carte : Notifications */}
<View style={ui.card}>
<Text style={ui.title}>Notifications</Text>
- <Text style={ui.value}>Statut : {settings.notificationsEnabled ? "ON" : "OFF"}</Text>
+ <Text style={ui.value}>
+ Statut : {settings.notificationsEnabled ? "ON" : "OFF"}
+ </Text>
<TouchableOpacity
style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
{!!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}>
</TouchableOpacity>
</View>
- {/* Bouton Save (garde-fou) */}
<TouchableOpacity
style={[ui.button, styles.fullButton, styles.saveButton, saving && styles.btnDisabled]}
onPress={handleSave}
}
const styles = StyleSheet.create({
- safeArea: {
- flex: 1,
- backgroundColor: ui.screen.backgroundColor,
- },
- screenTitle: {
- fontSize: 22,
- fontWeight: "900",
- marginBottom: 12,
- color: "#0f172a",
- },
- fullButton: {
- flexGrow: 0,
- flexBasis: "auto",
- width: "100%",
- marginTop: 10,
- },
- saveButton: {
- marginTop: 4,
- marginBottom: 10,
- },
+ safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
+ screenTitle: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
+ fullButton: { flexGrow: 0, flexBasis: "auto", width: "100%", marginTop: 10 },
+ saveButton: { marginTop: 4, marginBottom: 10 },
secondaryButton: {
marginTop: 10,
paddingVertical: 12,
alignItems: "center",
backgroundColor: "#fff",
},
- secondaryButtonText: {
- fontWeight: "900",
- color: "#0f172a",
- opacity: 0.85,
- },
- btnDisabled: {
- opacity: 0.7,
- },
+ secondaryButtonText: { fontWeight: "900", color: "#0f172a", opacity: 0.85 },
+ btnDisabled: { opacity: 0.7 },
});
\ No newline at end of file
}
}
- init();
+ void init();
return () => {
active = false;
};
setBusy(true);
setInfo(null);
- // Pair alignée dashboard : BTC + devise settings
- const currency = settings.currency === "USD" ? "USD" : "EUR";
- const pair = `BTC/${currency}`;
+ // Pair alignée DB/serveur : EUR -> BTC/EUR ; USDT -> BTC/USDT
+ const quote = settings.currency === "USDT" ? "USDT" : "EUR";
+ const pair = `BTC/${quote}`;
- // 1) Serveur d’abord (align Stéphane)
+ // 1) Serveur d’abord (source de vérité)
await selectStrategy({ pair, mode: key, params: {} });
// 2) Puis local (affichage immédiat)
await saveSettings(rollback);
setSettings(rollback);
- setInfo(`Erreur serveur stratégie : ${e?.message ?? "inconnue"}`);
+ setInfo(`Impossible de sélectionner la stratégie. (${e?.message ?? "Erreur inconnue"})`);
} finally {
setBusy(false);
}
ListHeaderComponent={
<View style={ui.card}>
<Text style={ui.title}>Stratégie</Text>
- <Text style={ui.muted}>Choisis une stratégie. Elle est enregistrée sur le serveur.</Text>
+ <Text style={ui.muted}>
+ Choisissez une stratégie. Elle est enregistrée sur le serveur.
+ </Text>
<Text style={[ui.muted, { marginTop: 8 }]}>
Actuelle : <Text style={styles.boldInline}>{settings.selectedStrategyKey}</Text>
isSelected(item.key) && styles.btnSelected,
busy && styles.btnDisabled,
]}
- onPress={() => handleSelect(item.key)}
+ onPress={() => void handleSelect(item.key)}
disabled={busy}
>
<Text style={ui.buttonText}>
- {isSelected(item.key) ? "Sélectionnée ✅" : busy ? "Envoi..." : "Sélectionner"}
+ {isSelected(item.key) ? "Sélectionnée ✅" : busy ? "Envoi…" : "Sélectionner"}
</Text>
</TouchableOpacity>
</View>
() => [
{
key: "intro",
- title: "La crypto, c’est quoi ?",
+ title: "Bienvenue sur Wallette",
text:
- "Une cryptomonnaie est une monnaie numérique. Son prix varie selon l’offre et la demande, et peut bouger très vite.",
+ "Wallette vous aide à suivre le marché via des signaux et des alertes.\n\nObjectif : comprendre et observer. Pas “trader” pour de vrai.",
icon: "sparkles-outline",
},
{
- key: "types",
- title: "Types de cryptos",
+ key: "crypto_basics",
+ title: "La crypto, c’est quoi ?",
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",
+ "Une cryptomonnaie est une monnaie numérique.\nSon prix varie selon l’offre et la demande, parfois très vite.",
+ icon: "planet-outline",
},
{
- key: "role",
- title: "Rôle de Wall-e-tte",
+ key: "pairs",
+ title: "Paires BTC/EUR et BTC/USDT",
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",
+ "L’application utilise des paires de prix :\n• BTC/EUR\n• BTC/USDT\n\nUSDT est un “stablecoin” proche de 1 dollar.",
+ icon: "swap-horizontal-outline",
+ },
+ {
+ key: "signal",
+ title: "Signal du marché",
+ text:
+ "Le signal résume la décision d’une stratégie :\n• BUY (acheter)\n• SELL (vendre)\n• HOLD (attendre)\n• STOP_LOSS (se protéger)\n\nLe signal inclut une confiance (%) et une raison.",
+ icon: "pulse-outline",
+ },
+ {
+ key: "alerts",
+ title: "Alertes",
+ text:
+ "Les alertes sont envoyées en temps réel.\nElles peuvent être CRITICAL / WARNING / INFO.\n\nAstuce : commencez par les alertes CRITICAL.",
+ icon: "alert-circle-outline",
},
{
- key: "app",
- title: "Comment utiliser l’app",
+ key: "wallet",
+ title: "Portefeuille",
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",
+ "L’écran Portefeuille affiche votre solde BTC et l’historique des transactions.\n\nLes données proviennent du serveur du projet.",
+ icon: "wallet-outline",
+ },
+ {
+ key: "strategy",
+ title: "Stratégie",
+ text:
+ "Une stratégie analyse le marché et produit des signaux.\n\nVous pouvez en sélectionner une dans l’écran “Stratégie”.",
+ icon: "analytics-outline",
},
{
key: "settings",
- title: "Paramètres importants",
+ title: "Paramètres",
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.",
+ "Vous pouvez régler :\n• Devise : EUR ou USDT\n• Rafraîchissement : manuel ou auto\n• Notifications : activer/désactiver\n\nLe tutoriel reste accessible via Paramètres.",
icon: "settings-outline",
},
+ {
+ key: "warning",
+ title: "Avertissement",
+ text:
+ "Wallette est un outil éducatif.\nCe n’est pas un conseil financier.\n\nNe mettez jamais d’argent réel sur base d’un projet étudiant.",
+ icon: "shield-checkmark-outline",
+ },
],
[]
);
</TouchableOpacity>
</View>
- {/* CONTENT (on laisse de la place en bas pour la barre fixe) */}
+ {/* CONTENT */}
<View style={[styles.content, ui.container]}>
<View style={styles.iconWrap}>
<Ionicons name={slide.icon} size={38} color="#0f172a" style={{ opacity: 0.85 }} />
style={[styles.primaryBtn]}
onPress={isLast ? finish : next}
>
- <Text style={styles.primaryText}>
- {isLast ? "Terminer" : "Suivant"}
- </Text>
+ <Text style={styles.primaryText}>{isLast ? "Terminer" : "Suivant"}</Text>
</TouchableOpacity>
</View>
</View>
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
+ paddingBottom: 140,
},
iconWrap: {
lineHeight: 20,
},
- // ✅ barre du bas fixée
bottomFixed: {
position: "absolute",
left: 0,
paddingVertical: 12,
alignItems: "center",
justifyContent: "center",
- backgroundColor: "#16a34a", // ✅ un vert "crypto friendly"
+ backgroundColor: "#16a34a",
},
primaryText: { color: "#fff", fontWeight: "900" },
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
- TextInput,
- Alert as RNAlert,
- FlatList,
-} from "react-native";
+import { View, Text, StyleSheet, FlatList } from "react-native";
import { useCallback, useMemo, useState } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { useFocusEffect } from "@react-navigation/native";
import { ui } from "../components/ui/uiStyles";
-import type { PortfolioAsset, PortfolioState } from "../models/Portfolio";
import { loadSettings } from "../utils/settingsStorage";
import type { UserSettings } from "../models/UserSettings";
+import type { PortfolioState } from "../models/Portfolio";
-/**
- * ✅ API-only (wallet)
- */
-import { getPortfolio, upsertPortfolio, resetPortfolio } from "../services/api/walletApi";
-
-/**
- * ✅ API-only (price)
- */
+import { getPortfolio, getPrimaryWalletId, getWalletEvents, type WalletEvent } from "../services/api/walletApi";
import { getCurrentPrice } from "../services/api/priceApi";
/**
- * WalletScreen (Step 3/4)
- * -----------------------
- * Multi-cryptos
- * - quantité par asset
- * - valeur globale
+ * WalletScreen (serveur/DB only)
+ * -----------------------------
+ * Affiche uniquement ce que le serveur fournit :
+ * - Wallet BTC (quantité)
+ * - Valeur estimée (prix BTC/EUR ou BTC/USDT)
+ * - Historique events du wallet (/wallets/:id/events)
*
- * Prix : API-only (pas de mock)
+ * Aucun ajout/modif local ici (aligné consigne + DB).
*/
+
+function formatNumber(n: number, decimals = 6) {
+ return Number.isFinite(n) ? n.toFixed(decimals) : "0.000000";
+}
+
+function quoteFromSettings(s: UserSettings): "EUR" | "USDT" {
+ return s.currency === "USDT" ? "USDT" : "EUR";
+}
+
+function asNumber(x: any): number {
+ const n = typeof x === "number" ? x : Number(x);
+ return Number.isFinite(n) ? n : 0;
+}
+
+function formatDate(ms?: number) {
+ const t = typeof ms === "number" && Number.isFinite(ms) ? ms : Date.now();
+ return new Date(t).toLocaleString();
+}
+
export default function WalletScreen() {
const [portfolio, setPortfolio] = useState<PortfolioState | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
- // Prix chargés depuis l'API (par symbole)
- const [prices, setPrices] = useState<Record<string, number>>({});
- const [pricesError, setPricesError] = useState<string | null>(null);
+ const [walletId, setWalletId] = useState<string | null>(null);
- // Ajout asset
- const [symbolInput, setSymbolInput] = useState<string>("BTC");
- const [qtyInput, setQtyInput] = useState<string>("0");
+ const [btcPrice, setBtcPrice] = useState<number | null>(null);
+ const [priceError, setPriceError] = useState<string | null>(null);
- const [info, setInfo] = useState<string | null>(null);
+ const [events, setEvents] = useState<WalletEvent[]>([]);
+ const [eventsError, setEventsError] = useState<string | null>(null);
useFocusEffect(
useCallback(() => {
let active = true;
async function init() {
- setInfo(null);
- setPricesError(null);
+ setPriceError(null);
+ setEventsError(null);
- const [p, s] = await Promise.all([getPortfolio(), loadSettings()]);
+ const s = await loadSettings();
if (!active) return;
-
- setPortfolio(p);
setSettings(s);
- // Charger les prix pour les assets actuels
+ // WalletId + Portfolio serveur
try {
- const entries = await Promise.all(
- p.assets.map(async (a) => {
- const pair = `${a.symbol}/${s.currency}`;
- const cur = await getCurrentPrice(pair);
- return [a.symbol, cur.price] as const;
- })
- );
-
+ const [id, p] = await Promise.all([getPrimaryWalletId(), getPortfolio()]);
if (!active) return;
- const map: Record<string, number> = {};
- for (const [sym, pr] of entries) map[sym] = pr;
- setPrices(map);
+ setWalletId(id);
+ setPortfolio(p);
+
+ // Prix BTC (pair DB only)
+ const quote = quoteFromSettings(s);
+ const pair = `BTC/${quote}`;
+ try {
+ const cur = await getCurrentPrice(pair);
+ if (!active) return;
+ setBtcPrice(cur.price);
+ } catch (e: any) {
+ if (!active) return;
+ setBtcPrice(null);
+ setPriceError(e?.message ?? "Impossible de charger le prix BTC.");
+ }
+
+ // Events
+ if (id) {
+ try {
+ const evts = await getWalletEvents(50);
+ if (!active) return;
+ setEvents(evts);
+ } catch (e: any) {
+ if (!active) return;
+ setEvents([]);
+ setEventsError(e?.message ?? "Impossible de charger l’historique des transactions.");
+ }
+ } else {
+ setEvents([]);
+ }
} catch (e: any) {
if (!active) return;
- setPricesError(e?.message ?? "Impossible de charger les prix (API).");
+ setPortfolio({ assets: [], updatedAtMs: Date.now() });
+ setWalletId(null);
+ setEvents([]);
+ setEventsError(e?.message ?? "Impossible de charger le portefeuille.");
}
}
}, [])
);
- const lastUpdatedLabel = useMemo(() => {
- if (!portfolio) return "—";
- return new Date(portfolio.updatedAtMs).toLocaleString();
+ const btcQty = useMemo(() => {
+ if (!portfolio) return 0;
+ const a = portfolio.assets.find((x) => x.symbol === "BTC");
+ return a?.quantity ?? 0;
}, [portfolio]);
- const parsedQty = useMemo(() => {
- const normalized = qtyInput.replace(",", ".").trim();
- const val = Number(normalized);
- if (!Number.isFinite(val)) return null;
- if (val < 0) return null;
- return val;
- }, [qtyInput]);
-
- const normalizedSymbol = useMemo(() => symbolInput.toUpperCase().trim(), [symbolInput]);
+ const quote = useMemo(() => (settings ? quoteFromSettings(settings) : "EUR"), [settings]);
const totalValue = useMemo(() => {
- if (!portfolio) return 0;
-
- return portfolio.assets.reduce((sum, a) => {
- const price = prices[a.symbol];
- if (typeof price !== "number") return sum; // prix absent => on ignore
- return sum + a.quantity * price;
- }, 0);
- }, [portfolio, prices]);
-
- const handleAddOrUpdate = async () => {
- if (!portfolio || !settings) return;
-
- setInfo(null);
-
- if (!normalizedSymbol || normalizedSymbol.length < 2) {
- setInfo("Symbole invalide (ex: BTC).");
- return;
- }
-
- if (parsedQty === null) {
- setInfo("Quantité invalide. Exemple : 0.25");
- return;
- }
-
- // ✅ API-only : on vérifie que la paire est récupérable
- try {
- const pair = `${normalizedSymbol}/${settings.currency}`;
- const cur = await getCurrentPrice(pair);
-
- // on mémorise le prix reçu (utile pour affichage)
- setPrices((prev) => ({ ...prev, [normalizedSymbol]: cur.price }));
- setPricesError(null);
- } catch (e: any) {
- setInfo(`Prix indisponible (API) pour ${normalizedSymbol}/${settings.currency}.`);
- setPricesError(e?.message ?? "Erreur API prix.");
- return;
- }
-
- const existingIndex = portfolio.assets.findIndex((a) => a.symbol === normalizedSymbol);
-
- let updatedAssets: PortfolioAsset[];
- if (existingIndex >= 0) {
- updatedAssets = portfolio.assets.map((a) =>
- a.symbol === normalizedSymbol ? { ...a, quantity: parsedQty } : a
- );
- } else {
- updatedAssets = [...portfolio.assets, { symbol: normalizedSymbol, quantity: parsedQty }];
- }
-
- const updated: PortfolioState = {
- assets: updatedAssets,
- updatedAtMs: Date.now(),
- };
-
- await upsertPortfolio(updated);
- setPortfolio(updated);
-
- setInfo(existingIndex >= 0 ? "Asset mis à jour ✅" : "Asset ajouté ✅");
- };
-
- const handleDelete = (symbol: string) => {
- if (!portfolio) return;
-
- RNAlert.alert(
- `Supprimer ${symbol} ?`,
- "Cette action retire l’asset du portefeuille.",
- [
- { text: "Annuler", style: "cancel" },
- {
- text: "Supprimer",
- style: "destructive",
- onPress: async () => {
- const updated: PortfolioState = {
- assets: portfolio.assets.filter((a) => a.symbol !== symbol),
- updatedAtMs: Date.now(),
- };
-
- await upsertPortfolio(updated);
- setPortfolio(updated);
-
- // nettoyage prix local
- setPrices((prev) => {
- const next = { ...prev };
- delete next[symbol];
- return next;
- });
-
- setInfo(`${symbol} supprimé ✅`);
- },
- },
- ]
- );
- };
-
- const handleClear = () => {
- RNAlert.alert(
- "Réinitialiser le portefeuille ?",
- "Cela supprime tous les assets du portefeuille.",
- [
- { text: "Annuler", style: "cancel" },
- {
- text: "Réinitialiser",
- style: "destructive",
- onPress: async () => {
- const fresh = await resetPortfolio();
- setPortfolio(fresh);
- setPrices({});
- setInfo("Portefeuille réinitialisé ✅");
- },
- },
- ]
- );
- };
+ if (btcPrice === null) return null;
+ return btcQty * btcPrice;
+ }, [btcQty, btcPrice]);
if (!portfolio || !settings) {
return (
<SafeAreaView style={styles.safeArea}>
<FlatList
contentContainerStyle={ui.container}
- data={portfolio.assets}
- keyExtractor={(it) => it.symbol}
+ data={events}
+ keyExtractor={(it, idx) => String(it.event_id ?? idx)}
ListHeaderComponent={
<View>
<Text style={styles.screenTitle}>Portefeuille</Text>
- {/* Résumé global */}
+ {/* Résumé wallet */}
<View style={ui.card}>
<Text style={ui.title}>Résumé</Text>
- <View style={ui.rowBetween}>
- <Text style={ui.value}>Valeur globale :</Text>
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ WalletId : <Text style={styles.boldInline}>{walletId ?? "—"}</Text>
+ </Text>
+
+ <View style={[ui.rowBetween, { marginTop: 10 }]}>
+ <Text style={ui.value}>Solde BTC</Text>
+ <Text style={ui.valueBold}>{formatNumber(btcQty, 6)} BTC</Text>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Prix BTC</Text>
<Text style={ui.valueBold}>
- {totalValue.toFixed(2)} {settings.currency}
+ {btcPrice !== null ? `${btcPrice.toFixed(2)} ${quote}` : "—"}
</Text>
</View>
- <Text style={[ui.muted, { marginTop: 6 }]}>
- Dernière mise à jour : <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
- </Text>
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Valeur estimée</Text>
+ <Text style={ui.valueBold}>
+ {totalValue !== null ? `${totalValue.toFixed(2)} ${quote}` : "—"}
+ </Text>
+ </View>
- {!!pricesError && (
- <Text style={[ui.muted, { marginTop: 6 }]}>
- ⚠️ {pricesError}
+ {!!priceError && (
+ <Text style={[ui.muted, { marginTop: 10 }]}>
+ {priceError}
</Text>
)}
-
- <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
- <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
- </TouchableOpacity>
</View>
- {/* Ajouter / modifier */}
+ {/* Historique */}
<View style={ui.card}>
- <Text style={ui.title}>Ajouter / Modifier un asset</Text>
-
- <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL)</Text>
- <TextInput
- value={symbolInput}
- onChangeText={setSymbolInput}
- autoCapitalize="characters"
- placeholder="BTC"
- style={styles.input}
- />
-
- <Text style={[ui.muted, { marginTop: 10 }]}>Quantité</Text>
- <TextInput
- value={qtyInput}
- onChangeText={setQtyInput}
- keyboardType="decimal-pad"
- placeholder="0.25"
- style={styles.input}
- />
-
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleAddOrUpdate}>
- <Text style={ui.buttonText}>Enregistrer</Text>
- </TouchableOpacity>
-
- {!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
+ <Text style={ui.title}>Historique des transactions</Text>
+ <Text style={ui.muted}>
+ Données issues du serveur (events du wallet).
+ </Text>
+
+ {!!eventsError && (
+ <Text style={[ui.muted, { marginTop: 10 }]}>
+ {eventsError}
+ </Text>
+ )}
</View>
- <Text style={[ui.muted, { marginBottom: 10 }]}>Liste des assets :</Text>
+ <Text style={[ui.muted, { marginBottom: 10 }]}>Derniers événements :</Text>
</View>
}
ListEmptyComponent={
<View style={ui.card}>
- <Text style={ui.title}>Aucun asset</Text>
- <Text style={ui.muted}>Ajoutez BTC/ETH/SOL… pour commencer.</Text>
+ <Text style={ui.title}>Aucune transaction</Text>
+ <Text style={ui.muted}>Aucun événement enregistré pour l’instant.</Text>
</View>
}
renderItem={({ item }) => {
- const price = prices[item.symbol];
- const value = typeof price === "number" ? item.quantity * price : null;
+ const type = String(item.event_type ?? "—").toUpperCase();
+ const qty = asNumber(item.quantity_base);
+ const price = asNumber(item.price_quote);
+ const q = String(item.quote_symbol ?? quote);
return (
<View style={ui.card}>
<View style={ui.rowBetween}>
- <Text style={ui.valueBold}>{item.symbol}</Text>
- <TouchableOpacity onPress={() => handleDelete(item.symbol)}>
- <Text style={styles.deleteText}>Supprimer</Text>
- </TouchableOpacity>
+ <Text style={ui.valueBold}>{type}</Text>
+ <Text style={ui.muted}>{formatDate(item.executed_at_ms)}</Text>
</View>
<View style={[ui.rowBetween, { marginTop: 8 }]}>
<Text style={ui.value}>Quantité</Text>
- <Text style={ui.valueBold}>{item.quantity.toFixed(6)}</Text>
+ <Text style={ui.valueBold}>{formatNumber(qty, 6)} BTC</Text>
</View>
<View style={[ui.rowBetween, { marginTop: 6 }]}>
<Text style={ui.value}>Prix</Text>
<Text style={ui.valueBold}>
- {typeof price === "number" ? `${price.toFixed(2)} ${settings.currency}` : "—"}
- </Text>
- </View>
-
- <View style={[ui.rowBetween, { marginTop: 6 }]}>
- <Text style={ui.value}>Valeur</Text>
- <Text style={ui.valueBold}>
- {value !== null ? `${value.toFixed(2)} ${settings.currency}` : "—"}
+ {price > 0 ? `${price.toFixed(2)} ${q}` : "—"}
</Text>
</View>
</View>
}
const styles = StyleSheet.create({
- safeArea: {
- flex: 1,
- backgroundColor: ui.screen.backgroundColor,
- },
- screenTitle: {
- fontSize: 22,
- fontWeight: "900",
- marginBottom: 12,
- color: "#0f172a",
- },
- boldInline: {
- fontWeight: "900",
- color: "#0f172a",
- },
-
- 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,
- },
-
- secondaryButton: {
- marginTop: 10,
- paddingVertical: 10,
- borderRadius: 10,
- borderWidth: 1,
- borderColor: "#e5e7eb",
- alignItems: "center",
- backgroundColor: "#fff",
- },
- secondaryButtonText: {
- fontWeight: "900",
- color: "#dc2626",
- },
-
- deleteText: {
- fontWeight: "900",
- color: "#dc2626",
- },
+ safeArea: { flex: 1, backgroundColor: ui.screen.backgroundColor },
+ screenTitle: { fontSize: 22, fontWeight: "900", marginBottom: 12, color: "#0f172a" },
+ boldInline: { fontWeight: "900", color: "#0f172a" },
});
\ No newline at end of file
/**
* authApi.ts
* ----------
- * Façade d'authentification.
+ * Décision projet : PAS d'auth serveur.
*
- * Aujourd'hui (Step local) :
- * - login/register/logout en local (AsyncStorage)
+ * 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.
*
- * Demain (API ready) :
- * - tu pourras remplacer l'implémentation par :
- * POST /api/auth/login
- * POST /api/auth/register
- * GET /api/auth/me
- * POST /api/auth/logout
- *
- * L'UI ne change pas : elle appelle toujours authApi.
+ * 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 };
const res = await createUser(params);
if (!res.ok) return { ok: false, message: res.message };
+ // ✅ userId aligné serveur
const session: Session = {
- userId: res.user.userId,
+ userId: SERVER_USER_ID,
email: res.user.email,
createdAtMs: Date.now(),
};
const res = await verifyLogin(params);
if (!res.ok) return { ok: false, message: res.message };
+ // ✅ userId aligné serveur
const session: Session = {
- userId: res.user.userId,
+ userId: SERVER_USER_ID,
email: res.user.email,
createdAtMs: Date.now(),
};
}
/**
- * Retourne l'utilisateur courant (si session active).
- * Utile pour afficher profil / sécuriser certaines actions.
+ * 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?.userId) return null;
+ if (!session) return null;
- return await findUserById(session.userId);
+ // 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);
}
-/**
- * Juste la session (pratique pour guards).
- */
export async function getSession(): Promise<Session | null> {
return await loadSession();
}
\ No newline at end of file
return "INFO";
}
-/**
- * dashboardApi (contrat Stéphane - STRICT)
- * ---------------------------------------
- * - GET /api/price/current?pair=BTC/EUR
- * - GET /api/signal/current?userId=...&pair=BTC/EUR
- */
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.");
const settings = await loadSettings();
- const currency: "EUR" | "USD" = settings.currency === "USD" ? "USD" : "EUR";
+ const quote = settings.currency === "USDT" ? "USDT" : "EUR";
+ const pair = `BTC/${quote}`;
- // Dashboard strict : BTC seulement (align web). Adaptable plus tard.
- const pair = `BTC/${currency}`;
-
- // 1) Prix (service prix)
const price = await getCurrentPrice(pair);
- // 2) Signal (service stratégies)
const sig = await apiGet<any>(
`/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`
);
- const decision = safeDecision(sig?.action ?? sig?.decision);
- const alertLevel = safeLevel(sig?.alertLevel ?? sig?.level ?? sig?.criticality);
- const confidence = typeof sig?.confidence === "number" ? sig.confidence : 0;
- const reason = String(sig?.reason ?? sig?.message ?? "—");
-
- const timestamp =
- typeof sig?.timestamp === "number"
- ? sig.timestamp
- : typeof sig?.timestamp_ms === "number"
- ? sig.timestamp_ms
- : price.timestampMs;
+ const data = sig ?? null;
return {
pair,
price: price.price,
strategy: settings.selectedStrategyKey,
- decision,
- confidence,
- reason,
- alertLevel,
- timestamp,
+ decision: safeDecision(data?.action ?? data?.decision),
+ confidence: typeof data?.confidence === "number" ? data.confidence : 0,
+ reason: String(data?.reason ?? data?.message ?? "—"),
+ alertLevel: safeLevel(data?.alertLevel ?? data?.level ?? data?.criticality),
+ timestamp:
+ typeof data?.timestamp_ms === "number"
+ ? data.timestamp_ms
+ : typeof data?.timestamp === "number"
+ ? data.timestamp
+ : price.timestampMs,
};
}
\ No newline at end of file
-import type { Signal } from "../../types/Signal";
import { apiGet } from "./http";
import { loadSession } from "../../utils/sessionStorage";
+import type { Signal, SignalAction, SignalCriticality, SignalStatus } from "../../types/Signal";
/**
- * signalApi (API-only)
- * --------------------
- * NOTE: endpoint à confirmer/aligner avec le chef.
- * Proposition :
- * - GET /api/signal/recent?userId=...&limit=20
+ * signalApi (align serveur + gateway, robust)
+ * ------------------------------------------
+ * - Charge l'historique des signaux depuis strategy-service.
+ * - Normalise les champs (snake_case -> camelCase) pour coller à types/Signal.ts
+ * - Pas de mock.
*/
+
+function asArray(x: any): any[] {
+ if (Array.isArray(x)) return x;
+ if (Array.isArray(x?.signals)) return x.signals;
+ if (Array.isArray(x?.events)) return x.events;
+ if (Array.isArray(x?.items)) return x.items;
+ return [];
+}
+
+function safeAction(a: any): SignalAction {
+ const v = String(a ?? "HOLD").toUpperCase();
+ if (v === "BUY" || v === "SELL" || v === "STOP_LOSS") return v as SignalAction;
+ return "HOLD";
+}
+
+function safeCriticality(c: any): SignalCriticality {
+ const v = String(c ?? "INFO").toUpperCase();
+ if (v === "CRITICAL" || v === "WARNING") return v as SignalCriticality;
+ return "INFO";
+}
+
+function safeStatus(s: any): SignalStatus {
+ const v = String(s ?? "ACTIVE").toUpperCase();
+ if (v === "SUPERSEDED" || v === "EXECUTED" || v === "INVALID") return v as SignalStatus;
+ return "ACTIVE";
+}
+
+function toNumber(x: any, fallback = 0): number {
+ const n = typeof x === "number" ? x : Number(x);
+ return Number.isFinite(n) ? n : fallback;
+}
+
+function normalizeSignal(raw: any, idx: number): Signal {
+ // id
+ const id =
+ String(
+ raw?.signalId ??
+ raw?.signal_id ??
+ raw?.id ??
+ raw?.event_id ??
+ `sig_${Date.now()}_${idx}`
+ );
+
+ const pair = String(raw?.pair ?? raw?.pair_code ?? "BTC/EUR");
+
+ const timestamp = toNumber(raw?.timestamp ?? raw?.timestamp_ms ?? raw?.created_at_ms ?? Date.now(), Date.now());
+
+ const action = safeAction(raw?.action ?? raw?.decision);
+ const criticality = safeCriticality(raw?.criticality ?? raw?.alertLevel ?? raw?.level);
+ const status = safeStatus(raw?.status);
+
+ const confidence = Math.max(0, Math.min(1, toNumber(raw?.confidence ?? 0, 0)));
+
+ const reason = String(raw?.reason ?? raw?.message ?? "—");
+
+ const priceAtSignal = toNumber(
+ raw?.priceAtSignal ?? raw?.price_at_signal ?? raw?.price ?? raw?.current_price ?? 0,
+ 0
+ );
+
+ return {
+ signalId: id,
+ pair,
+ timestamp,
+ action,
+ criticality,
+ status,
+ confidence,
+ reason,
+ priceAtSignal,
+ };
+}
+
export async function getRecentSignals(limit = 20): Promise<Signal[]> {
const session = await loadSession();
const userId = session?.userId;
- if (!userId) throw new Error("Session absente : impossible de charger l'historique.");
+ if (!userId) throw new Error("Session absente.");
- const data = await apiGet<{ signals: Signal[] }>(
- `/signal/recent?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
- );
+ const candidates = [
+ `/signal/recent?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`,
+ `/signal/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`,
+ `/signal/history?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`,
+ ];
+
+ let lastErr: unknown = null;
+
+ for (const path of candidates) {
+ try {
+ const data = await apiGet<any>(path);
+ const arr = asArray(data);
+
+ // si route existe mais vide -> ok
+ if (arr.length === 0 && (data || data === null)) return [];
+
+ return arr.map((x, i) => normalizeSignal(x, i));
+ } catch (e) {
+ lastErr = e;
+ }
+ }
- return data.signals ?? [];
+ throw lastErr ?? new Error("Impossible de charger les signaux.");
}
\ No newline at end of file
*/
function pairToPairId(pair: string): number {
- // Convention du projet : (MarketDataRepo mentionne pairId=1 pour BTC)
const p = pair.trim().toUpperCase();
- if (p.startsWith("BTC/")) return 1;
- if (p.startsWith("ETH/")) return 2;
- if (p.startsWith("LTC/")) return 3;
- throw new Error(`Pair non supportée (pas de pairId) : ${pair}`);
+
+ if (p === "BTC/EUR") return 1;
+ if (p === "BTC/USDT") return 2;
+
+ throw new Error(`Pair non supportée par la DB (pairs) : ${pair}`);
}
export async function selectStrategy(params: {
-import { apiGet } from "./http";
+import { apiGet, apiPost } from "./http";
import { loadSession } from "../../utils/sessionStorage";
import type { PortfolioState } from "../../models/Portfolio";
/**
- * walletApi (contrat Stéphane - STRICT)
- * ------------------------------------
+ * walletApi (aligné wallet-service)
+ * --------------------------------
* - GET /api/wallets?userId=...
+ * -> { wallets:[...], count }
* - GET /api/wallets/:id
+ * -> { wallet:{...} }
* - GET /api/wallets/:id/events
+ * -> { events:[...], count }
+ * - POST /api/wallets/:id/events
+ * -> body.event_type obligatoire
*/
-type WalletListResponse = any;
+type WalletListResponse = { wallets?: any[]; count?: number } | any;
function pickWalletId(list: any): string | null {
const arr =
const first = arr[0];
return (
+ (typeof first?.wallet_id === "string" && first.wallet_id) ||
(typeof first?.walletId === "string" && first.walletId) ||
(typeof first?.id === "string" && first.id) ||
(typeof first?._id === "string" && first._id) ||
);
}
-function extractAssets(details: any): { symbol: string; quantity: number }[] {
- const assetsArr =
- (Array.isArray(details?.portfolio?.assets) && details.portfolio.assets) ||
- (Array.isArray(details?.assets) && details.assets) ||
- null;
-
- if (assetsArr) {
- return assetsArr
- .filter((a: any) => a && typeof a.symbol === "string" && typeof a.quantity === "number")
- .map((a: any) => ({ symbol: String(a.symbol).toUpperCase(), quantity: Number(a.quantity) }))
- .filter((a: any) => Number.isFinite(a.quantity) && a.quantity > 0)
- .sort((a: any, b: any) => a.symbol.localeCompare(b.symbol));
- }
-
- const balances = details?.balances;
- if (balances && typeof balances === "object") {
- return Object.entries(balances)
- .filter(([, qty]) => typeof qty === "number" && Number.isFinite(qty) && qty > 0)
- .map(([symbol, qty]) => ({ symbol: String(symbol).toUpperCase(), quantity: Number(qty) }))
- .sort((a, b) => a.symbol.localeCompare(b.symbol));
- }
-
- return [];
-}
-
export async function getPrimaryWalletId(): Promise<string | null> {
const session = await loadSession();
const userId = session?.userId;
if (!userId) throw new Error("Session absente : impossible de charger les wallets.");
- const list = await apiGet<WalletListResponse>(`/wallets?userId=${encodeURIComponent(userId)}`);
- return pickWalletId(list);
+ const data = await apiGet<WalletListResponse>(`/wallets?userId=${encodeURIComponent(userId)}`);
+ return pickWalletId(data);
}
export async function getPortfolio(): Promise<PortfolioState> {
const walletId = await getPrimaryWalletId();
if (!walletId) return { assets: [], updatedAtMs: Date.now() };
- const details = await apiGet<any>(`/wallets/${encodeURIComponent(walletId)}`);
- const assets = extractAssets(details);
+ const data = await apiGet<any>(`/wallets/${encodeURIComponent(walletId)}`);
- return { assets, updatedAtMs: Date.now() };
+ // wallet-service: { wallet: {...} }
+ const w = data?.wallet ?? data;
+
+ const symbol = String(w?.asset_symbol ?? w?.assetSymbol ?? "BTC").toUpperCase();
+ const qtyRaw = w?.quantity ?? "0";
+ const qty = Number(qtyRaw);
+
+ if (!Number.isFinite(qty) || qty <= 0) {
+ return { assets: [], updatedAtMs: Date.now() };
+ }
+
+ return {
+ assets: [{ symbol, quantity: qty }],
+ updatedAtMs: Date.now(),
+ };
}
export type WalletEvent = {
- id?: string;
- type?: string; // BUY/SELL
- symbol?: string; // BTC
- quantity?: number;
- price?: number;
- timestamp?: number;
+ event_id?: string;
+ event_type?: string; // BUY/SELL/...
+ quantity_base?: string; // "0.0100000000"
+ price_quote?: string; // "42150.2300000000"
+ quote_symbol?: string; // "EUR" ou "USDT"
+ executed_at_ms?: number;
};
export async function getWalletEvents(limit = 50): Promise<WalletEvent[]> {
`/wallets/${encodeURIComponent(walletId)}/events?limit=${encodeURIComponent(String(limit))}`
);
- if (Array.isArray(data)) return data as WalletEvent[];
if (Array.isArray(data?.events)) return data.events as WalletEvent[];
if (Array.isArray(data?.items)) return data.items as WalletEvent[];
+ if (Array.isArray(data)) return data as WalletEvent[];
return [];
+}
+
+export async function addWalletEvent(params: {
+ walletId: string;
+ event_type: "BUY" | "SELL";
+ quantity_base: string; // ex: "0.0100000000"
+ price_quote?: string; // requis pour BUY
+ quote_symbol?: "EUR" | "USDT"; // requis pour BUY
+}): Promise<void> {
+ await apiPost(`/wallets/${encodeURIComponent(params.walletId)}/events`, params);
}
\ No newline at end of file
-export type TradeDecision =
- | "BUY"
- | "SELL"
- | "HOLD"
- | "STOP_LOSS";
+export type TradeDecision = "BUY" | "SELL" | "HOLD" | "STOP_LOSS";
-export type AlertLevel =
- | "CRITICAL"
- | "WARNING"
- | "INFO";
+export type AlertLevel = "CRITICAL" | "WARNING" | "INFO";
export interface DashboardSummary {
- pair: string;
+ pair: string; // ex: BTC/EUR ou BTC/USDT
price: number;
strategy: string;
decision: TradeDecision;
reason: string;
alertLevel: AlertLevel;
timestamp: number;
-}
-
+}
\ No newline at end of file
import type { UserSettings } from "../models/UserSettings";
import { loadSession } from "./sessionStorage";
-/**
- * KEY devient "userSettings:<userId>"
- */
function keyFor(userId: string) {
return `userSettings:${userId}`;
}
selectedStrategyKey: "RSI_SIMPLE",
};
+function normalizeCurrency(input: any): UserSettings["currency"] {
+ const v = String(input ?? "").toUpperCase();
+
+ // Migration anciens formats
+ if (v === "USD") return "USDT";
+ if (v === "USDT") return "USDT";
+ return "EUR";
+}
+
export async function loadSettings(): Promise<UserSettings> {
const session = await loadSession();
- if (!session) return DEFAULT_SETTINGS; // sécurité (normalement, App bloque sans session)
+ if (!session) return DEFAULT_SETTINGS;
const raw = await AsyncStorage.getItem(keyFor(session.userId));
if (!raw) return DEFAULT_SETTINGS;
try {
const parsed = JSON.parse(raw) as Partial<UserSettings>;
- return { ...DEFAULT_SETTINGS, ...parsed };
+
+ return {
+ ...DEFAULT_SETTINGS,
+ ...parsed,
+ currency: normalizeCurrency((parsed as any).currency),
+ };
} catch {
return DEFAULT_SETTINGS;
}
const session = await loadSession();
if (!session) return;
- await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(settings));
+ // sécurité : on force la currency à un enum valide
+ const safe: UserSettings = {
+ ...settings,
+ currency: normalizeCurrency((settings as any).currency),
+ };
+
+ await AsyncStorage.setItem(keyFor(session.userId), JSON.stringify(safe));
}
\ No newline at end of file