-const DEV_LAN_IP = "192.168.129.121";
\ No newline at end of file
+import { Platform } from "react-native";
+
+/**
+ * env.ts
+ * ------
+ * Objectif : 1 seul point de config réseau.
+ *
+ * DEV : IP LAN (PC qui fait tourner gateway en local)
+ * PROD : URL publique (serveur prof / déploiement)
+ *
+ * Le mobile parle UNIQUEMENT au Gateway :
+ * - REST : <GATEWAY>/api/...
+ * - Socket : <GATEWAY> (socket.io proxy via gateway)
+ */
+
+// ✅ DEV (chez toi / en classe quand tu lances sur ton PC)
+const DEV_LAN_IP = "192.168.129.121";
+const DEV_GATEWAY = `http://${DEV_LAN_IP}:3000`;
+
+// ✅ PROD (quand le chef vous donne l’URL du serveur prof)
+// Mets un placeholder clair pour ne pas oublier
+const PROD_GATEWAY = "https://CHANGE_ME_GATEWAY_URL";
+
+// Si tu veux autoriser un PROD en http (pas recommandé iOS), tu peux.
+// const PROD_GATEWAY = "http://CHANGE_ME_GATEWAY_URL:3000";
+
+/**
+ * Pour l'instant :
+ * - en Expo / dev => DEV_GATEWAY
+ * - en build (APK/IPA) => PROD_GATEWAY
+ */
+export const GATEWAY_BASE_URL = __DEV__ ? DEV_GATEWAY : PROD_GATEWAY;
+
+// REST (via gateway)
+export const API_BASE_URL = `${GATEWAY_BASE_URL}/api`;
+
+// Socket.IO (via gateway)
+export const SERVER_URL = GATEWAY_BASE_URL;
+
+/**
+ * Helpers (debug)
+ */
+export const ENV_MODE = __DEV__ ? "DEV" : "PROD";
+export const PLATFORM = Platform.OS;
\ No newline at end of file
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
* -----------
{
text: "Supprimer",
style: "destructive",
- onPress: () => {
- alertStore.clear?.();
+ onPress: async () => {
+ await clearAlertsLocal();
setItems(alertStore.getAll?.() ?? []);
},
},
contentContainerStyle={ui.container}
data={filteredSorted}
keyExtractor={(it, idx) =>
- it.id ??
+ // Alert n'a pas forcément d'id => fallback stable
+ (it as any).id ??
`${it.timestamp}-${it.pair}-${it.action}-${it.alertLevel}-${idx}`
}
ListHeaderComponent={
import { Ionicons } from "@expo/vector-icons";
import type { DashboardSummary } from "../types/DashboardSummary";
-import { fetchDashboardSummary } from "../services/dashboardService";
+
+/**
+ * ✅ Façade Dashboard (API-ready)
+ * - aujourd'hui : ce fichier peut encore être mock si ton dashboardApi l'est
+ * - demain : REST via gateway (/api/...)
+ */
+import { getDashboardSummary } from "../services/api/dashboardApi";
import { loadSettings } from "../utils/settingsStorage";
import type { UserSettings } from "../models/UserSettings";
import { loadPortfolio } from "../utils/portfolioStorage";
import type { PortfolioState, PortfolioAsset } from "../models/Portfolio";
-import { getMockPrice } from "../mocks/prices.mock";
import { loadSession } from "../utils/sessionStorage";
+/**
+ * ✅ API-only (price)
+ */
+import { getCurrentPrice } from "../services/api/priceApi";
+
/**
* DashboardScreen — Step 4
* ------------------------
* - Multi-users : userId vient de la session (AsyncStorage)
* - Settings/Portfolio sont déjà "scopés" par userId (via storages)
* - Socket.IO auth utilise userId de session
+ *
+ * IMPORTANT :
+ * - Les prix (portfolio) sont chargés via API-only (pas de mock).
+ * - Pour performance : on charge les prix du TOP 3 (affichage "valeur partielle").
*/
export default function DashboardScreen() {
const { height } = useWindowDimensions();
const [lastRefreshMs, setLastRefreshMs] = useState<number | null>(null);
const [refreshing, setRefreshing] = useState<boolean>(false);
+ // ✅ Prix (API-only) pour TOP 3 assets
+ const [assetPrices, setAssetPrices] = useState<Record<string, number>>({});
+ const [assetPricesError, setAssetPricesError] = useState<string | null>(null);
+
const navigation = useNavigation();
+ /**
+ * Top 3 assets (par quantité, puis symbole — simple et défendable)
+ * NOTE: si tu veux une vraie logique "top valeur", il faut les prix de tous les assets.
+ */
+ const topAssets: PortfolioAsset[] = useMemo(() => {
+ if (!portfolio) return [];
+
+ const copy = [...portfolio.assets];
+ copy.sort((a, b) => b.quantity - a.quantity || a.symbol.localeCompare(b.symbol));
+ return copy.slice(0, 3);
+ }, [portfolio]);
+
+ const remainingCount = useMemo(() => {
+ if (!portfolio) return 0;
+ return Math.max(0, portfolio.assets.length - topAssets.length);
+ }, [portfolio, topAssets]);
+
+ /**
+ * Charge prix API-only pour les TOP 3 assets
+ */
+ const loadTopAssetPrices = useCallback(async (assets: PortfolioAsset[], currency: string) => {
+ setAssetPricesError(null);
+
+ if (assets.length === 0) {
+ setAssetPrices({});
+ return;
+ }
+
+ try {
+ const entries = await Promise.all(
+ assets.map(async (a) => {
+ const pair = `${a.symbol}/${currency}`;
+ const cur = await getCurrentPrice(pair);
+ return [a.symbol, cur.price] as const;
+ })
+ );
+
+ const map: Record<string, number> = {};
+ for (const [sym, pr] of entries) map[sym] = pr;
+ setAssetPrices(map);
+ } catch (e: any) {
+ setAssetPrices({});
+ setAssetPricesError(e?.message ?? "Impossible de charger les prix (API).");
+ }
+ }, []);
+
+ /**
+ * Chargement initial (focus)
+ * - dashboard
+ * - settings
+ * - portfolio
+ * + prix (API-only) sur top 3
+ */
useFocusEffect(
useCallback(() => {
let isActive = true;
setLoading(true);
const [dashboardData, userSettings, portfolioData] = await Promise.all([
- fetchDashboardSummary(),
+ getDashboardSummary(),
loadSettings(),
loadPortfolio(),
]);
setSettings(userSettings);
setPortfolio(portfolioData);
setLastRefreshMs(Date.now());
+
+ // prix API-only sur top 3
+ const copy = [...portfolioData.assets];
+ copy.sort((a, b) => b.quantity - a.quantity || a.symbol.localeCompare(b.symbol));
+ const top = copy.slice(0, 3);
+ await loadTopAssetPrices(top, userSettings.currency);
} catch {
if (isActive) setError("Impossible de charger le dashboard.");
} finally {
}
}
- loadData();
+ void loadData();
return () => {
isActive = false;
};
- }, [])
+ }, [loadTopAssetPrices])
);
+ /**
+ * Refresh auto (si activé)
+ */
useEffect(() => {
if (!settings) return;
if (settings.refreshMode !== "auto") return;
const intervalId = setInterval(async () => {
try {
- const data = await fetchDashboardSummary();
+ const data = await getDashboardSummary();
if (!cancelled) {
setSummary(data);
setLastRefreshMs(Date.now());
};
}, [settings]);
+ /**
+ * Refresh manuel
+ */
const handleManualRefresh = async () => {
try {
setRefreshing(true);
- const data = await fetchDashboardSummary();
+ const data = await getDashboardSummary();
setSummary(data);
setLastRefreshMs(Date.now());
setError(null);
+
+ // refresh prix top assets aussi (API-only)
+ if (settings) {
+ await loadTopAssetPrices(topAssets, settings.currency);
+ }
} catch {
setError("Erreur lors de l'actualisation.");
} finally {
};
}, [settings]);
+ /**
+ * Alerte "urgente" (priorité : CRITICAL > WARNING > INFO)
+ * Le dashboard n'affiche qu'une seule alerte (la plus importante).
+ */
const urgentAlert: Alert | null = useMemo(() => {
if (liveAlerts.length === 0) return null;
return liveAlerts[0];
}, [liveAlerts]);
- const portfolioTotalValue = useMemo(() => {
- if (!portfolio) return 0;
-
- return portfolio.assets.reduce((sum, a) => {
- const price = getMockPrice(a.symbol);
- if (price === null) return sum;
- return sum + a.quantity * price;
- }, 0);
- }, [portfolio]);
-
- const topAssets: PortfolioAsset[] = useMemo(() => {
- if (!portfolio) return [];
+ /**
+ * Valeur portefeuille (partielle = top3), API-only
+ */
+ const portfolioPartialValue = useMemo(() => {
+ if (!portfolio) return null;
- const withValue = portfolio.assets.map((a) => {
- const price = getMockPrice(a.symbol) ?? 0;
- return { ...a, _value: a.quantity * price };
- });
+ let sum = 0;
+ let hasAny = false;
- withValue.sort((a, b) => (b as any)._value - (a as any)._value);
- return withValue.slice(0, 3).map(({ symbol, quantity }) => ({ symbol, quantity }));
- }, [portfolio]);
+ for (const a of topAssets) {
+ const price = assetPrices[a.symbol];
+ if (typeof price !== "number") continue;
+ hasAny = true;
+ sum += a.quantity * price;
+ }
- const remainingCount = useMemo(() => {
- if (!portfolio) return 0;
- return Math.max(0, portfolio.assets.length - topAssets.length);
- }, [portfolio, topAssets]);
+ return hasAny ? sum : null;
+ }, [portfolio, topAssets, assetPrices]);
if (loading) {
return (
</View>
<View style={ui.rowBetween}>
- <Text style={ui.value}>Valeur globale :</Text>
+ <Text style={ui.value}>Valeur (top 3) :</Text>
<Text style={ui.valueBold}>
- {portfolioTotalValue.toFixed(2)} {settings.currency}
+ {portfolioPartialValue !== null ? `${portfolioPartialValue.toFixed(2)} ${settings.currency}` : "—"}
</Text>
</View>
+ {!!assetPricesError && (
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={2}>
+ ⚠️ {assetPricesError}
+ </Text>
+ )}
+
{portfolio.assets.length === 0 ? (
<Text style={[ui.muted, { marginTop: 8 }]}>
Aucun asset (ajoute BTC/ETH/SOL dans Portefeuille)
)}
</View>
)}
-
- <Text style={[ui.muted, { marginTop: 8 }]} numberOfLines={1}>
- BTC @ {summary.price.toFixed(2)} {settings.currency} (mock)
- </Text>
</View>
</TouchableOpacity>
import { SafeAreaView } from "react-native-safe-area-context";
import type { Signal, SignalAction, SignalCriticality } from "../types/Signal";
-import { fetchRecentSignals } from "../services/signalService";
+
+/**
+ * ✅ API-ready (façade)
+ */
+import { getRecentSignals } from "../services/api/signalApi";
+
import { ui } from "../components/ui/uiStyles";
function getActionColor(action: SignalAction): string {
try {
setError(null);
setLoading(true);
- const data = await fetchRecentSignals(20);
+
+ const data = await getRecentSignals(20);
if (active) setItems(data);
} catch {
if (active) setError("Impossible de charger l'historique.");
flexWrap: "wrap",
}}
>
- <View
- style={[
- ui.badge,
- { backgroundColor: `${actionColor}22`, marginTop: 0 },
- ]}
- >
- <Text style={[ui.badgeText, { color: actionColor }]}>
- {item.action}
- </Text>
+ <View style={[ui.badge, { backgroundColor: `${actionColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: actionColor }]}>{item.action}</Text>
</View>
- <View
- style={[
- ui.badge,
- { backgroundColor: `${critColor}22`, marginTop: 0 },
- ]}
- >
- <Text style={[ui.badgeText, { color: critColor }]}>
- {item.criticality}
- </Text>
+ <View style={[ui.badge, { backgroundColor: `${critColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: critColor }]}>{item.criticality}</Text>
</View>
- <View
- style={[
- ui.badge,
- { backgroundColor: "#11182722", marginTop: 0 },
- ]}
- >
- <Text style={[ui.badgeText, { color: "#111827" }]}>
- {item.status}
- </Text>
+ <View style={[ui.badge, { backgroundColor: "#11182722", marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: "#111827" }]}>{item.status}</Text>
</View>
</View>
<View style={[ui.rowBetween, { marginTop: 10 }]}>
<Text style={ui.value}>Confiance</Text>
- <Text style={ui.valueBold}>
- {(item.confidence * 100).toFixed(1)}%
- </Text>
+ <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(1)}%</Text>
</View>
<View style={[ui.rowBetween, { marginTop: 6 }]}>
* - 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,
}) {
const [settings, setSettings] = useState<UserSettings | null>(null);
const [infoMessage, setInfoMessage] = useState<string | null>(null);
+ const [saving, setSaving] = useState<boolean>(false);
useEffect(() => {
+ let active = true;
+
async function init() {
const data = await loadSettings();
+ if (!active) return;
setSettings(data);
}
+
init();
+
+ return () => {
+ active = false;
+ };
}, []);
if (!settings) {
);
}
- const toggleCurrency = () => {
+ /**
+ * Sauvegarde "safe"
+ * - évite les doubles clics
+ * - affiche un message simple
+ */
+ const persist = async (next: UserSettings, msg?: string) => {
+ try {
+ setSaving(true);
+ await saveSettings(next);
+ setSettings(next);
+ if (msg) setInfoMessage(msg);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const toggleCurrency = async () => {
+ setInfoMessage(null);
const newCurrency = settings.currency === "EUR" ? "USD" : "EUR";
- setSettings({ ...settings, currency: newCurrency });
+ await persist({ ...settings, currency: newCurrency }, `Devise : ${newCurrency} ✅`);
};
- const toggleRefreshMode = () => {
+ const toggleRefreshMode = async () => {
+ setInfoMessage(null);
const newMode = settings.refreshMode === "manual" ? "auto" : "manual";
- setSettings({ ...settings, refreshMode: newMode });
+ await persist({ ...settings, refreshMode: newMode }, `Rafraîchissement : ${newMode} ✅`);
};
const toggleNotifications = async () => {
setInfoMessage(null);
- if (settings.notificationsEnabled) {
- setSettings({ ...settings, notificationsEnabled: false });
- setInfoMessage("Notifications désactivées (pense à sauvegarder).");
- return;
- }
-
- const granted = await requestNotificationPermission();
- if (!granted) {
- setSettings({ ...settings, notificationsEnabled: false });
- setInfoMessage("Permission refusée : notifications désactivées.");
+ // OFF -> ON : on demande la permission
+ if (!settings.notificationsEnabled) {
+ const granted = await requestNotificationPermission();
+ if (!granted) {
+ await persist(
+ { ...settings, notificationsEnabled: false },
+ "Permission refusée : notifications désactivées."
+ );
+ return;
+ }
+
+ await persist(
+ { ...settings, notificationsEnabled: true },
+ "Notifications activées ✅"
+ );
return;
}
- setSettings({ ...settings, notificationsEnabled: true });
- setInfoMessage("Notifications activées ✅ (pense à sauvegarder).");
+ // ON -> OFF
+ 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 () => {
- await saveSettings(settings);
- setInfoMessage("Paramètres sauvegardés ✅");
+ setInfoMessage(null);
+ await persist(settings, "Paramètres sauvegardés ✅");
};
const handleReplayTutorial = () => {
<Text style={ui.title}>Devise</Text>
<Text style={ui.value}>Actuelle : {settings.currency}</Text>
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleCurrency}>
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
+ onPress={toggleCurrency}
+ disabled={saving}
+ >
<Text style={ui.buttonText}>Changer devise</Text>
</TouchableOpacity>
</View>
<Text style={ui.title}>Rafraîchissement</Text>
<Text style={ui.value}>Mode : {settings.refreshMode}</Text>
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleRefreshMode}>
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
+ onPress={toggleRefreshMode}
+ disabled={saving}
+ >
<Text style={ui.buttonText}>Changer mode</Text>
</TouchableOpacity>
</View>
<Text style={ui.title}>Notifications</Text>
<Text style={ui.value}>Statut : {settings.notificationsEnabled ? "ON" : "OFF"}</Text>
- <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleNotifications}>
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, saving && styles.btnDisabled]}
+ onPress={toggleNotifications}
+ disabled={saving}
+ >
<Text style={ui.buttonText}>
{settings.notificationsEnabled ? "Désactiver" : "Activer"} les notifications
</Text>
</TouchableOpacity>
</View>
- {/* Bouton Save */}
- <TouchableOpacity style={[ui.button, styles.fullButton, styles.saveButton]} onPress={handleSave}>
- <Text style={ui.buttonText}>Sauvegarder</Text>
+ {/* Bouton Save (garde-fou) */}
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, styles.saveButton, saving && styles.btnDisabled]}
+ onPress={handleSave}
+ disabled={saving}
+ >
+ <Text style={ui.buttonText}>{saving ? "Sauvegarde…" : "Sauvegarder"}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
color: "#0f172a",
opacity: 0.85,
},
+ btnDisabled: {
+ opacity: 0.7,
+ },
});
\ No newline at end of file
import { SafeAreaView } from "react-native-safe-area-context";
import { ui } from "../components/ui/uiStyles";
-import { loadSettings, saveSettings } from "../utils/settingsStorage";
+import { loadSettings } from "../utils/settingsStorage";
import type { UserSettings } from "../models/UserSettings";
import type { StrategyKey, StrategyOption } from "../types/Strategy";
import { fetchStrategies } from "../services/strategyService";
/**
- * StrategyScreen
- * --------------
- * L'utilisateur sélectionne une stratégie.
- * La stratégie choisie est persistée dans UserSettings (AsyncStorage).
- *
- * Plus tard : on pourra aussi appeler POST /api/strategy/select,
- * sans changer l'écran.
+ * ✅ API-ready (façade)
+ * Aujourd'hui : update local settings
+ * Demain : POST /api/strategy/select
*/
+import { selectStrategy } from "../services/api/strategyApi";
+
export default function StrategyScreen() {
const [settings, setSettings] = useState<UserSettings | null>(null);
const [strategies, setStrategies] = useState<StrategyOption[]>([]);
const isSelected = (key: StrategyKey) => settings.selectedStrategyKey === key;
const handleSelect = async (key: StrategyKey) => {
- const updated: UserSettings = { ...settings, selectedStrategyKey: key };
- setSettings(updated);
-
- // Sauvegarde immédiate (simple et défendable)
- await saveSettings(updated);
+ // ✅ Façade : aujourd'hui local, demain API
+ const updated = await selectStrategy(key);
+ setSettings(updated);
setInfo(`Stratégie sélectionnée : ${key}`);
};
<Text style={[ui.muted, { marginTop: 8 }]}>{item.description}</Text>
<TouchableOpacity
- style={[ui.button, styles.fullButton, isSelected(item.key) && styles.btnSelected]}
+ style={[
+ ui.button,
+ styles.fullButton,
+ isSelected(item.key) && styles.btnSelected,
+ ]}
onPress={() => handleSelect(item.key)}
>
<Text style={ui.buttonText}>
import { ui } from "../components/ui/uiStyles";
import type { PortfolioAsset, PortfolioState } from "../models/Portfolio";
-import { loadPortfolio, savePortfolio, clearPortfolio } from "../utils/portfolioStorage";
-import { getMockPrice } from "../mocks/prices.mock";
import { loadSettings } from "../utils/settingsStorage";
import type { UserSettings } from "../models/UserSettings";
/**
- * WalletScreen (Step 3)
- * ---------------------
- * Mono-user / multi-cryptos
- * - liste d'assets (BTC, ETH, SOL...)
+ * ✅ API-only (wallet)
+ */
+import { getPortfolio, upsertPortfolio, resetPortfolio } from "../services/api/walletApi";
+
+/**
+ * ✅ API-only (price)
+ */
+import { getCurrentPrice } from "../services/api/priceApi";
+
+/**
+ * WalletScreen (Step 3/4)
+ * -----------------------
+ * Multi-cryptos
* - quantité par asset
- * - valeur globale du portefeuille
+ * - valeur globale
*
- * Aujourd'hui : prix mock
- * Demain : prix via GET /api/price/current?pair=XXX/EUR
+ * Prix : API-only (pas de mock)
*/
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);
+
// Ajout asset
const [symbolInput, setSymbolInput] = useState<string>("BTC");
const [qtyInput, setQtyInput] = useState<string>("0");
async function init() {
setInfo(null);
- const [p, s] = await Promise.all([loadPortfolio(), loadSettings()]);
+ setPricesError(null);
+
+ const [p, s] = await Promise.all([getPortfolio(), loadSettings()]);
if (!active) return;
setPortfolio(p);
setSettings(s);
+
+ // Charger les prix pour les assets actuels
+ 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;
+ })
+ );
+
+ if (!active) return;
+
+ const map: Record<string, number> = {};
+ for (const [sym, pr] of entries) map[sym] = pr;
+ setPrices(map);
+ } catch (e: any) {
+ if (!active) return;
+ setPricesError(e?.message ?? "Impossible de charger les prix (API).");
+ }
}
- init();
+ void init();
return () => {
active = false;
if (!portfolio) return 0;
return portfolio.assets.reduce((sum, a) => {
- const price = getMockPrice(a.symbol);
- if (price === null) return sum;
+ const price = prices[a.symbol];
+ if (typeof price !== "number") return sum; // prix absent => on ignore
return sum + a.quantity * price;
}, 0);
- }, [portfolio]);
+ }, [portfolio, prices]);
const handleAddOrUpdate = async () => {
- if (!portfolio) return;
+ if (!portfolio || !settings) return;
setInfo(null);
return;
}
- const price = getMockPrice(normalizedSymbol);
- if (price === null) {
- setInfo("Prix inconnu (mock). Essayez: BTC, ETH, SOL, ADA.");
+ // ✅ 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;
}
let updatedAssets: PortfolioAsset[];
if (existingIndex >= 0) {
- // update
updatedAssets = portfolio.assets.map((a) =>
a.symbol === normalizedSymbol ? { ...a, quantity: parsedQty } : a
);
} else {
- // add
updatedAssets = [...portfolio.assets, { symbol: normalizedSymbol, quantity: parsedQty }];
}
updatedAtMs: Date.now(),
};
- await savePortfolio(updated);
+ await upsertPortfolio(updated);
setPortfolio(updated);
setInfo(existingIndex >= 0 ? "Asset mis à jour ✅" : "Asset ajouté ✅");
RNAlert.alert(
`Supprimer ${symbol} ?`,
- "Cette action retire l’asset du portefeuille local.",
+ "Cette action retire l’asset du portefeuille.",
[
{ text: "Annuler", style: "cancel" },
{
assets: portfolio.assets.filter((a) => a.symbol !== symbol),
updatedAtMs: Date.now(),
};
- await savePortfolio(updated);
+
+ 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 stockage local.",
+ "Cela supprime tous les assets du portefeuille.",
[
{ text: "Annuler", style: "cancel" },
{
text: "Réinitialiser",
style: "destructive",
onPress: async () => {
- await clearPortfolio();
- const fresh = await loadPortfolio();
+ const fresh = await resetPortfolio();
setPortfolio(fresh);
+ setPrices({});
setInfo("Portefeuille réinitialisé ✅");
},
},
</View>
<Text style={[ui.muted, { marginTop: 6 }]}>
- Dernière mise à jour :{" "}
- <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
+ Dernière mise à jour : <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
</Text>
+ {!!pricesError && (
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ ⚠️ {pricesError}
+ </Text>
+ )}
+
<TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
<Text style={styles.secondaryButtonText}>Réinitialiser</Text>
</TouchableOpacity>
<View style={ui.card}>
<Text style={ui.title}>Ajouter / Modifier un asset</Text>
- <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL, ADA)</Text>
+ <Text style={ui.muted}>Symbole (ex: BTC, ETH, SOL)</Text>
<TextInput
value={symbolInput}
onChangeText={setSymbolInput}
</TouchableOpacity>
{!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
- <Text style={[ui.muted, { marginTop: 10 }]}>
- Prix utilisés = mock pour Step 3 (API à brancher plus tard).
- </Text>
</View>
- <Text style={[ui.muted, { marginBottom: 10 }]}>
- Liste des assets :
- </Text>
+ <Text style={[ui.muted, { marginBottom: 10 }]}>Liste des assets :</Text>
</View>
}
ListEmptyComponent={
</View>
}
renderItem={({ item }) => {
- const price = getMockPrice(item.symbol);
- const value = price !== null ? item.quantity * price : null;
+ const price = prices[item.symbol];
+ const value = typeof price === "number" ? item.quantity * price : null;
return (
<View style={ui.card}>
</View>
<View style={[ui.rowBetween, { marginTop: 6 }]}>
- <Text style={ui.value}>Prix (mock)</Text>
+ <Text style={ui.value}>Prix</Text>
<Text style={ui.valueBold}>
- {price !== null ? `${price.toFixed(2)} ${settings.currency}` : "—"}
+ {typeof price === "number" ? `${price.toFixed(2)} ${settings.currency}` : "—"}
</Text>
</View>
import type { Alert } from "../../types/Alert";
+import { apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
import { alertStore } from "../alertStore";
/**
- * alertsApi
- * ---------
- * Contrat futur (gateway):
- * - GET /api/alerts/events?userId=...&limit=10
- * - POST /api/alerts
- * - POST /api/alerts/:id/toggle
- *
- * Pour l'instant : on lit ce qu'on a en local (alertStore alimenté par Socket).
+ * alertsApi (API-first)
+ * --------------------
+ * - Lecture events serveur (optionnel) : GET /api/alerts/events
+ * - Clear : local (car pas d'endpoint "clear" dans le contrat)
*/
-export async function getRecentAlerts(limit = 10): Promise<Alert[]> {
- const all = alertStore.getAll?.() ?? [];
- return all.slice(0, limit);
+export async function getAlertEvents(limit = 10): Promise<Alert[]> {
+ const session = await loadSession();
+ const userId = session?.userId;
+ if (!userId) throw new Error("Session absente : impossible de charger les alertes.");
+
+ const data = await apiGet<{ events: Alert[] }>(
+ `/alerts/events?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
+ );
+
+ return data.events ?? [];
}
-export async function clearLocalAlerts(): Promise<void> {
+/**
+ * Clear local : vide la liste affichée (alertStore).
+ * (Le contrat API ne prévoit pas de suppression globale côté serveur.)
+ */
+export async function clearAlertsLocal(): Promise<void> {
alertStore.clear?.();
}
\ No newline at end of file
--- /dev/null
+import type { DashboardSummary } from "../../types/DashboardSummary";
+import type { Alert } from "../../types/Alert";
+import { apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
+import { loadSettings } from "../../utils/settingsStorage";
+
+/**
+ * dashboardApi (API-only)
+ * -----------------------
+ * Construit un DashboardSummary en appelant les endpoints du contrat.
+ * Aucun fallback mock.
+ */
+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 pair = "BTC/EUR"; // TODO: quand tu gères multi-paires sur dashboard, tu changes ici.
+
+ // 1) Prix courant
+ const priceData = await apiGet<{
+ pair: string;
+ timestamp_ms: number;
+ current_price: number;
+ }>(`/price/current?pair=${encodeURIComponent(pair)}`);
+
+ // 2) Signal courant
+ const signalData = await apiGet<{
+ action: Alert["action"]; // BUY/SELL/HOLD/STOP_LOSS
+ criticality: Alert["alertLevel"]; // CRITICAL/WARNING/INFO
+ confidence: number; // 0..1
+ reason: string;
+ timestamp_ms: number;
+ }>(`/signal/current?userId=${encodeURIComponent(userId)}&pair=${encodeURIComponent(pair)}`);
+
+ return {
+ pair,
+ price: Number(priceData.current_price),
+ strategy: settings.selectedStrategyKey, // affichage “mobile” (en attendant un champ API)
+ decision: signalData.action,
+ alertLevel: signalData.criticality,
+ confidence: Number(signalData.confidence),
+ reason: String(signalData.reason),
+ timestamp: Number(signalData.timestamp_ms),
+ };
+}
\ No newline at end of file
--- /dev/null
+import { API_BASE_URL } from "../../config/env";
+
+/**
+ * Petit helper HTTP (fetch) :
+ * - force JSON
+ * - vérifie le format { ok: true, data } / { ok:false, error }
+ * - jette une erreur claire si l’API répond mal ou si HTTP != 2xx
+ */
+export async function apiGet<T>(path: string): Promise<T> {
+ const res = await fetch(`${API_BASE_URL}${path}`, { method: "GET" });
+
+ const json = await res.json().catch(() => null);
+
+ if (!res.ok) {
+ const msg = json?.error?.message ?? `HTTP ${res.status}`;
+ throw new Error(msg);
+ }
+ if (!json || json.ok !== true) {
+ const msg = json?.error?.message ?? "Réponse API invalide (ok=false)";
+ throw new Error(msg);
+ }
+ return json.data as T;
+}
+
+export async function apiPost<T>(path: string, body: unknown): Promise<T> {
+ const res = await fetch(`${API_BASE_URL}${path}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+
+ const json = await res.json().catch(() => null);
+
+ if (!res.ok) {
+ const msg = json?.error?.message ?? `HTTP ${res.status}`;
+ throw new Error(msg);
+ }
+ if (!json || json.ok !== true) {
+ const msg = json?.error?.message ?? "Réponse API invalide (ok=false)";
+ throw new Error(msg);
+ }
+ return json.data as T;
+}
\ No newline at end of file
-import type { DashboardSummary } from "../../types/DashboardSummary";
-import { fetchDashboardSummary } from "../dashboardService";
+import { apiGet } from "./http";
/**
- * priceApi
- * --------
- * Contrat futur (gateway):
+ * priceApi (API-only)
+ * -------------------
+ * Contrat :
* - GET /api/price/current?pair=BTC/EUR
*
- * Pour l'instant : on renvoie la donnée mock déjà utilisée par le Dashboard.
+ * Retour attendu (à ajuster si le backend diffère) :
+ * { ok:true, data:{ pair, timestamp_ms, current_price } }
*/
-export async function getCurrentPriceForDashboard(): Promise<Pick<DashboardSummary, "pair" | "price" | "timestamp">> {
- const d = await fetchDashboardSummary();
- return { pair: d.pair, price: d.price, timestamp: d.timestamp };
+
+export type PriceCurrent = {
+ pair: string;
+ timestampMs: number;
+ price: number;
+};
+
+export async function getCurrentPrice(pair: string): Promise<PriceCurrent> {
+ const data = await apiGet<{
+ pair: string;
+ timestamp_ms: number;
+ current_price: number;
+ }>(`/price/current?pair=${encodeURIComponent(pair)}`);
+
+ return {
+ pair: data.pair,
+ timestampMs: Number(data.timestamp_ms),
+ price: Number(data.current_price),
+ };
}
\ No newline at end of file
import type { Signal } from "../../types/Signal";
-import { fetchRecentSignals } from "../signalService";
+import { apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
/**
- * signalApi
- * ---------
- * Contrat futur (gateway):
- * - GET /api/signal/current?userId=...&pair=BTC/EUR
- * - (option) GET /api/signal/recent?userId=...&limit=20
- *
- * Pour l'instant : on utilise les mocks existants.
+ * signalApi (API-only)
+ * --------------------
+ * NOTE: endpoint à confirmer/aligner avec le chef.
+ * Proposition :
+ * - GET /api/signal/recent?userId=...&limit=20
*/
export async function getRecentSignals(limit = 20): Promise<Signal[]> {
- return await fetchRecentSignals(limit);
+ const session = await loadSession();
+ const userId = session?.userId;
+ if (!userId) throw new Error("Session absente : impossible de charger l'historique.");
+
+ const data = await apiGet<{ signals: Signal[] }>(
+ `/signal/recent?userId=${encodeURIComponent(userId)}&limit=${encodeURIComponent(String(limit))}`
+ );
+
+ return data.signals ?? [];
}
\ No newline at end of file
-import { loadSettings, saveSettings } from "../../utils/settingsStorage";
+import { apiPost } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
import type { UserSettings } from "../../models/UserSettings";
/**
- * strategyApi
- * -----------
- * Contrat futur (gateway):
- * - POST /api/strategy/select { userId, pair, strategyKey, params... }
- *
- * Pour l'instant : on sauvegarde localement dans settingsStorage.
+ * strategyApi (API-only)
+ * ----------------------
+ * POST /api/strategy/select
*/
export async function selectStrategy(strategyKey: string): Promise<UserSettings> {
- const s = await loadSettings();
- const next: UserSettings = { ...s, selectedStrategyKey: strategyKey };
- await saveSettings(next);
- return next;
+ const session = await loadSession();
+ const userId = session?.userId;
+ if (!userId) throw new Error("Session absente : impossible de sélectionner une stratégie.");
+
+ // Le backend décidera quoi renvoyer : settings mis à jour ou juste ok.
+ // On part sur "settings" pour éviter de recharger partout.
+ const data = await apiPost<{ settings: UserSettings }>(`/strategy/select`, {
+ userId,
+ strategyKey,
+ });
+
+ if (!data.settings) throw new Error("Réponse API invalide : settings manquant.");
+ return data.settings;
}
\ No newline at end of file
--- /dev/null
+import { apiPost, apiGet } from "./http";
+import { loadSession } from "../../utils/sessionStorage";
+import type { PortfolioState } from "../../models/Portfolio";
+
+/**
+ * walletApi (API-only)
+ * --------------------
+ * - GET /api/wallets?userId=... (à confirmer)
+ * - POST /api/wallets/upsert
+ *
+ * NOTE: reset = upsert d'un portefeuille vide (pas besoin d'un endpoint dédié)
+ */
+
+function nowMs() {
+ return Date.now();
+}
+
+export async function getPortfolio(): Promise<PortfolioState> {
+ const session = await loadSession();
+ const userId = session?.userId;
+ if (!userId) throw new Error("Session absente : impossible de charger le portefeuille.");
+
+ const data = await apiGet<{ portfolio: PortfolioState }>(
+ `/wallets?userId=${encodeURIComponent(userId)}`
+ );
+
+ if (!data.portfolio) throw new Error("Réponse API invalide : portfolio manquant.");
+ return data.portfolio;
+}
+
+export async function upsertPortfolio(next: PortfolioState): Promise<PortfolioState> {
+ const session = await loadSession();
+ const userId = session?.userId;
+ if (!userId) throw new Error("Session absente : impossible de sauvegarder le portefeuille.");
+
+ const data = await apiPost<{ portfolio: PortfolioState }>(`/wallets/upsert`, {
+ userId,
+ portfolio: next,
+ });
+
+ if (!data.portfolio) throw new Error("Réponse API invalide : portfolio manquant.");
+ return data.portfolio;
+}
+
+/**
+ * Reset portefeuille (API-only)
+ * => on sauvegarde un portefeuille vide via /wallets/upsert
+ */
+export async function resetPortfolio(): Promise<PortfolioState> {
+ const empty: PortfolioState = { assets: [], updatedAtMs: nowMs() };
+ return await upsertPortfolio(empty);
+}
\ No newline at end of file