+++ /dev/null
-> Why do I have a folder named ".expo" in my project?
-
-The ".expo" folder is created when an Expo project is started using "expo start" command.
-
-> What do the files contain?
-
-- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
-- "settings.json": contains the server configuration that is used to serve the application manifest.
-
-> Should I commit the ".expo" folder?
-
-No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
-Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
+++ /dev/null
-{
- "devices": []
-}
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
+import { TouchableOpacity } from "react-native";
+import { Ionicons } from "@expo/vector-icons";
import DashboardScreen from "./src/screens/DashboardScreen";
import SettingsScreen from "./src/screens/SettingsScreen";
import HistoryScreen from "./src/screens/HistoryScreen";
+import AlertsScreen from "./src/screens/AlertsScreen";
+import StrategyScreen from "./src/screens/StrategyScreen";
+import WalletScreen from "./src/screens/WalletScreen";
// Types des routes (pour éviter les erreurs de navigation)
export type RootStackParamList = {
Dashboard: undefined;
Settings: undefined;
History: undefined;
+ Alerts: undefined;
+ Strategy: undefined;
+ Wallet: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
<Stack.Screen
name="Dashboard"
component={DashboardScreen}
- options={{ title: "Dashboard" }}
+ options={({ navigation }) => ({
+ title: "Dashboard",
+ headerRight: () => (
+ <TouchableOpacity onPress={() => navigation.navigate("Settings")}>
+ <Ionicons name="settings-outline" size={22} color="#0f172a" />
+ </TouchableOpacity>
+ ),
+ })}
+ />
+
+ <Stack.Screen
+ name="Wallet"
+ component={WalletScreen}
+ options={{ title: "Portefeuille" }}
+ />
+
+ <Stack.Screen
+ name="Strategy"
+ component={StrategyScreen}
+ options={{ title: "Stratégie" }}
+ />
+
+ <Stack.Screen
+ name="Alerts"
+ component={AlertsScreen}
+ options={{ title: "Alertes" }}
+ />
+
+ <Stack.Screen
+ name="History"
+ component={HistoryScreen}
+ options={{ title: "Historique" }}
/>
<Stack.Screen
component={SettingsScreen}
options={{ title: "Paramètres" }}
/>
-
- <Stack.Screen
- name="History"
- component={HistoryScreen}
- options={{ title: "Historique" }}
- />
-
</Stack.Navigator>
</NavigationContainer>
);
"name": "wall-e-tte",
"version": "1.0.0",
"dependencies": {
+ "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.1.28",
"@react-navigation/native-stack": "^7.13.0",
"expo": "~54.0.33",
+ "expo-notifications": "~0.32.16",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6.9.0"
}
"integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==",
"license": "MIT"
},
+ "node_modules/@expo/vector-icons": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz",
+ "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo-font": ">=14.0.4",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@expo/ws-tunnel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz",
"node": ">=8"
}
},
+ "node_modules/@ide/backoff": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
+ "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
+ "license": "MIT"
+ },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
+ "node_modules/assert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+ "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-nan": "^1.3.2",
+ "object-is": "^1.1.5",
+ "object.assign": "^4.1.4",
+ "util": "^0.12.5"
+ }
+ },
"node_modules/async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"license": "MIT"
},
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
"@babel/core": "^7.0.0"
}
},
+ "node_modules/badgin": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
+ "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"node": ">= 0.8"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"node": ">=8"
}
},
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"url": "https://dotenvx.com"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"stackframe": "^1.3.4"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.23",
}
}
},
+ "node_modules/expo-application": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
+ "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-constants": {
+ "version": "18.0.13",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
+ "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config": "~12.0.13",
+ "@expo/env": "~2.0.8"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-font": {
+ "version": "14.0.11",
+ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
+ "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
+ "license": "MIT",
+ "dependencies": {
+ "fontfaceobserver": "^2.1.0"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-modules-autolinking": {
"version": "3.0.24",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz",
"react-native": "*"
}
},
+ "node_modules/expo-notifications": {
+ "version": "0.32.16",
+ "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
+ "integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/image-utils": "^0.8.8",
+ "@ide/backoff": "^1.0.0",
+ "abort-controller": "^3.0.0",
+ "assert": "^2.0.0",
+ "badgin": "^1.1.5",
+ "expo-application": "~7.0.8",
+ "expo-constants": "~18.0.13"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-server": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz",
}
}
},
- "node_modules/expo/node_modules/@expo/vector-icons": {
- "version": "15.0.3",
- "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.3.tgz",
- "integrity": "sha512-SBUyYKphmlfUBqxSfDdJ3jAdEVSALS2VUPOUyqn48oZmb2TL/O7t7/PQm5v4NQujYEPLPMTLn9KVw6H7twwbTA==",
- "license": "MIT",
- "peerDependencies": {
- "expo-font": ">=14.0.4",
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/expo/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"react-native": "*"
}
},
- "node_modules/expo/node_modules/expo-constants": {
- "version": "18.0.13",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
- "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
- "license": "MIT",
- "dependencies": {
- "@expo/config": "~12.0.13",
- "@expo/env": "~2.0.8"
- },
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
"node_modules/expo/node_modules/expo-file-system": {
"version": "19.0.21",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
"react-native": "*"
}
},
- "node_modules/expo/node_modules/expo-font": {
- "version": "14.0.11",
- "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
- "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "fontfaceobserver": "^2.1.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/expo/node_modules/expo-keep-awake": {
"version": "15.0.8",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
"integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==",
"license": "BSD-2-Clause"
},
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/freeport-async": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz",
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"node": ">=8.0.0"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/getenv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
"node": ">=4"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"node": ">=4"
}
},
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"loose-envify": "^1.0.0"
}
},
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"node": ">=8"
}
},
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-nan": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+ "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"node": ">=8"
}
},
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0"
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"node": ">=0.10.0"
}
},
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"node": ">=4.0.0"
}
},
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
],
"license": "MIT"
},
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/sax": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
"node": ">= 0.8"
}
},
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"node": ">= 8"
}
},
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/wonka": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
"web": "expo start --web"
},
"dependencies": {
+ "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/native": "^7.1.28",
"@react-navigation/native-stack": "^7.13.0",
"expo": "~54.0.33",
+ "expo-notifications": "~0.32.16",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
+++ /dev/null
-import { View, Text, TouchableOpacity } from "react-native";
-import { ui } from "./ui/uiStyles";
-
-type Props = {
- onGoSettings: () => void;
- onGoHistory: () => void;
-};
-
-/**
- * ActionsCard
- * -----------
- * Regroupe les actions rapides de l'utilisateur.
- * Pour l'instant :
- * - Voir stratégie (placeholder)
- * - Historique (placeholder)
- * - Paramètres (navigation réelle via callback)
- */
-export default function ActionsCard({ onGoSettings, onGoHistory }: Props) {
- return (
- <View style={ui.card}>
- <Text style={ui.title}>Actions</Text>
-
- <View style={[ui.rowBetween, { flexWrap: "wrap", gap: 8 }]}>
- <TouchableOpacity style={ui.button} onPress={() => {}}>
- <Text style={ui.buttonText}>Voir stratégie</Text>
- </TouchableOpacity>
-
- <TouchableOpacity style={ui.button} onPress={onGoHistory}>
- <Text style={ui.buttonText}>Historique</Text>
- </TouchableOpacity>
-
- <TouchableOpacity style={ui.button} onPress={onGoSettings}>
- <Text style={ui.buttonText}>Paramètres</Text>
- </TouchableOpacity>
- </View>
- </View>
- );
-}
\ No newline at end of file
-import { View, Text } from "react-native";
-import type {
- DashboardSummary,
- TradeDecision,
- AlertLevel,
-} from "../types/DashboardSummary";
+import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
+import type { DashboardSummary } from "../types/DashboardSummary";
+import type { UserSettings } from "../models/UserSettings";
import { ui } from "./ui/uiStyles";
-type Props = {
- summary: DashboardSummary;
-};
-
-function getDecisionColor(decision: TradeDecision): string {
- switch (decision) {
- case "BUY":
- return "#16a34a";
- case "SELL":
- return "#dc2626";
- case "STOP_LOSS":
- return "#991b1b";
- case "HOLD":
- default:
- return "#ca8a04";
- }
-}
-
-function getAlertColor(level: AlertLevel): string {
- switch (level) {
- case "CRITICAL":
- return "#b91c1c";
- case "WARNING":
- return "#ca8a04";
- case "INFO":
- default:
- return "#2563eb";
- }
-}
-
/**
* StrategyCard
* ------------
- * Affiche la stratégie + décision (BUY/SELL/HOLD/STOP_LOSS)
- * + niveau d'alerte (CRITICAL/WARNING/INFO)
- * + confiance et raison.
+ * Affiche la décision (BUY/SELL/HOLD/STOP_LOSS),
+ * la justification, et la stratégie choisie par l'utilisateur.
*
- * Les enums sont affichés en badges ("pill") pour être lisibles sur mobile.
+ * On propose un bouton "Changer stratégie" qui envoie vers l'écran Strategy.
*/
-export default function StrategyCard({ summary }: Props) {
- const decisionColor = getDecisionColor(summary.decision);
- const alertColor = getAlertColor(summary.alertLevel);
+type Props = {
+ summary: DashboardSummary;
+ settings: UserSettings;
+ onGoStrategy: () => void;
+};
+export default function StrategyCard({ summary, settings, onGoStrategy }: Props) {
return (
<View style={ui.card}>
- <Text style={ui.title}>Stratégie</Text>
+ <Text style={ui.title}>Conseiller</Text>
- <Text style={ui.valueBold}>{summary.strategy}</Text>
+ {/* Décision principale */}
+ <Text style={ui.bigCenter}>{summary.decision}</Text>
- {/* Badge décision */}
- <View style={[ui.badge, { backgroundColor: `${decisionColor}22` }]}>
- <Text style={[ui.badgeText, { color: decisionColor }]}>
- {summary.decision}
- </Text>
- </View>
-
- {/* Badge niveau d'alerte */}
- <View style={[ui.badge, { backgroundColor: `${alertColor}22` }]}>
- <Text style={[ui.badgeText, { color: alertColor }]}>
- {summary.alertLevel}
- </Text>
- </View>
+ {/* Justification (centrée) */}
+ <Text style={[ui.muted, styles.centerText]}>
+ Pourquoi ? {summary.reason}
+ </Text>
- <View style={[ui.rowBetween, { marginTop: 10 }]}>
- <Text style={ui.value}>Confiance</Text>
- <Text style={ui.valueBold}>
- {(summary.confidence * 100).toFixed(1)} %
+ {/* Stratégie sélectionnée */}
+ <View style={{ marginTop: 10 }}>
+ <Text style={ui.muted}>
+ Stratégie sélectionnée :{" "}
+ <Text style={styles.boldInline}>{settings.selectedStrategyKey}</Text>
</Text>
</View>
- <Text style={[ui.muted, { marginTop: 8 }]}>{summary.reason}</Text>
+ {/* Bouton vers l'écran stratégie */}
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton]}
+ onPress={onGoStrategy}
+ >
+ <Text style={ui.buttonText}>Changer stratégie</Text>
+ </TouchableOpacity>
</View>
);
-}
\ No newline at end of file
+}
+
+const styles = StyleSheet.create({
+ centerText: {
+ textAlign: "center",
+ },
+
+ boldInline: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+
+ /**
+ * Important :
+ * ui.button est prévu pour une grille (ActionsCard) -> flexGrow + flexBasis.
+ * Ici on veut un bouton "plein largeur", donc on neutralise.
+ */
+ fullButton: {
+ flexGrow: 0,
+ flexBasis: "auto",
+ width: "100%",
+ marginTop: 12,
+ },
+});
\ No newline at end of file
-export const SERVER_URL = "http://192.168.129.121:3000";
\ No newline at end of file
+// ⚠️ En dev téléphone réel : URL = IP du PC sur le même réseau
+export const SERVER_URL = "http://192.168.129.121:3000";
+export const WS_URL = "ws://192.168.129.121:3000"; // optionnel, Socket.IO peut aussi utiliser http(s)
\ No newline at end of file
--- /dev/null
+import type { StrategyOption } from "../types/Strategy";
+
+/**
+ * Mock Strategies
+ * ---------------
+ * Step 2 : mono-user, mono-crypto, multi-stratégies.
+ * On garde une liste fixe, et plus tard on branchera l'API.
+ */
+export async function getStrategies(): Promise<StrategyOption[]> {
+ return [
+ {
+ key: "RSI_SIMPLE",
+ label: "RSI simple",
+ description: "BUY si RSI bas, SELL si RSI haut (version simplifiée).",
+ risk: "SAFE",
+ },
+ {
+ key: "MA_CROSS",
+ label: "Moyennes mobiles (cross)",
+ description: "BUY quand la moyenne courte croise au-dessus de la longue.",
+ risk: "NORMAL",
+ },
+ {
+ key: "MACD_BASIC",
+ label: "MACD basique",
+ description: "Analyse MACD simplifiée (signal/ligne).",
+ risk: "NORMAL",
+ },
+ {
+ key: "HOLD_ONLY",
+ label: "Hold only",
+ description: "Toujours HOLD (utile pour comparer / debug).",
+ risk: "SAFE",
+ },
+ ];
+}
\ No newline at end of file
-export type Currency = "EUR" | "USD";
+/**
+ * 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 AlertPreference =
- | "critical"
- | "warning"
- | "all";
+ // Paramètres UI / usage
+ currency: "EUR" | "USD";
+ favoriteSymbol: string;
-export type RefreshMode =
- | "manual"
- | "auto";
+ // Préférences alertes
+ alertPreference: "critical" | "all";
-export interface UserSettings {
- userId?: string; // ✅ pour Socket.IO (optionnel, Step 4 sera l'auth)
- currency: Currency;
- favoriteSymbol: string;
- alertPreference: AlertPreference;
- refreshMode: RefreshMode;
-}
+ // 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
--- /dev/null
+/**
+ * Wallet (Step 1)
+ * ---------------
+ * Mono-utilisateur, mono-crypto : on stocke seulement BTC.
+ * (Step 3 : on passera à plusieurs cryptos + valeur globale)
+ */
+export interface WalletState {
+ assetSymbol: "BTC";
+ quantity: number; // ex: 0.25
+ updatedAtMs: number;
+}
\ No newline at end of file
--- /dev/null
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert as RNAlert } from "react-native";
+import { useEffect, useMemo, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+import type { Alert } from "../types/Alert";
+import { alertStore } from "../services/alertStore";
+import { ui } from "../components/ui/uiStyles";
+
+/**
+ * Types locaux (évite dépendre d'exports qui ne sont pas toujours présents)
+ */
+type AlertLevel = "CRITICAL" | "WARNING" | "INFO";
+type TradeDecision = "BUY" | "SELL" | "HOLD" | "STOP_LOSS";
+
+type AlertFilter = "ALL" | AlertLevel;
+
+function levelRank(level: AlertLevel) {
+ switch (level) {
+ case "CRITICAL":
+ return 3;
+ case "WARNING":
+ return 2;
+ case "INFO":
+ default:
+ return 1;
+ }
+}
+
+function getLevelColor(level: AlertLevel): string {
+ switch (level) {
+ case "CRITICAL":
+ return "#dc2626";
+ case "WARNING":
+ return "#ca8a04";
+ case "INFO":
+ default:
+ return "#2563eb";
+ }
+}
+
+function getActionColor(action: TradeDecision): string {
+ switch (action) {
+ case "BUY":
+ return "#16a34a";
+ case "SELL":
+ return "#dc2626";
+ case "STOP_LOSS":
+ return "#991b1b";
+ case "HOLD":
+ default:
+ return "#ca8a04";
+ }
+}
+
+/**
+ * AlertsScreen
+ * ------------
+ * Affiche les alertes reçues via Socket.IO (stockées dans alertStore).
+ *
+ * - Filtre optionnel (ALL / CRITICAL / WARNING / INFO)
+ * - Par défaut : ALL avec tri CRITICAL > WARNING > INFO puis plus récent
+ * - Bouton Clear avec confirmation
+ */
+export default function AlertsScreen() {
+ const [alerts, setAlerts] = useState<Alert[]>([]);
+ const [filter, setFilter] = useState<AlertFilter>("ALL");
+
+ // abonnement au store (temps réel)
+ useEffect(() => {
+ const unsub = alertStore.subscribe(setAlerts);
+ return () => {
+ unsub();
+ };
+ }, []);
+
+ const filteredAndSorted = useMemo(() => {
+ const filtered =
+ filter === "ALL"
+ ? alerts
+ : alerts.filter((a) => (a.alertLevel as AlertLevel) === filter);
+
+ return [...filtered].sort((a, b) => {
+ const la = a.alertLevel as AlertLevel;
+ const lb = b.alertLevel as AlertLevel;
+
+ // tri par niveau d'alerte (desc)
+ const byLevel = levelRank(lb) - levelRank(la);
+ if (byLevel !== 0) return byLevel;
+
+ // tri par date (desc) si dispo
+ const ta = a.timestamp ?? 0;
+ const tb = b.timestamp ?? 0;
+ return tb - ta;
+ });
+ }, [alerts, filter]);
+
+ const confirmClear = () => {
+ if (alerts.length === 0) return;
+
+ RNAlert.alert(
+ "Supprimer les alertes ?",
+ "Cette action efface la liste locale des alertes reçues (cela ne touche pas la base de données).",
+ [
+ { text: "Annuler", style: "cancel" },
+ {
+ text: "Supprimer",
+ style: "destructive",
+ onPress: () => alertStore.clear(),
+ },
+ ]
+ );
+ };
+
+ const FilterButton = ({ value, label }: { value: AlertFilter; label: string }) => {
+ const active = filter === value;
+ return (
+ <TouchableOpacity
+ style={[
+ styles.filterBtn,
+ active ? styles.filterBtnActive : styles.filterBtnInactive,
+ ]}
+ onPress={() => setFilter(value)}
+ >
+ <Text style={[styles.filterText, active && styles.filterTextActive]}>
+ {label}
+ </Text>
+ </TouchableOpacity>
+ );
+ };
+
+ return (
+ <SafeAreaView style={styles.safeArea}>
+ <FlatList
+ contentContainerStyle={ui.container}
+ data={filteredAndSorted}
+ keyExtractor={(_, idx) => `alert-${idx}`}
+ ListHeaderComponent={
+ <View style={ui.card}>
+ <View style={styles.headerRow}>
+ <Text style={ui.title}>Alertes</Text>
+
+ <TouchableOpacity
+ style={[styles.clearBtn, alerts.length === 0 && styles.clearBtnDisabled]}
+ onPress={confirmClear}
+ disabled={alerts.length === 0}
+ >
+ <Text style={styles.clearBtnText}>Clear</Text>
+ </TouchableOpacity>
+ </View>
+
+ <Text style={ui.muted}>
+ Filtre : {filter === "ALL" ? "Toutes" : filter} — Total : {alerts.length}
+ </Text>
+
+ <View style={styles.filtersRow}>
+ <FilterButton value="ALL" label="Toutes" />
+ <FilterButton value="CRITICAL" label="Critical" />
+ <FilterButton value="WARNING" label="Warning" />
+ <FilterButton value="INFO" label="Info" />
+ </View>
+
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Tri par défaut : CRITICAL > WARNING > INFO, puis plus récent.
+ </Text>
+ </View>
+ }
+ ListEmptyComponent={
+ <View style={ui.card}>
+ <Text style={ui.title}>Aucune alerte</Text>
+ <Text style={ui.muted}>En attente d’alertes Socket.IO…</Text>
+ </View>
+ }
+ renderItem={({ item }) => {
+ const lvl = item.alertLevel as AlertLevel;
+ const act = item.action as TradeDecision;
+
+ const lvlColor = getLevelColor(lvl);
+ const actColor = getActionColor(act);
+
+ return (
+ <View style={ui.card}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.valueBold}>{item.pair}</Text>
+ <Text style={ui.muted}>
+ {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"}
+ </Text>
+ </View>
+
+ <View style={{ flexDirection: "row", gap: 8, flexWrap: "wrap", marginTop: 10 }}>
+ <View style={[ui.badge, { backgroundColor: `${lvlColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: lvlColor }]}>{lvl}</Text>
+ </View>
+
+ <View style={[ui.badge, { backgroundColor: `${actColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: actColor }]}>{act}</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>
+ </View>
+
+ {typeof item.price === "number" && (
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Prix</Text>
+ <Text style={ui.valueBold}>{item.price.toFixed(2)}</Text>
+ </View>
+ )}
+
+ <Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
+ </View>
+ );
+ }}
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: ui.screen.backgroundColor,
+ },
+
+ headerRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+
+ clearBtn: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ backgroundColor: "#fff",
+ },
+ clearBtnDisabled: {
+ opacity: 0.5,
+ },
+ clearBtnText: {
+ fontWeight: "900",
+ color: "#dc2626",
+ },
+
+ filtersRow: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: 8,
+ marginTop: 10,
+ },
+ filterBtn: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 999,
+ borderWidth: 1,
+ },
+ filterBtnInactive: {
+ borderColor: "#e5e7eb",
+ backgroundColor: "#fff",
+ },
+ filterBtnActive: {
+ borderColor: "#14532D",
+ backgroundColor: "#14532D22",
+ },
+ filterText: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+ filterTextActive: {
+ color: "#14532D",
+ },
+});
\ No newline at end of file
-import { View, Text, StyleSheet, ScrollView } from "react-native";
-import { useState, useCallback, useEffect } from "react";
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ useWindowDimensions,
+} from "react-native";
+import { useState, useCallback, useEffect, useMemo } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
+import { Ionicons } from "@expo/vector-icons";
import type { DashboardSummary } from "../types/DashboardSummary";
import { fetchDashboardSummary } from "../services/dashboardService";
import { loadSettings } from "../utils/settingsStorage";
import type { UserSettings } from "../models/UserSettings";
-import MarketCard from "../components/MarketCard";
-import StrategyCard from "../components/StrategyCard";
-import WalletCard from "../components/WalletCard";
-import ActionsCard from "../components/ActionsCard";
-
import { ui } from "../components/ui/uiStyles";
-// ✅ Socket.IO
import { socketService } from "../services/socketService";
import { SERVER_URL } from "../config/env";
import type { Alert } from "../types/Alert";
+import { alertStore } from "../services/alertStore";
+import { showAlertNotification } from "../services/notificationService";
+
+import { loadWallet } from "../utils/walletStorage";
+import type { WalletState } from "../models/Wallet";
/**
- * DashboardScreen
- * ----------------
- * Écran principal mobile.
- * - Charge Dashboard + Settings
- * - Refresh auto si activé
- * - Socket.IO optionnel (non bloquant) pour alertes live
- * - UI cohérente via uiStyles/theme
+ * DashboardScreen (WF-01) — Responsive + No-scroll goal
+ * ----------------------------------------------------
+ * - Cartes cliquables (chevron subtil) :
+ * * Portefeuille -> Wallet
+ * * Urgence -> Alertes
+ * * Prix BTC -> Historique
+ * - Conseiller : bouton -> Stratégie
+ * - Socket.IO non bloquant + notifications locales
*/
export default function DashboardScreen() {
+ const { height } = useWindowDimensions();
+ const compact = height < 760;
+
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
- const [loading, setLoading] = useState<boolean>(true);
+ const [wallet, setWallet] = useState<WalletState | null>(null);
- // erreurs REST/refresh
+ const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
- // Socket state (non bloquant)
const [socketConnected, setSocketConnected] = useState<boolean>(false);
const [socketError, setSocketError] = useState<string | null>(null);
+
const [liveAlerts, setLiveAlerts] = useState<Alert[]>([]);
+ const [lastRefreshMs, setLastRefreshMs] = useState<number | null>(null);
+ const [refreshing, setRefreshing] = useState<boolean>(false);
+
const navigation = useNavigation();
- /**
- * Chargement initial + rechargement quand on revient sur l'écran
- */
useFocusEffect(
useCallback(() => {
let isActive = true;
setError(null);
setLoading(true);
- const [dashboardData, userSettings] = await Promise.all([
+ const [dashboardData, userSettings, walletData] = await Promise.all([
fetchDashboardSummary(),
loadSettings(),
+ loadWallet(),
]);
- if (isActive) {
- setSummary(dashboardData);
- setSettings(userSettings);
- }
+ if (!isActive) return;
+
+ setSummary(dashboardData);
+ setSettings(userSettings);
+ setWallet(walletData);
+ setLastRefreshMs(Date.now());
} catch {
if (isActive) setError("Impossible de charger le dashboard.");
} finally {
}, [])
);
- /**
- * Refresh auto (sans clignotement global)
- */
useEffect(() => {
if (!settings) return;
if (settings.refreshMode !== "auto") return;
const intervalId = setInterval(async () => {
try {
const data = await fetchDashboardSummary();
- if (!cancelled) setSummary(data);
+ if (!cancelled) {
+ setSummary(data);
+ setLastRefreshMs(Date.now());
+ }
} catch {
if (!cancelled) setError("Erreur lors du rafraîchissement automatique.");
}
};
}, [settings]);
- /**
- * Socket.IO (non bloquant)
- * - Si userId absent => on désactive socket proprement.
- * - Si serveur pas prêt => connect_error timeout => on affiche un message, sans crasher.
- */
+ const handleManualRefresh = async () => {
+ try {
+ setRefreshing(true);
+ const data = await fetchDashboardSummary();
+ setSummary(data);
+ setLastRefreshMs(Date.now());
+ setError(null);
+ } catch {
+ setError("Erreur lors de l'actualisation.");
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
useEffect(() => {
if (!settings) return;
setSocketError(null);
- // ✅ userId optionnel pour Step 1/2/3 (pas d'auth)
- const userId = settings.userId;
-
- if (!userId) {
- setSocketConnected(false);
- setSocketError("Socket désactivé : userId absent.");
- return;
- }
+ const userId = "test-user";
try {
socketService.connect(SERVER_URL, userId);
}
const unsubscribeAlert = socketService.onAlert((alert) => {
- setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
+ alertStore.add(alert);
+ setLiveAlerts((prev) => [alert, ...prev].slice(0, 100));
+
+ if (settings.notificationsEnabled) {
+ void (async () => {
+ try {
+ await showAlertNotification(alert);
+ } catch (e) {
+ console.log("⚠️ Notification error:", e);
+ }
+ })();
+ }
});
socketService.ping();
};
}, [settings]);
- // Chargement
+ const urgentAlert: Alert | null = useMemo(() => {
+ if (liveAlerts.length === 0) return null;
+
+ const critical = liveAlerts.filter((a) => a.alertLevel === "CRITICAL");
+ if (critical.length > 0) return critical[0];
+
+ const warning = liveAlerts.filter((a) => a.alertLevel === "WARNING");
+ if (warning.length > 0) return warning[0];
+
+ return liveAlerts[0];
+ }, [liveAlerts]);
+
+ const walletTotalValue = useMemo(() => {
+ if (!wallet || !summary) return null;
+ return wallet.quantity * summary.price;
+ }, [wallet, summary]);
+
if (loading) {
return (
<View style={ui.centered}>
);
}
- // Erreur bloquante (si rien à afficher)
if (error && (!summary || !settings)) {
return (
<View style={ui.centered}>
);
}
- // Sécurité
- if (!summary || !settings) {
+ if (!summary || !settings || !wallet) {
return (
<View style={ui.centered}>
<Text>Initialisation…</Text>
);
}
+ const Chevron = () => (
+ <Ionicons name="chevron-forward-outline" size={18} color="#0f172a" style={{ opacity: 0.35 }} />
+ );
+
return (
<SafeAreaView style={styles.safeArea}>
- <ScrollView contentContainerStyle={ui.container}>
- {/* Bannière warning REST/refresh (non bloquant) */}
+ <ScrollView
+ contentContainerStyle={[ui.container, compact && styles.containerCompact]}
+ showsVerticalScrollIndicator={false}
+ >
{error && (
- <View style={[ui.banner, styles.bannerWarning]}>
- <Text style={[ui.bannerText, styles.bannerWarningText]}>{error}</Text>
+ <View style={[ui.banner, styles.bannerWarning, compact && styles.bannerCompact]}>
+ <Text style={[ui.bannerText, styles.bannerWarningText]} numberOfLines={2}>
+ {error}
+ </Text>
</View>
)}
- {/* Bannière Socket (non bloquant) */}
- <View style={[ui.banner, styles.bannerSocket]}>
- <Text style={[ui.bannerText, styles.socketTitle]}>
- Socket.IO : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
+ {/* 1) CONSEILLER */}
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Conseiller</Text>
+
+ <Text style={[ui.bigCenter, compact && styles.bigCompact]}>{summary.decision}</Text>
+
+ <Text style={[ui.muted, styles.centerText]} numberOfLines={compact ? 2 : 3}>
+ Pourquoi ? {summary.reason}
</Text>
- {!!socketError && <Text style={ui.muted}>{socketError}</Text>}
- <Text style={[ui.muted, { marginTop: 4 }]}>
- Alertes reçues : {liveAlerts.length}
+ <Text style={[ui.muted, { marginTop: compact ? 6 : 10 }]} numberOfLines={1}>
+ Stratégie : <Text style={styles.boldInline}>{settings.selectedStrategyKey}</Text>
</Text>
- </View>
- <MarketCard summary={summary} settings={settings} />
- <StrategyCard summary={summary} />
- <WalletCard settings={settings} />
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, compact && styles.buttonCompact]}
+ onPress={() => navigation.navigate("Strategy" as never)}
+ >
+ <Text style={ui.buttonText}>Sélectionner stratégie</Text>
+ </TouchableOpacity>
+ </View>
- <ActionsCard
- onGoSettings={() => navigation.navigate("Settings" as never)}
- onGoHistory={() => navigation.navigate("History" as never)}
- />
+ {/* 2) PORTEFEUILLE — cliquable => Wallet */}
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Wallet" as never)}>
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <View style={styles.headerRow}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Portefeuille</Text>
+ <Chevron />
+ </View>
+
+ <View style={ui.rowBetween}>
+ <Text style={ui.value}>Quantité BTC :</Text>
+ <Text style={ui.valueBold}>{wallet.quantity.toFixed(6)} BTC</Text>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Valeur Totale :</Text>
+ <Text style={ui.valueBold}>
+ {walletTotalValue !== null ? `${walletTotalValue.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </View>
+
+ <Text style={[ui.muted, { marginTop: 6 }]} numberOfLines={1}>
+ BTC @ {summary.price.toFixed(2)} {settings.currency}
+ </Text>
+ </View>
+ </TouchableOpacity>
+
+ {/* 3) URGENCE — cliquable => Alertes */}
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("Alerts" as never)}>
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <View style={styles.headerRow}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Urgence</Text>
+ <Chevron />
+ </View>
+
+ {urgentAlert ? (
+ <View style={[styles.urgentBox, compact && styles.urgentBoxCompact]}>
+ <Text style={styles.urgentTitle} numberOfLines={2}>
+ {urgentAlert.alertLevel} : {urgentAlert.reason}
+ </Text>
+ <Text style={ui.muted} numberOfLines={1}>
+ {urgentAlert.action} {urgentAlert.pair} — {(urgentAlert.confidence * 100).toFixed(0)}%
+ </Text>
+ </View>
+ ) : (
+ <Text style={ui.muted}>Aucune alerte pour le moment.</Text>
+ )}
+
+ <Text style={[ui.muted, { marginTop: compact ? 6 : 8 }]} numberOfLines={1}>
+ Socket : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
+ {socketError ? ` — ${socketError}` : ""}
+ </Text>
+ </View>
+ </TouchableOpacity>
+
+ {/* 4) PRIX BTC — cliquable => Historique */}
+ <TouchableOpacity activeOpacity={0.85} onPress={() => navigation.navigate("History" as never)}>
+ <View style={[ui.card, compact && styles.cardCompact]}>
+ <View style={styles.priceHeaderRow}>
+ <View style={styles.headerRow}>
+ <Text style={[ui.title, compact && styles.titleCompact]}>Prix BTC</Text>
+ <Chevron />
+ </View>
+
+ <TouchableOpacity
+ style={[styles.refreshBtn, refreshing && styles.refreshBtnDisabled, compact && styles.refreshBtnCompact]}
+ onPress={handleManualRefresh}
+ disabled={refreshing}
+ >
+ <Text style={styles.refreshBtnText}>{refreshing ? "…" : "Actualiser"}</Text>
+ </TouchableOpacity>
+ </View>
+
+ <Text style={ui.muted} numberOfLines={1}>
+ Dernière maj : {lastRefreshMs ? new Date(lastRefreshMs).toLocaleTimeString() : "—"}
+ </Text>
+
+ <View style={[styles.priceCard, compact && styles.priceCardCompact]}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.value}>Prix BTC</Text>
+ <Text style={styles.priceBig}>
+ {summary.price.toFixed(2)} {settings.currency}
+ </Text>
+ </View>
+ </View>
+ </View>
+ </TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
backgroundColor: ui.screen.backgroundColor,
},
- errorText: {
- color: "#dc2626",
- fontWeight: "900",
+ containerCompact: {
+ padding: 12,
+ },
+
+ cardCompact: {
+ padding: 12,
+ marginBottom: 10,
+ },
+
+ titleCompact: {
+ marginBottom: 6,
+ },
+
+ bigCompact: {
+ fontSize: 24,
+ marginVertical: 4,
},
bannerWarning: {
bannerWarningText: {
color: "#ca8a04",
},
+ bannerCompact: {
+ padding: 10,
+ marginBottom: 10,
+ },
+
+ errorText: {
+ color: "#dc2626",
+ fontWeight: "900",
+ },
+
+ centerText: {
+ textAlign: "center",
+ },
+
+ boldInline: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+
+ fullButton: {
+ flexGrow: 0,
+ flexBasis: "auto",
+ width: "100%",
+ marginTop: 12,
+ },
+ buttonCompact: {
+ paddingVertical: 10,
+ marginTop: 10,
+ },
- bannerSocket: {
- borderColor: "#cbd5e1",
+ // ✅ header row + chevron icon
+ headerRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
},
- socketTitle: {
+
+ urgentBox: {
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ borderRadius: 10,
+ padding: 10,
+ backgroundColor: "#ffffff",
+ },
+ urgentBoxCompact: {
+ padding: 8,
+ },
+ urgentTitle: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+
+ priceHeaderRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 8,
+ },
+
+ refreshBtn: {
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderRadius: 10,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ backgroundColor: "#fff",
+ },
+ refreshBtnCompact: {
+ paddingVertical: 6,
+ },
+ refreshBtnDisabled: {
+ opacity: 0.6,
+ },
+ refreshBtnText: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+
+ priceCard: {
+ marginTop: 10,
+ borderWidth: 1,
+ borderColor: "#e5e7eb",
+ borderRadius: 10,
+ padding: 12,
+ backgroundColor: "#ffffff",
+ },
+ priceCardCompact: {
+ paddingVertical: 10,
+ },
+ priceBig: {
+ fontSize: 22,
+ fontWeight: "900",
color: "#0f172a",
},
});
\ No newline at end of file
if (loading) {
return (
- <View style={styles.centered}>
+ <View style={ui.centered}>
<Text>Chargement de l'historique…</Text>
</View>
);
if (error) {
return (
- <View style={styles.centered}>
+ <View style={ui.centered}>
<Text style={styles.errorText}>{error}</Text>
</View>
);
return (
<SafeAreaView style={styles.safeArea}>
<FlatList
- contentContainerStyle={styles.container}
+ contentContainerStyle={ui.container}
data={items}
keyExtractor={(it) => it.signalId}
ListEmptyComponent={
</View>
{/* Badges */}
- <View style={{ flexDirection: "row", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
- <View style={[ui.badge, { backgroundColor: `${actionColor}22`, marginTop: 0 }]}>
- <Text style={[ui.badgeText, { color: actionColor }]}>{item.action}</Text>
+ <View
+ style={{
+ flexDirection: "row",
+ gap: 8,
+ marginTop: 10,
+ flexWrap: "wrap",
+ }}
+ >
+ <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 }]}>
flex: 1,
backgroundColor: ui.screen.backgroundColor,
},
- container: {
- padding: 16,
- },
- centered: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: ui.screen.backgroundColor,
- },
errorText: {
color: "#dc2626",
fontWeight: "800",
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { useEffect, useState } from "react";
-import { loadSettings, saveSettings } from "../utils/settingsStorage";
-import { UserSettings } from "../models/UserSettings";
import { SafeAreaView } from "react-native-safe-area-context";
+import { loadSettings, saveSettings } from "../utils/settingsStorage";
+import type { UserSettings } from "../models/UserSettings";
+
+import { requestNotificationPermission } from "../services/notificationService";
+import { ui } from "../components/ui/uiStyles";
+
export default function SettingsScreen() {
const [settings, setSettings] = useState<UserSettings | null>(null);
+ const [infoMessage, setInfoMessage] = useState<string | null>(null);
useEffect(() => {
async function init() {
if (!settings) {
return (
- <View style={styles.container}>
+ <View style={ui.centered}>
<Text>Chargement des paramètres…</Text>
</View>
);
setSettings({ ...settings, refreshMode: newMode });
};
+ const toggleNotifications = async () => {
+ setInfoMessage(null);
+
+ if (settings.notificationsEnabled) {
+ setSettings({ ...settings, notificationsEnabled: false });
+ return;
+ }
+
+ const granted = await requestNotificationPermission();
+
+ if (!granted) {
+ setInfoMessage("Permission refusée : notifications désactivées.");
+ setSettings({ ...settings, notificationsEnabled: false });
+ return;
+ }
+
+ setInfoMessage("Notifications activées ✅ (pense à sauvegarder)");
+ setSettings({ ...settings, notificationsEnabled: true });
+ };
+
const handleSave = async () => {
await saveSettings(settings);
+ setInfoMessage("Paramètres sauvegardés ✅");
};
return (
<SafeAreaView style={styles.safeArea}>
- <View style={styles.container}>
- <Text style={styles.title}>Paramètres</Text>
+ <View style={ui.container}>
+ <Text style={styles.screenTitle}>Paramètres</Text>
- <Text>Devise : {settings.currency}</Text>
- <TouchableOpacity style={styles.button} onPress={toggleCurrency}>
- <Text style={styles.buttonText}>Changer devise</Text>
- </TouchableOpacity>
+ {/* Carte : Devise */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Devise</Text>
+ <Text style={ui.value}>Actuelle : {settings.currency}</Text>
- <Text>Mode rafraîchissement : {settings.refreshMode}</Text>
- <TouchableOpacity style={styles.button} onPress={toggleRefreshMode}>
- <Text style={styles.buttonText}>Changer mode</Text>
- </TouchableOpacity>
+ <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleCurrency}>
+ <Text style={ui.buttonText}>Changer devise</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 style={[ui.button, styles.fullButton]} onPress={toggleRefreshMode}>
+ <Text style={ui.buttonText}>Changer mode</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>
+
+ <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={toggleNotifications}>
+ <Text style={ui.buttonText}>
+ {settings.notificationsEnabled ? "Désactiver" : "Activer"} les notifications
+ </Text>
+ </TouchableOpacity>
- <TouchableOpacity style={styles.saveButton} onPress={handleSave}>
- <Text style={styles.buttonText}>Sauvegarder</Text>
+ {!!infoMessage && <Text style={styles.infoText}>{infoMessage}</Text>}
+ </View>
+
+ {/* Bouton Save */}
+ <TouchableOpacity style={[ui.button, styles.fullButton, styles.saveButton]} onPress={handleSave}>
+ <Text style={ui.buttonText}>Sauvegarder</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
const styles = StyleSheet.create({
safeArea: {
flex: 1,
- backgroundColor: "#fff",
- },
- container: {
- padding: 16,
+ backgroundColor: ui.screen.backgroundColor,
},
- title: {
+ screenTitle: {
fontSize: 22,
- fontWeight: "bold",
- marginBottom: 16,
+ fontWeight: "900",
+ marginBottom: 12,
+ color: "#0f172a",
},
- button: {
- backgroundColor: "#555",
- padding: 10,
- marginVertical: 6,
- borderRadius: 4,
+ infoText: {
+ marginTop: 10,
+ opacity: 0.8,
},
- saveButton: {
- backgroundColor: "#2563eb",
- padding: 12,
- marginTop: 20,
- borderRadius: 6,
+
+ /**
+ * fullButton
+ * ----------
+ * On neutralise les propriétés "grille" de ui.button (flexGrow/flexBasis),
+ * car elles sont utiles dans ActionsCard, mais pas dans Settings.
+ */
+ fullButton: {
+ flexGrow: 0,
+ flexBasis: "auto",
+ width: "100%",
+ marginTop: 10,
},
- buttonText: {
- color: "#fff",
- fontWeight: "bold",
- textAlign: "center",
+
+ saveButton: {
+ marginBottom: 10,
},
-});
+});
\ No newline at end of file
--- /dev/null
+import { View, Text, StyleSheet, TouchableOpacity, FlatList } from "react-native";
+import { useEffect, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+import { ui } from "../components/ui/uiStyles";
+import { loadSettings, saveSettings } 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.
+ */
+export default function StrategyScreen() {
+ const [settings, setSettings] = useState<UserSettings | null>(null);
+ const [strategies, setStrategies] = useState<StrategyOption[]>([]);
+ const [loading, setLoading] = useState<boolean>(true);
+ const [info, setInfo] = useState<string | null>(null);
+
+ useEffect(() => {
+ let active = true;
+
+ async function init() {
+ try {
+ setLoading(true);
+ const [s, list] = await Promise.all([loadSettings(), fetchStrategies()]);
+ if (!active) return;
+
+ setSettings(s);
+ setStrategies(list);
+ } finally {
+ if (active) setLoading(false);
+ }
+ }
+
+ init();
+
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ if (loading || !settings) {
+ return (
+ <View style={ui.centered}>
+ <Text>Chargement des stratégies…</Text>
+ </View>
+ );
+ }
+
+ 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);
+
+ setInfo(`Stratégie sélectionnée : ${key}`);
+ };
+
+ return (
+ <SafeAreaView style={styles.safeArea}>
+ <FlatList
+ contentContainerStyle={ui.container}
+ data={strategies}
+ keyExtractor={(it) => it.key}
+ ListHeaderComponent={
+ <View style={ui.card}>
+ <Text style={ui.title}>Stratégie</Text>
+ <Text style={ui.muted}>
+ Choisis une stratégie. Elle sera affichée sur le Dashboard.
+ </Text>
+
+ <Text style={[ui.muted, { marginTop: 8 }]}>
+ Actuelle : <Text style={styles.boldInline}>{settings.selectedStrategyKey}</Text>
+ </Text>
+
+ {!!info && <Text style={[ui.muted, { marginTop: 8 }]}>{info}</Text>}
+ </View>
+ }
+ renderItem={({ item }) => (
+ <View style={[ui.card, isSelected(item.key) && styles.cardSelected]}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.valueBold}>{item.label}</Text>
+ <Text style={styles.riskTag}>{item.risk}</Text>
+ </View>
+
+ <Text style={[ui.muted, { marginTop: 8 }]}>{item.description}</Text>
+
+ <TouchableOpacity
+ style={[ui.button, styles.fullButton, isSelected(item.key) && styles.btnSelected]}
+ onPress={() => handleSelect(item.key)}
+ >
+ <Text style={ui.buttonText}>
+ {isSelected(item.key) ? "Sélectionnée ✅" : "Sélectionner"}
+ </Text>
+ </TouchableOpacity>
+ </View>
+ )}
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: ui.screen.backgroundColor,
+ },
+
+ boldInline: {
+ fontWeight: "900",
+ color: "#0f172a",
+ },
+
+ fullButton: {
+ flexGrow: 0,
+ flexBasis: "auto",
+ width: "100%",
+ marginTop: 12,
+ },
+
+ cardSelected: {
+ borderColor: "#16a34a",
+ },
+
+ btnSelected: {
+ opacity: 0.9,
+ },
+
+ riskTag: {
+ fontWeight: "900",
+ opacity: 0.75,
+ },
+});
\ No newline at end of file
--- /dev/null
+import { View, Text, StyleSheet, TouchableOpacity, TextInput, Alert as RNAlert } from "react-native";
+import { useMemo, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useFocusEffect } from "@react-navigation/native";
+import { useCallback } from "react";
+
+import { ui } from "../components/ui/uiStyles";
+import { loadWallet, saveWallet, clearWallet } from "../utils/walletStorage";
+import type { WalletState } from "../models/Wallet";
+import { fetchDashboardSummary } from "../services/dashboardService";
+import { loadSettings } from "../utils/settingsStorage";
+import type { UserSettings } from "../models/UserSettings";
+
+/**
+ * WalletScreen (WF-03 Step 1)
+ * ---------------------------
+ * Mono-utilisateur / mono-crypto : BTC uniquement.
+ * - L'utilisateur encode la quantité de BTC qu'il possède
+ * - On calcule la valeur estimée via le prix BTC du dashboard
+ * - Stockage local (AsyncStorage) pour ne pas dépendre de l'API
+ */
+export default function WalletScreen() {
+ const [wallet, setWallet] = useState<WalletState | null>(null);
+ const [settings, setSettings] = useState<UserSettings | null>(null);
+
+ // Prix BTC actuel (mock aujourd'hui, API demain)
+ const [btcPrice, setBtcPrice] = useState<number | null>(null);
+
+ // input texte (évite les bugs de virgule/points)
+ const [qtyInput, setQtyInput] = useState<string>("0");
+
+ const [info, setInfo] = useState<string | null>(null);
+
+ // Recharge quand l’écran reprend le focus (retour depuis autre page)
+ useFocusEffect(
+ useCallback(() => {
+ let active = true;
+
+ async function init() {
+ setInfo(null);
+
+ const [w, s, dash] = await Promise.all([
+ loadWallet(),
+ loadSettings(),
+ fetchDashboardSummary(),
+ ]);
+
+ if (!active) return;
+
+ setWallet(w);
+ setSettings(s);
+ setBtcPrice(dash.price);
+
+ setQtyInput(String(w.quantity));
+ }
+
+ init();
+
+ return () => {
+ active = false;
+ };
+ }, [])
+ );
+
+ 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 totalValue = useMemo(() => {
+ if (parsedQty === null || btcPrice === null) return null;
+ return parsedQty * btcPrice;
+ }, [parsedQty, btcPrice]);
+
+ const lastUpdatedLabel = useMemo(() => {
+ if (!wallet) return "—";
+ return new Date(wallet.updatedAtMs).toLocaleString();
+ }, [wallet]);
+
+ const handleSave = async () => {
+ if (!wallet) return;
+
+ if (parsedQty === null) {
+ setInfo("Quantité invalide. Exemple : 0.25");
+ return;
+ }
+
+ const updated: WalletState = {
+ ...wallet,
+ quantity: parsedQty,
+ updatedAtMs: Date.now(),
+ };
+
+ await saveWallet(updated);
+ setWallet(updated);
+ setInfo("Portefeuille sauvegardé ✅");
+ };
+
+ const handleClear = () => {
+ RNAlert.alert(
+ "Réinitialiser le portefeuille ?",
+ "Cela remet la quantité BTC à 0 (stockage local).",
+ [
+ { text: "Annuler", style: "cancel" },
+ {
+ text: "Réinitialiser",
+ style: "destructive",
+ onPress: async () => {
+ await clearWallet();
+ const fresh = await loadWallet();
+ setWallet(fresh);
+ setQtyInput("0");
+ setInfo("Portefeuille réinitialisé ✅");
+ },
+ },
+ ]
+ );
+ };
+
+ if (!wallet || !settings) {
+ return (
+ <View style={ui.centered}>
+ <Text>Chargement du portefeuille…</Text>
+ </View>
+ );
+ }
+
+ return (
+ <SafeAreaView style={styles.safeArea}>
+ <View style={ui.container}>
+ <Text style={styles.screenTitle}>Portefeuille</Text>
+
+ {/* Carte BTC */}
+ <View style={ui.card}>
+ <Text style={ui.title}>BTC</Text>
+
+ <Text style={ui.muted}>Quantité détenue</Text>
+
+ <TextInput
+ value={qtyInput}
+ onChangeText={setQtyInput}
+ keyboardType="decimal-pad"
+ placeholder="ex: 0.25"
+ style={styles.input}
+ />
+
+ <Text style={[ui.muted, { marginTop: 10 }]}>
+ Prix BTC actuel :{" "}
+ <Text style={styles.boldInline}>
+ {btcPrice !== null ? `${btcPrice.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </Text>
+
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Valeur estimée :{" "}
+ <Text style={styles.boldInline}>
+ {totalValue !== null ? `${totalValue.toFixed(2)} ${settings.currency}` : "—"}
+ </Text>
+ </Text>
+
+ {/* ✅ Dernière mise à jour */}
+ <Text style={[ui.muted, { marginTop: 6 }]}>
+ Dernière mise à jour :{" "}
+ <Text style={styles.boldInline}>{lastUpdatedLabel}</Text>
+ </Text>
+
+ <TouchableOpacity style={[ui.button, styles.fullButton]} onPress={handleSave}>
+ <Text style={ui.buttonText}>Enregistrer</Text>
+ </TouchableOpacity>
+
+ <TouchableOpacity style={styles.secondaryButton} onPress={handleClear}>
+ <Text style={styles.secondaryButtonText}>Réinitialiser</Text>
+ </TouchableOpacity>
+
+ {!!info && <Text style={[ui.muted, { marginTop: 10 }]}>{info}</Text>}
+ </View>
+
+ {/* Carte info Step */}
+ <View style={ui.card}>
+ <Text style={ui.title}>Step 1</Text>
+ <Text style={ui.muted}>
+ Mono-utilisateur / mono-crypto (BTC). Step 3 : portefeuille multi-cryptos + valeur globale.
+ </Text>
+ </View>
+ </View>
+ </SafeAreaView>
+ );
+}
+
+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",
+ },
+});
\ No newline at end of file
--- /dev/null
+import type { Alert } from "../types/Alert";
+
+type Listener = (alerts: Alert[]) => void;
+
+class AlertStore {
+ private alerts: Alert[] = [];
+ private listeners = new Set<Listener>();
+
+ add(alert: Alert) {
+ this.alerts = [alert, ...this.alerts].slice(0, 200);
+ this.emit();
+ }
+
+ getAll() {
+ return this.alerts;
+ }
+
+ subscribe(listener: Listener) {
+ this.listeners.add(listener);
+ listener(this.alerts);
+
+ // ✅ cleanup doit retourner void (pas boolean)
+ return () => {
+ this.listeners.delete(listener);
+ };
+ }
+
+ clear() {
+ this.alerts = [];
+ this.emit();
+ }
+
+ private emit() {
+ for (const l of this.listeners) l(this.alerts);
+ }
+}
+
+export const alertStore = new AlertStore();
\ No newline at end of file
--- /dev/null
+import * as Notifications from "expo-notifications";
+import { Platform } from "react-native";
+import type { Alert } from "../types/Alert";
+
+/**
+ * Notification handler (Foreground)
+ * ---------------------------------
+ * Sur certaines versions, Expo demande aussi shouldShowBanner/shouldShowList.
+ * On les met explicitement pour éviter les erreurs TypeScript.
+ */
+Notifications.setNotificationHandler({
+ handleNotification: async () => ({
+ shouldShowAlert: true,
+ shouldPlaySound: true,
+ shouldSetBadge: false,
+
+ // ✅ champs demandés par les versions récentes
+ shouldShowBanner: true,
+ shouldShowList: true,
+ }),
+});
+
+/**
+ * Initialisation Android (channel)
+ * --------------------------------
+ * Sur Android, un channel "alerts" permet d'avoir une importance HIGH.
+ */
+export async function initNotificationChannel(): Promise<void> {
+ if (Platform.OS !== "android") return;
+
+ await Notifications.setNotificationChannelAsync("alerts", {
+ name: "Alertes",
+ importance: Notifications.AndroidImportance.HIGH,
+ sound: "default",
+ vibrationPattern: [0, 250, 250, 250],
+ lockscreenVisibility: Notifications.AndroidNotificationVisibility.PUBLIC,
+ });
+}
+
+function formatTitle(alert: Alert): string {
+ return `${alert.alertLevel} — ${alert.action} ${alert.pair}`;
+}
+
+function formatBody(alert: Alert): string {
+ const conf = Math.round(alert.confidence * 100);
+ return `${conf}% — ${alert.reason}`;
+}
+
+/**
+ * Demande de permission
+ * ---------------------
+ * Retourne true si l'utilisateur accepte.
+ */
+export async function requestNotificationPermission(): Promise<boolean> {
+ const current = await Notifications.getPermissionsAsync();
+ if (current.status === "granted") return true;
+
+ const req = await Notifications.requestPermissionsAsync();
+ return req.status === "granted";
+}
+
+/**
+ * Afficher une notification locale immédiate
+ * ------------------------------------------
+ * data doit être un Record<string, unknown> (pas un objet typé direct).
+ */
+export async function showAlertNotification(alert: Alert): Promise<void> {
+ await initNotificationChannel();
+
+ const data: Record<string, unknown> = {
+ pair: alert.pair,
+ action: alert.action,
+ alertLevel: alert.alertLevel,
+ confidence: alert.confidence,
+ reason: alert.reason,
+ price: alert.price ?? null,
+ timestamp: alert.timestamp ?? Date.now(),
+ };
+
+ await Notifications.scheduleNotificationAsync({
+ content: {
+ title: formatTitle(alert),
+ body: formatBody(alert),
+ data,
+ sound: "default",
+ },
+ trigger: null, // immédiat
+ });
+}
\ No newline at end of file
--- /dev/null
+import type { StrategyOption, StrategyKey } from "../types/Strategy";
+import { getStrategies } from "../mocks/strategies.mock";
+
+/**
+ * strategyService
+ * ---------------
+ * Aujourd'hui : mock
+ * Demain : REST
+ *
+ * REST attendu (contrat groupe) :
+ * - POST /api/strategy/select
+ */
+export async function fetchStrategies(): Promise<StrategyOption[]> {
+ return await getStrategies();
+}
+
+/**
+ * Placeholder : plus tard on fera le POST ici.
+ * Pour le moment, la sélection est gérée via AsyncStorage (settings).
+ */
+export async function selectStrategy(_strategyKey: StrategyKey): Promise<void> {
+ return;
+}
\ No newline at end of file
--- /dev/null
+/**
+ * Strategy
+ * --------
+ * Contrat simple côté mobile.
+ * Plus tard: ces données viendront de l'API / DB.
+ */
+
+export type StrategyKey =
+ | "RSI_SIMPLE"
+ | "MA_CROSS"
+ | "MACD_BASIC"
+ | "HOLD_ONLY";
+
+export interface StrategyOption {
+ key: StrategyKey;
+ label: string;
+ description: string;
+ risk: "SAFE" | "NORMAL" | "AGGRESSIVE";
+}
\ No newline at end of file
const KEY = "userSettings";
+/**
+ * Settings par défaut
+ * -------------------
+ * On les fusionne avec ce qui est en storage.
+ */
const DEFAULT_SETTINGS: UserSettings = {
currency: "EUR",
favoriteSymbol: "BTC",
alertPreference: "critical",
refreshMode: "manual",
+ notificationsEnabled: false,
+
+ // ✅ stratégie par défaut
+ selectedStrategyKey: "RSI_SIMPLE",
};
export async function loadSettings(): Promise<UserSettings> {
- try {
- const raw = await AsyncStorage.getItem(KEY);
-
- if (!raw) {
- return DEFAULT_SETTINGS;
- }
+ const raw = await AsyncStorage.getItem(KEY);
+ if (!raw) return DEFAULT_SETTINGS;
+ try {
const parsed = JSON.parse(raw) as Partial<UserSettings>;
-
- return {
- ...DEFAULT_SETTINGS,
- ...parsed,
- };
- } catch (error) {
- console.error("Erreur lors du chargement des settings :", error);
+ return { ...DEFAULT_SETTINGS, ...parsed };
+ } catch {
return DEFAULT_SETTINGS;
}
}
export async function saveSettings(settings: UserSettings): Promise<void> {
- try {
- await AsyncStorage.setItem(KEY, JSON.stringify(settings));
- } catch (error) {
- console.error("Erreur lors de la sauvegarde des settings :", error);
- throw error;
- }
-}
+ await AsyncStorage.setItem(KEY, JSON.stringify(settings));
+}
\ No newline at end of file
--- /dev/null
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import type { WalletState } from "../models/Wallet";
+
+const KEY = "walletStep1";
+
+/**
+ * Valeurs par défaut : wallet vide.
+ */
+const DEFAULT_WALLET: WalletState = {
+ assetSymbol: "BTC",
+ quantity: 0,
+ updatedAtMs: Date.now(),
+};
+
+export async function loadWallet(): Promise<WalletState> {
+ const raw = await AsyncStorage.getItem(KEY);
+ if (!raw) return DEFAULT_WALLET;
+
+ try {
+ const parsed = JSON.parse(raw) as Partial<WalletState>;
+ return { ...DEFAULT_WALLET, ...parsed };
+ } catch {
+ return DEFAULT_WALLET;
+ }
+}
+
+export async function saveWallet(wallet: WalletState): Promise<void> {
+ const safe: WalletState = {
+ assetSymbol: "BTC",
+ quantity: Number.isFinite(wallet.quantity) ? wallet.quantity : 0,
+ updatedAtMs: Date.now(),
+ };
+
+ await AsyncStorage.setItem(KEY, JSON.stringify(safe));
+}
+
+export async function clearWallet(): Promise<void> {
+ await AsyncStorage.removeItem(KEY);
+}
\ No newline at end of file