]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Modification 'mobile' - Mise en place des différente fenetre + modification UI
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 15:55:27 +0000 (16:55 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 15:55:27 +0000 (16:55 +0100)
27 files changed:
Wallette/mobile/.expo/README.md [deleted file]
Wallette/mobile/.expo/devices.json [deleted file]
Wallette/mobile/App.tsx
Wallette/mobile/assets/adaptive-icon.png [new file with mode: 0644]
Wallette/mobile/assets/favicon.png [new file with mode: 0644]
Wallette/mobile/assets/icon.png [new file with mode: 0644]
Wallette/mobile/assets/splash-icon.png [new file with mode: 0644]
Wallette/mobile/package-lock.json
Wallette/mobile/package.json
Wallette/mobile/src/components/ActionsCard.tsx [deleted file]
Wallette/mobile/src/components/StrategyCard.tsx
Wallette/mobile/src/config/env.ts
Wallette/mobile/src/mocks/strategies.mock.ts [new file with mode: 0644]
Wallette/mobile/src/models/UserSettings.ts
Wallette/mobile/src/models/Wallet.ts [new file with mode: 0644]
Wallette/mobile/src/screens/AlertsScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/screens/HistoryScreen.tsx
Wallette/mobile/src/screens/SettingsScreen.tsx
Wallette/mobile/src/screens/StrategyScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/screens/WalletScreen.tsx [new file with mode: 0644]
Wallette/mobile/src/services/alertStore.ts [new file with mode: 0644]
Wallette/mobile/src/services/notificationService.ts [new file with mode: 0644]
Wallette/mobile/src/services/strategyService.ts [new file with mode: 0644]
Wallette/mobile/src/types/Strategy.ts [new file with mode: 0644]
Wallette/mobile/src/utils/settingsStorage.ts
Wallette/mobile/src/utils/walletStorage.ts [new file with mode: 0644]

diff --git a/Wallette/mobile/.expo/README.md b/Wallette/mobile/.expo/README.md
deleted file mode 100644 (file)
index ce8c4b6..0000000
+++ /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 (file)
index 5efff6c..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "devices": []
-}
index e491b2b9915dfb303d3b0153f767ddc6300c5872..8f9403ff4f5030d083c2464201f265c112703819 100644 (file)
@@ -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<RootStackParamList>();
@@ -21,7 +29,38 @@ export default function App() {
         <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
@@ -29,13 +68,6 @@ export default function App() {
           component={SettingsScreen}
           options={{ title: "Paramètres" }}
         />
-
-        <Stack.Screen 
-        name="History" 
-        component={HistoryScreen} 
-        options={{ title: "Historique" }} 
-        />
-        
       </Stack.Navigator>
     </NavigationContainer>
   );
diff --git a/Wallette/mobile/assets/adaptive-icon.png b/Wallette/mobile/assets/adaptive-icon.png
new file mode 100644 (file)
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 (file)
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 (file)
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 (file)
index 0000000..03d6f6b
Binary files /dev/null and b/Wallette/mobile/assets/splash-icon.png differ
index c917a217048275f79bdcf54b0c324fca41f47fe0..fd361d289845da2662cb77af19fe9f45703b1cc8 100644 (file)
@@ -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",
       "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",
index b229db2bbc2c78fa80483ada364e5374d8e76306..0db1739d0bb07caebe29b336277c1a21de80481c 100644 (file)
@@ -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 (file)
index dfc2a6c..0000000
+++ /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 (
-    <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
index 1a940e7f5abb30c066056f5fcf4bbbdeaf292b94..d783848e814244da766739f820280f88d68450b8 100644 (file)
@@ -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 (
     <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
index 799b6a2b44b2d75175dc9d51b43544fb45392808..36a9d9a6cb7ed054187e118e8374bce18a5e640e 100644 (file)
@@ -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 (file)
index 0000000..f2a2efd
--- /dev/null
@@ -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<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
index fd54d3dee0f0a3288bb95913125efec5f0844ae0..97b718120f4bfd82fbac1f8438f714459536ff5d 100644 (file)
@@ -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 (file)
index 0000000..f36769f
--- /dev/null
@@ -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 (file)
index 0000000..dff674a
--- /dev/null
@@ -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<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 &gt; WARNING &gt; 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
index a8a34b03a023a47ec806fde1ab5c8dd5ff1b3ee7..8bd123a3a4b64874a67011bbff7e0e3a5ad77192 100644 (file)
@@ -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<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;
@@ -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 (
       <View style={ui.centered}>
@@ -154,7 +197,6 @@ export default function DashboardScreen() {
     );
   }
 
-  // Erreur bloquante (si rien à afficher)
   if (error && (!summary || !settings)) {
     return (
       <View style={ui.centered}>
@@ -163,8 +205,7 @@ export default function DashboardScreen() {
     );
   }
 
-  // Sécurité
-  if (!summary || !settings) {
+  if (!summary || !settings || !wallet) {
     return (
       <View style={ui.centered}>
         <Text>Initialisation…</Text>
@@ -172,36 +213,132 @@ export default function DashboardScreen() {
     );
   }
 
+  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>
   );
@@ -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
index d065eec1fec19c75d6d7cffe9053d5a4528fe2cb..ef568e90ccf8e49fa8dda365a31b29ca9fb7e01a 100644 (file)
@@ -66,7 +66,7 @@ export default function HistoryScreen() {
 
   if (loading) {
     return (
-      <View style={styles.centered}>
+      <View style={ui.centered}>
         <Text>Chargement de l'historique…</Text>
       </View>
     );
@@ -74,7 +74,7 @@ export default function HistoryScreen() {
 
   if (error) {
     return (
-      <View style={styles.centered}>
+      <View style={ui.centered}>
         <Text style={styles.errorText}>{error}</Text>
       </View>
     );
@@ -83,7 +83,7 @@ export default function HistoryScreen() {
   return (
     <SafeAreaView style={styles.safeArea}>
       <FlatList
-        contentContainerStyle={styles.container}
+        contentContainerStyle={ui.container}
         data={items}
         keyExtractor={(it) => it.signalId}
         ListEmptyComponent={
@@ -104,23 +104,53 @@ export default function HistoryScreen() {
               </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 }]}>
@@ -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",
index 66dd3b206d5d3b888f11c775873cfd4083d43e8e..24c9e776caa7cf3c5794facd8a55ce05395fcf5d 100644 (file)
@@ -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<UserSettings | null>(null);
+  const [infoMessage, setInfoMessage] = useState<string | null>(null);
 
   useEffect(() => {
     async function init() {
@@ -17,7 +22,7 @@ export default function SettingsScreen() {
 
   if (!settings) {
     return (
-      <View style={styles.container}>
+      <View style={ui.centered}>
         <Text>Chargement des paramètres…</Text>
       </View>
     );
@@ -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 (
     <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>
@@ -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 (file)
index 0000000..f3ebfdc
--- /dev/null
@@ -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<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
diff --git a/Wallette/mobile/src/screens/WalletScreen.tsx b/Wallette/mobile/src/screens/WalletScreen.tsx
new file mode 100644 (file)
index 0000000..0768379
--- /dev/null
@@ -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<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
diff --git a/Wallette/mobile/src/services/alertStore.ts b/Wallette/mobile/src/services/alertStore.ts
new file mode 100644 (file)
index 0000000..4ac745d
--- /dev/null
@@ -0,0 +1,38 @@
+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
diff --git a/Wallette/mobile/src/services/notificationService.ts b/Wallette/mobile/src/services/notificationService.ts
new file mode 100644 (file)
index 0000000..5e6cf16
--- /dev/null
@@ -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<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
diff --git a/Wallette/mobile/src/services/strategyService.ts b/Wallette/mobile/src/services/strategyService.ts
new file mode 100644 (file)
index 0000000..2d7a558
--- /dev/null
@@ -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<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
diff --git a/Wallette/mobile/src/types/Strategy.ts b/Wallette/mobile/src/types/Strategy.ts
new file mode 100644 (file)
index 0000000..a8c3440
--- /dev/null
@@ -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
index 4936eec8b0804e08004e12c75a6cfe925b8e50f8..07570d727a7eee2c342e08efb94dd3760f92a518 100644 (file)
@@ -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<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
diff --git a/Wallette/mobile/src/utils/walletStorage.ts b/Wallette/mobile/src/utils/walletStorage.ts
new file mode 100644 (file)
index 0000000..f74b2aa
--- /dev/null
@@ -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<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