From: Thibaud Moustier Date: Wed, 25 Feb 2026 15:55:27 +0000 (+0100) Subject: Mobile : Modification 'mobile' - Mise en place des différente fenetre + modification UI X-Git-Url: https://git.digitality.be/?a=commitdiff_plain;h=2f2503c9ec1fda9f857a5ee129f49f29b58059f8;p=pdw25-26 Mobile : Modification 'mobile' - Mise en place des différente fenetre + modification UI --- diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md deleted file mode 100644 index ce8c4b6..0000000 --- a/Wallette/mobile/.expo/README.md +++ /dev/null @@ -1,13 +0,0 @@ -> Why do I have a folder named ".expo" in my project? - -The ".expo" folder is created when an Expo project is started using "expo start" command. - -> What do the files contain? - -- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. -- "settings.json": contains the server configuration that is used to serve the application manifest. - -> Should I commit the ".expo" folder? - -No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. -Upon project creation, the ".expo" folder is already added to your ".gitignore" file. diff --git a/Wallette/mobile/.expo/devices.json b/Wallette/mobile/.expo/devices.json deleted file mode 100644 index 5efff6c..0000000 --- a/Wallette/mobile/.expo/devices.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "devices": [] -} diff --git a/Wallette/mobile/App.tsx b/Wallette/mobile/App.tsx index e491b2b..8f9403f 100644 --- a/Wallette/mobile/App.tsx +++ b/Wallette/mobile/App.tsx @@ -1,15 +1,23 @@ 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(); @@ -21,7 +29,38 @@ export default function App() { ({ + title: "Dashboard", + headerRight: () => ( + navigation.navigate("Settings")}> + + + ), + })} + /> + + + + + + + + - - - ); diff --git a/Wallette/mobile/assets/adaptive-icon.png b/Wallette/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/Wallette/mobile/assets/adaptive-icon.png differ diff --git a/Wallette/mobile/assets/favicon.png b/Wallette/mobile/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/Wallette/mobile/assets/favicon.png differ diff --git a/Wallette/mobile/assets/icon.png b/Wallette/mobile/assets/icon.png new file mode 100644 index 0000000..a0b1526 Binary files /dev/null and b/Wallette/mobile/assets/icon.png differ diff --git a/Wallette/mobile/assets/splash-icon.png b/Wallette/mobile/assets/splash-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/Wallette/mobile/assets/splash-icon.png differ diff --git a/Wallette/mobile/package-lock.json b/Wallette/mobile/package-lock.json index c917a21..fd361d2 100644 --- a/Wallette/mobile/package-lock.json +++ b/Wallette/mobile/package-lock.json @@ -8,10 +8,12 @@ "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", @@ -61,7 +63,6 @@ "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", @@ -1416,7 +1417,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -2198,6 +2198,17 @@ "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", @@ -2320,6 +2331,12 @@ "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", @@ -3192,7 +3209,6 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3396,12 +3412,40 @@ "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", @@ -3662,6 +3706,12 @@ "@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", @@ -3796,7 +3846,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3859,6 +3908,53 @@ "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", @@ -4283,6 +4379,23 @@ "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", @@ -4292,6 +4405,23 @@ "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", @@ -4347,6 +4477,20 @@ "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", @@ -4435,6 +4579,36 @@ "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", @@ -4501,7 +4675,6 @@ "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", @@ -4549,6 +4722,43 @@ } } }, + "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", @@ -4648,6 +4858,26 @@ "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", @@ -4829,17 +5059,6 @@ } } }, - "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", @@ -4962,20 +5181,6 @@ "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", @@ -4986,21 +5191,6 @@ "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", @@ -5186,6 +5376,21 @@ "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", @@ -5233,6 +5438,15 @@ "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", @@ -5251,6 +5465,30 @@ "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", @@ -5260,6 +5498,19 @@ "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", @@ -5313,6 +5564,18 @@ "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", @@ -5328,6 +5591,45 @@ "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", @@ -5500,12 +5802,40 @@ "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", @@ -5545,6 +5875,41 @@ "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", @@ -5563,6 +5928,39 @@ "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", @@ -6455,6 +6853,15 @@ "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", @@ -7066,6 +7473,51 @@ "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", @@ -7342,6 +7794,15 @@ "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", @@ -7521,7 +7982,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7559,7 +8019,6 @@ "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", @@ -7627,7 +8086,6 @@ "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": "*" @@ -7638,7 +8096,6 @@ "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", @@ -7738,7 +8195,6 @@ "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" } @@ -7990,6 +8446,23 @@ ], "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", @@ -8119,6 +8592,23 @@ "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", @@ -8689,7 +9179,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8886,6 +9375,19 @@ "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", @@ -8996,6 +9498,27 @@ "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", diff --git a/Wallette/mobile/package.json b/Wallette/mobile/package.json index b229db2..0db1739 100644 --- a/Wallette/mobile/package.json +++ b/Wallette/mobile/package.json @@ -9,10 +9,12 @@ "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", diff --git a/Wallette/mobile/src/components/ActionsCard.tsx b/Wallette/mobile/src/components/ActionsCard.tsx deleted file mode 100644 index dfc2a6c..0000000 --- a/Wallette/mobile/src/components/ActionsCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -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 ( - - Actions - - - {}}> - Voir stratégie - - - - Historique - - - - Paramètres - - - - ); -} \ No newline at end of file diff --git a/Wallette/mobile/src/components/StrategyCard.tsx b/Wallette/mobile/src/components/StrategyCard.tsx index 1a940e7..d783848 100644 --- a/Wallette/mobile/src/components/StrategyCard.tsx +++ b/Wallette/mobile/src/components/StrategyCard.tsx @@ -1,82 +1,73 @@ -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 ( - Stratégie + Conseiller - {summary.strategy} + {/* Décision principale */} + {summary.decision} - {/* Badge décision */} - - - {summary.decision} - - - - {/* Badge niveau d'alerte */} - - - {summary.alertLevel} - - + {/* Justification (centrée) */} + + Pourquoi ? {summary.reason} + - - Confiance - - {(summary.confidence * 100).toFixed(1)} % + {/* Stratégie sélectionnée */} + + + Stratégie sélectionnée :{" "} + {settings.selectedStrategyKey} - {summary.reason} + {/* Bouton vers l'écran stratégie */} + + Changer stratégie + ); -} \ 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 diff --git a/Wallette/mobile/src/config/env.ts b/Wallette/mobile/src/config/env.ts index 799b6a2..36a9d9a 100644 --- a/Wallette/mobile/src/config/env.ts +++ b/Wallette/mobile/src/config/env.ts @@ -1 +1,3 @@ -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 diff --git a/Wallette/mobile/src/mocks/strategies.mock.ts b/Wallette/mobile/src/mocks/strategies.mock.ts new file mode 100644 index 0000000..f2a2efd --- /dev/null +++ b/Wallette/mobile/src/mocks/strategies.mock.ts @@ -0,0 +1,36 @@ +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 { + 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 diff --git a/Wallette/mobile/src/models/UserSettings.ts b/Wallette/mobile/src/models/UserSettings.ts index fd54d3d..97b7181 100644 --- a/Wallette/mobile/src/models/UserSettings.ts +++ b/Wallette/mobile/src/models/UserSettings.ts @@ -1,18 +1,27 @@ -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 diff --git a/Wallette/mobile/src/models/Wallet.ts b/Wallette/mobile/src/models/Wallet.ts new file mode 100644 index 0000000..f36769f --- /dev/null +++ b/Wallette/mobile/src/models/Wallet.ts @@ -0,0 +1,11 @@ +/** + * 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 diff --git a/Wallette/mobile/src/screens/AlertsScreen.tsx b/Wallette/mobile/src/screens/AlertsScreen.tsx new file mode 100644 index 0000000..dff674a --- /dev/null +++ b/Wallette/mobile/src/screens/AlertsScreen.tsx @@ -0,0 +1,275 @@ +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([]); + const [filter, setFilter] = useState("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 ( + setFilter(value)} + > + + {label} + + + ); + }; + + return ( + + `alert-${idx}`} + ListHeaderComponent={ + + + Alertes + + + Clear + + + + + Filtre : {filter === "ALL" ? "Toutes" : filter} — Total : {alerts.length} + + + + + + + + + + + Tri par défaut : CRITICAL > WARNING > INFO, puis plus récent. + + + } + ListEmptyComponent={ + + Aucune alerte + En attente d’alertes Socket.IO… + + } + renderItem={({ item }) => { + const lvl = item.alertLevel as AlertLevel; + const act = item.action as TradeDecision; + + const lvlColor = getLevelColor(lvl); + const actColor = getActionColor(act); + + return ( + + + {item.pair} + + {item.timestamp ? new Date(item.timestamp).toLocaleString() : "—"} + + + + + + {lvl} + + + + {act} + + + + + Confiance + {(item.confidence * 100).toFixed(0)}% + + + {typeof item.price === "number" && ( + + Prix + {item.price.toFixed(2)} + + )} + + {item.reason} + + ); + }} + /> + + ); +} + +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 diff --git a/Wallette/mobile/src/screens/DashboardScreen.tsx b/Wallette/mobile/src/screens/DashboardScreen.tsx index a8a34b0..8bd123a 100644 --- a/Wallette/mobile/src/screens/DashboardScreen.tsx +++ b/Wallette/mobile/src/screens/DashboardScreen.tsx @@ -1,52 +1,63 @@ -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(null); const [settings, setSettings] = useState(null); - const [loading, setLoading] = useState(true); + const [wallet, setWallet] = useState(null); - // erreurs REST/refresh + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Socket state (non bloquant) const [socketConnected, setSocketConnected] = useState(false); const [socketError, setSocketError] = useState(null); + const [liveAlerts, setLiveAlerts] = useState([]); + const [lastRefreshMs, setLastRefreshMs] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const navigation = useNavigation(); - /** - * Chargement initial + rechargement quand on revient sur l'écran - */ useFocusEffect( useCallback(() => { let isActive = true; @@ -56,15 +67,18 @@ export default function DashboardScreen() { 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 { @@ -80,9 +94,6 @@ export default function DashboardScreen() { }, []) ); - /** - * Refresh auto (sans clignotement global) - */ useEffect(() => { if (!settings) return; if (settings.refreshMode !== "auto") return; @@ -92,7 +103,10 @@ export default function DashboardScreen() { 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."); } @@ -104,24 +118,26 @@ export default function DashboardScreen() { }; }, [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); @@ -133,7 +149,18 @@ export default function DashboardScreen() { } 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(); @@ -145,7 +172,23 @@ export default function DashboardScreen() { }; }, [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 ( @@ -154,7 +197,6 @@ export default function DashboardScreen() { ); } - // Erreur bloquante (si rien à afficher) if (error && (!summary || !settings)) { return ( @@ -163,8 +205,7 @@ export default function DashboardScreen() { ); } - // Sécurité - if (!summary || !settings) { + if (!summary || !settings || !wallet) { return ( Initialisation… @@ -172,36 +213,132 @@ export default function DashboardScreen() { ); } + const Chevron = () => ( + + ); + return ( - - {/* Bannière warning REST/refresh (non bloquant) */} + {error && ( - - {error} + + + {error} + )} - {/* Bannière Socket (non bloquant) */} - - - Socket.IO : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"} + {/* 1) CONSEILLER */} + + Conseiller + + {summary.decision} + + + Pourquoi ? {summary.reason} - {!!socketError && {socketError}} - - Alertes reçues : {liveAlerts.length} + + Stratégie : {settings.selectedStrategyKey} - - - - + navigation.navigate("Strategy" as never)} + > + Sélectionner stratégie + + - navigation.navigate("Settings" as never)} - onGoHistory={() => navigation.navigate("History" as never)} - /> + {/* 2) PORTEFEUILLE — cliquable => Wallet */} + navigation.navigate("Wallet" as never)}> + + + Portefeuille + + + + + Quantité BTC : + {wallet.quantity.toFixed(6)} BTC + + + + Valeur Totale : + + {walletTotalValue !== null ? `${walletTotalValue.toFixed(2)} ${settings.currency}` : "—"} + + + + + BTC @ {summary.price.toFixed(2)} {settings.currency} + + + + + {/* 3) URGENCE — cliquable => Alertes */} + navigation.navigate("Alerts" as never)}> + + + Urgence + + + + {urgentAlert ? ( + + + {urgentAlert.alertLevel} : {urgentAlert.reason} + + + {urgentAlert.action} {urgentAlert.pair} — {(urgentAlert.confidence * 100).toFixed(0)}% + + + ) : ( + Aucune alerte pour le moment. + )} + + + Socket : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"} + {socketError ? ` — ${socketError}` : ""} + + + + + {/* 4) PRIX BTC — cliquable => Historique */} + navigation.navigate("History" as never)}> + + + + Prix BTC + + + + + {refreshing ? "…" : "Actualiser"} + + + + + Dernière maj : {lastRefreshMs ? new Date(lastRefreshMs).toLocaleTimeString() : "—"} + + + + + Prix BTC + + {summary.price.toFixed(2)} {settings.currency} + + + + + ); @@ -213,9 +350,22 @@ const styles = StyleSheet.create({ 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: { @@ -224,11 +374,98 @@ const styles = StyleSheet.create({ 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 diff --git a/Wallette/mobile/src/screens/HistoryScreen.tsx b/Wallette/mobile/src/screens/HistoryScreen.tsx index d065eec..ef568e9 100644 --- a/Wallette/mobile/src/screens/HistoryScreen.tsx +++ b/Wallette/mobile/src/screens/HistoryScreen.tsx @@ -66,7 +66,7 @@ export default function HistoryScreen() { if (loading) { return ( - + Chargement de l'historique… ); @@ -74,7 +74,7 @@ export default function HistoryScreen() { if (error) { return ( - + {error} ); @@ -83,7 +83,7 @@ export default function HistoryScreen() { return ( it.signalId} ListEmptyComponent={ @@ -104,23 +104,53 @@ export default function HistoryScreen() { {/* Badges */} - - - {item.action} + + + + {item.action} + - - {item.criticality} + + + {item.criticality} + - - {item.status} + + + {item.status} + Confiance - {(item.confidence * 100).toFixed(1)}% + + {(item.confidence * 100).toFixed(1)}% + @@ -142,15 +172,6 @@ const styles = StyleSheet.create({ 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", diff --git a/Wallette/mobile/src/screens/SettingsScreen.tsx b/Wallette/mobile/src/screens/SettingsScreen.tsx index 66dd3b2..24c9e77 100644 --- a/Wallette/mobile/src/screens/SettingsScreen.tsx +++ b/Wallette/mobile/src/screens/SettingsScreen.tsx @@ -1,11 +1,16 @@ 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(null); + const [infoMessage, setInfoMessage] = useState(null); useEffect(() => { async function init() { @@ -17,7 +22,7 @@ export default function SettingsScreen() { if (!settings) { return ( - + Chargement des paramètres… ); @@ -33,27 +38,75 @@ export default function SettingsScreen() { 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 ( - - Paramètres + + Paramètres - Devise : {settings.currency} - - Changer devise - + {/* Carte : Devise */} + + Devise + Actuelle : {settings.currency} - Mode rafraîchissement : {settings.refreshMode} - - Changer mode - + + Changer devise + + + + {/* Carte : Refresh */} + + Rafraîchissement + Mode : {settings.refreshMode} + + + Changer mode + + + + {/* Carte : Notifications */} + + Notifications + + Statut : {settings.notificationsEnabled ? "ON" : "OFF"} + + + + + {settings.notificationsEnabled ? "Désactiver" : "Activer"} les notifications + + - - Sauvegarder + {!!infoMessage && {infoMessage}} + + + {/* Bouton Save */} + + Sauvegarder @@ -63,31 +116,33 @@ export default function SettingsScreen() { 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 diff --git a/Wallette/mobile/src/screens/StrategyScreen.tsx b/Wallette/mobile/src/screens/StrategyScreen.tsx new file mode 100644 index 0000000..f3ebfdc --- /dev/null +++ b/Wallette/mobile/src/screens/StrategyScreen.tsx @@ -0,0 +1,144 @@ +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(null); + const [strategies, setStrategies] = useState([]); + const [loading, setLoading] = useState(true); + const [info, setInfo] = useState(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 ( + + Chargement des stratégies… + + ); + } + + 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 ( + + it.key} + ListHeaderComponent={ + + Stratégie + + Choisis une stratégie. Elle sera affichée sur le Dashboard. + + + + Actuelle : {settings.selectedStrategyKey} + + + {!!info && {info}} + + } + renderItem={({ item }) => ( + + + {item.label} + {item.risk} + + + {item.description} + + handleSelect(item.key)} + > + + {isSelected(item.key) ? "Sélectionnée ✅" : "Sélectionner"} + + + + )} + /> + + ); +} + +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 diff --git a/Wallette/mobile/src/screens/WalletScreen.tsx b/Wallette/mobile/src/screens/WalletScreen.tsx new file mode 100644 index 0000000..0768379 --- /dev/null +++ b/Wallette/mobile/src/screens/WalletScreen.tsx @@ -0,0 +1,240 @@ +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(null); + const [settings, setSettings] = useState(null); + + // Prix BTC actuel (mock aujourd'hui, API demain) + const [btcPrice, setBtcPrice] = useState(null); + + // input texte (évite les bugs de virgule/points) + const [qtyInput, setQtyInput] = useState("0"); + + const [info, setInfo] = useState(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 ( + + Chargement du portefeuille… + + ); + } + + return ( + + + Portefeuille + + {/* Carte BTC */} + + BTC + + Quantité détenue + + + + + Prix BTC actuel :{" "} + + {btcPrice !== null ? `${btcPrice.toFixed(2)} ${settings.currency}` : "—"} + + + + + Valeur estimée :{" "} + + {totalValue !== null ? `${totalValue.toFixed(2)} ${settings.currency}` : "—"} + + + + {/* ✅ Dernière mise à jour */} + + Dernière mise à jour :{" "} + {lastUpdatedLabel} + + + + Enregistrer + + + + Réinitialiser + + + {!!info && {info}} + + + {/* Carte info Step */} + + Step 1 + + Mono-utilisateur / mono-crypto (BTC). Step 3 : portefeuille multi-cryptos + valeur globale. + + + + + ); +} + +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 diff --git a/Wallette/mobile/src/services/alertStore.ts b/Wallette/mobile/src/services/alertStore.ts new file mode 100644 index 0000000..4ac745d --- /dev/null +++ b/Wallette/mobile/src/services/alertStore.ts @@ -0,0 +1,38 @@ +import type { Alert } from "../types/Alert"; + +type Listener = (alerts: Alert[]) => void; + +class AlertStore { + private alerts: Alert[] = []; + private listeners = new Set(); + + 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 diff --git a/Wallette/mobile/src/services/notificationService.ts b/Wallette/mobile/src/services/notificationService.ts new file mode 100644 index 0000000..5e6cf16 --- /dev/null +++ b/Wallette/mobile/src/services/notificationService.ts @@ -0,0 +1,89 @@ +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 { + 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 { + 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 (pas un objet typé direct). + */ +export async function showAlertNotification(alert: Alert): Promise { + await initNotificationChannel(); + + const data: Record = { + 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 diff --git a/Wallette/mobile/src/services/strategyService.ts b/Wallette/mobile/src/services/strategyService.ts new file mode 100644 index 0000000..2d7a558 --- /dev/null +++ b/Wallette/mobile/src/services/strategyService.ts @@ -0,0 +1,23 @@ +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 { + 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 { + return; +} \ No newline at end of file diff --git a/Wallette/mobile/src/types/Strategy.ts b/Wallette/mobile/src/types/Strategy.ts new file mode 100644 index 0000000..a8c3440 --- /dev/null +++ b/Wallette/mobile/src/types/Strategy.ts @@ -0,0 +1,19 @@ +/** + * 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 diff --git a/Wallette/mobile/src/utils/settingsStorage.ts b/Wallette/mobile/src/utils/settingsStorage.ts index 4936eec..07570d7 100644 --- a/Wallette/mobile/src/utils/settingsStorage.ts +++ b/Wallette/mobile/src/utils/settingsStorage.ts @@ -3,38 +3,34 @@ import type { UserSettings } from "../models/UserSettings"; 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 { - 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; - - 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 { - 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 diff --git a/Wallette/mobile/src/utils/walletStorage.ts b/Wallette/mobile/src/utils/walletStorage.ts new file mode 100644 index 0000000..f74b2aa --- /dev/null +++ b/Wallette/mobile/src/utils/walletStorage.ts @@ -0,0 +1,39 @@ +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 { + const raw = await AsyncStorage.getItem(KEY); + if (!raw) return DEFAULT_WALLET; + + try { + const parsed = JSON.parse(raw) as Partial; + return { ...DEFAULT_WALLET, ...parsed }; + } catch { + return DEFAULT_WALLET; + } +} + +export async function saveWallet(wallet: WalletState): Promise { + 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 { + await AsyncStorage.removeItem(KEY); +} \ No newline at end of file