]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : Modification ui + notification on phone
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 12:40:32 +0000 (13:40 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Wed, 25 Feb 2026 12:40:32 +0000 (13:40 +0100)
Wallette/mobile/package-lock.json
Wallette/mobile/package.json
Wallette/mobile/src/models/UserSettings.ts
Wallette/mobile/src/screens/DashboardScreen.tsx
Wallette/mobile/src/screens/HistoryScreen.tsx
Wallette/mobile/src/screens/SettingsScreen.tsx
Wallette/mobile/src/services/notificationService.ts [new file with mode: 0644]
Wallette/mobile/src/utils/settingsStorage.ts

index c917a217048275f79bdcf54b0c324fca41f47fe0..212ee14f4e4c0569eec8091af248a4e07aed78d8 100644 (file)
@@ -12,6 +12,7 @@
         "@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 +62,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"
       }
         "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-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",
         "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",
       "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"
       },
       "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..3500e3a73dc14062c483b9120120e3b84c7daee3 100644 (file)
@@ -13,6 +13,7 @@
     "@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",
index fd54d3dee0f0a3288bb95913125efec5f0844ae0..c4f583b86cfa4e2b53fa36fc7ee1a3b9218a2149 100644 (file)
@@ -1,18 +1,23 @@
-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;
+}
\ No newline at end of file
index 01da368746fef7e0264c43d9cb665ff8acfed3c6..be6e15cfa82f26f66c52f771822eb813530433a5 100644 (file)
@@ -7,7 +7,6 @@ import type { DashboardSummary } from "../types/DashboardSummary";
 import { fetchDashboardSummary } from "../services/dashboardService";
 import { loadSettings } from "../utils/settingsStorage";
 import type { UserSettings } from "../models/UserSettings";
-import { alertStore } from "../services/alertStore";
 
 import MarketCard from "../components/MarketCard";
 import StrategyCard from "../components/StrategyCard";
@@ -21,6 +20,12 @@ import { socketService } from "../services/socketService";
 import { SERVER_URL } from "../config/env";
 import type { Alert } from "../types/Alert";
 
+// ✅ Store global alertes
+import { alertStore } from "../services/alertStore";
+
+// ✅ Notifications locales Expo
+import { showAlertNotification } from "../services/notificationService";
+
 /**
  * DashboardScreen
  * ----------------
@@ -30,10 +35,8 @@ import type { Alert } from "../types/Alert";
  * - Socket.IO non bloquant pour alertes live
  * - Dashboard = résumé : on affiche UNE seule alerte (la dernière)
  *
- *
- * Important :
- * - Le Socket est NON BLOQUANT : si le serveur n'est pas dispo, l'app continue de marcher.
- * - En Step 1/2/3, on n'a pas forcément de vrai userId => on utilise un userId de test.
+ * Notifications :
+ * - Si settings.notificationsEnabled = true -> notification locale à chaque alerte reçue
  */
 export default function DashboardScreen() {
   const [summary, setSummary] = useState<DashboardSummary | null>(null);
@@ -46,6 +49,8 @@ export default function DashboardScreen() {
   // Socket state (non bloquant)
   const [socketConnected, setSocketConnected] = useState<boolean>(false);
   const [socketError, setSocketError] = useState<string | null>(null);
+
+  // Résumé dashboard (dernière alerte reçue affichée)
   const [liveAlerts, setLiveAlerts] = useState<Alert[]>([]);
 
   const navigation = useNavigation();
@@ -133,8 +138,23 @@ export default function DashboardScreen() {
     }
 
     const unsubscribeAlert = socketService.onAlert((alert) => {
-      alertStore.add(alert); // ✅ stock global
-      setLiveAlerts((prev) => [alert, ...prev].slice(0, 50)); // (optionnel) résumé dashboard
+      // 1) Stock global (écran Alertes)
+      alertStore.add(alert);
+
+      // 2) Résumé dashboard (dernière alerte)
+      setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
+
+      // 3) Notification locale (si activée)
+      if (settings.notificationsEnabled) {
+        // On encapsule pour ne jamais casser le thread UI
+        void (async () => {
+          try {
+            await showAlertNotification(alert);
+          } catch (e) {
+            console.log("⚠️ Notification error:", e);
+          }
+        })();
+      }
     });
 
     socketService.ping();
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..b76423ef17ddc4eabecbf6c0429d641457f31d7a 100644 (file)
@@ -1,12 +1,22 @@
 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";
+
+// ✅ Notifications Expo
+import { requestNotificationPermission } from "../services/notificationService";
+
+// ✅ UI global
+import { ui } from "../components/ui/uiStyles";
+
 export default function SettingsScreen() {
   const [settings, setSettings] = useState<UserSettings | null>(null);
 
+  // Petit message utilisateur (permission refusée, etc.)
+  const [infoMessage, setInfoMessage] = useState<string | null>(null);
+
   useEffect(() => {
     async function init() {
       const data = await loadSettings();
@@ -17,7 +27,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 +43,89 @@ export default function SettingsScreen() {
     setSettings({ ...settings, refreshMode: newMode });
   };
 
+  /**
+   * Notifications :
+   * - si l'utilisateur active -> on demande la permission
+   * - si refus -> on reste désactivé + message
+   */
+  const toggleNotifications = async () => {
+    setInfoMessage(null);
+
+    // si on désactive, pas besoin de permission
+    if (settings.notificationsEnabled) {
+      setSettings({ ...settings, notificationsEnabled: false });
+      return;
+    }
+
+    // si on active -> demander permission
+    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>
+          <View style={{ marginTop: 10 }}>
+            <TouchableOpacity style={ui.button} onPress={toggleCurrency}>
+              <Text style={ui.buttonText}>Changer devise</Text>
+            </TouchableOpacity>
+          </View>
+        </View>
+
+        {/* Carte : Refresh */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Rafraîchissement</Text>
+          <Text style={ui.value}>Mode : {settings.refreshMode}</Text>
+
+          <View style={{ marginTop: 10 }}>
+            <TouchableOpacity style={ui.button} onPress={toggleRefreshMode}>
+              <Text style={ui.buttonText}>Changer mode</Text>
+            </TouchableOpacity>
+          </View>
+        </View>
 
-        <TouchableOpacity style={styles.saveButton} onPress={handleSave}>
-          <Text style={styles.buttonText}>Sauvegarder</Text>
+        {/* Carte : Notifications */}
+        <View style={ui.card}>
+          <Text style={ui.title}>Notifications</Text>
+          <Text style={ui.value}>
+            Statut : {settings.notificationsEnabled ? "ON" : "OFF"}
+          </Text>
+
+          <View style={{ marginTop: 10 }}>
+            <TouchableOpacity style={ui.button} onPress={toggleNotifications}>
+              <Text style={ui.buttonText}>
+                {settings.notificationsEnabled ? "Désactiver" : "Activer"} les
+                notifications
+              </Text>
+            </TouchableOpacity>
+          </View>
+
+          {!!infoMessage && <Text style={styles.infoText}>{infoMessage}</Text>}
+        </View>
+
+        {/* Bouton Save */}
+        <TouchableOpacity style={[ui.button, styles.saveButton]} onPress={handleSave}>
+          <Text style={ui.buttonText}>Sauvegarder</Text>
         </TouchableOpacity>
       </View>
     </SafeAreaView>
@@ -63,31 +135,21 @@ export default function SettingsScreen() {
 const styles = StyleSheet.create({
   safeArea: {
     flex: 1,
-    backgroundColor: "#fff",
+    backgroundColor: ui.screen.backgroundColor,
   },
-  container: {
-    padding: 16,
-  },
-  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,
   },
+  // On rend le bouton Save plus large (full width)
   saveButton: {
-    backgroundColor: "#2563eb",
-    padding: 12,
-    marginTop: 20,
-    borderRadius: 6,
-  },
-  buttonText: {
-    color: "#fff",
-    fontWeight: "bold",
-    textAlign: "center",
+    flexBasis: "100%",
+    marginTop: 4,
   },
-});
+});
\ 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
index 4936eec8b0804e08004e12c75a6cfe925b8e50f8..d86718ad087673f9abdd6648c9afb4206bdb606f 100644 (file)
@@ -3,38 +3,33 @@ 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",
+
+  // ✅ par défaut désactivé (l'utilisateur choisit)
+  notificationsEnabled: false,
 };
 
 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