Compare commits
63 commits
fix/headle
...
main
Author | SHA1 | Date | |
---|---|---|---|
38e2f821dd | |||
5461852ada | |||
a88f3bf6c7 | |||
e6f23b83be | |||
f11f9d8371 | |||
4aff7380ce | |||
7501c04bbd | |||
3aebd4cd1c | |||
aea3a26096 | |||
b635f29f45 | |||
fd081d46a6 | |||
c1b220f007 | |||
754e14946c | |||
48b663799d | |||
894d26dad1 | |||
7f30ef9abf | |||
65b86bbb13 | |||
23ecee5061 | |||
9bc7e3b7cb | |||
dbfcc1e405 | |||
732dc3df7b | |||
6d5f5ec82d | |||
8b1c5291d4 | |||
eaa8197bc3 | |||
1b67de2507 | |||
b22c1b7e75 | |||
2fa7b4839a | |||
4e2ea42195 | |||
5bc4fb382b | |||
b70d6ed9a0 | |||
0e1fe28477 | |||
7918e74184 | |||
9cfb40e510 | |||
dccf361dbc | |||
6d958c6c25 | |||
6378758f9b | |||
47f11d1b88 | |||
d4de0b4541 | |||
b5ae235ba4 | |||
5bf3f9b6f9 | |||
27ead01714 | |||
a83d423c77 | |||
9b272bea61 | |||
be0cd62cb9 | |||
f39875b810 | |||
6e290bdb69 | |||
0001a50a5f | |||
e47a33bcd8 | |||
6af58755c1 | |||
b10ff5a6e7 | |||
cf61de639c | |||
7ab708a536 | |||
0ac28515df | |||
09ea8cd563 | |||
644480182d | |||
bf957ba115 | |||
9f6452d5e3 | |||
4280820e01 | |||
6a773367d4 | |||
70cca596e4 | |||
010aa2c2fc | |||
9523152165 | |||
b5a1c21e8e |
72 changed files with 4127 additions and 3435 deletions
161
CHANGELOG.md
161
CHANGELOG.md
|
@ -2,6 +2,167 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.11.11](https://github.com/alerte-secours/as-app/compare/v1.11.10...v1.11.11) (2025-07-23)
|
||||||
|
|
||||||
|
## [1.11.10](https://github.com/alerte-secours/as-app/compare/v1.11.9...v1.11.10) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.9](https://github.com/alerte-secours/as-app/compare/v1.11.8...v1.11.9) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.8](https://github.com/alerte-secours/as-app/compare/v1.11.7...v1.11.8) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.7](https://github.com/alerte-secours/as-app/compare/v1.11.6...v1.11.7) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.6](https://github.com/alerte-secours/as-app/compare/v1.11.5...v1.11.6) (2025-07-21)
|
||||||
|
|
||||||
|
## [1.11.5](https://github.com/alerte-secours/as-app/compare/v1.11.4...v1.11.5) (2025-07-21)
|
||||||
|
|
||||||
|
## [1.11.4](https://github.com/alerte-secours/as-app/compare/v1.11.3...v1.11.4) (2025-07-20)
|
||||||
|
|
||||||
|
## [1.11.3](https://github.com/alerte-secours/as-app/compare/v1.11.2...v1.11.3) (2025-07-20)
|
||||||
|
|
||||||
|
## [1.11.2](https://github.com/alerte-secours/as-app/compare/v1.11.1...v1.11.2) (2025-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios:** BGTaskSchedulerPermittedIdentifiers + prebuild wip ([e6f23b8](https://github.com/alerte-secours/as-app/commit/e6f23b83be7f30adc6385dc8f3e074521c7caf22))
|
||||||
|
|
||||||
|
## [1.11.1](https://github.com/alerte-secours/as-app/compare/v1.11.0...v1.11.1) (2025-07-18)
|
||||||
|
|
||||||
|
## [1.11.0](https://github.com/alerte-secours/as-app/compare/v1.10.9...v1.11.0) (2025-07-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **anchor:** bglost notification scroll to permissions + wip ([fd081d4](https://github.com/alerte-secours/as-app/commit/fd081d46a657059353bfb1e6022b68eedaee4e1a))
|
||||||
|
* optout sentry reporting ([c1b220f](https://github.com/alerte-secours/as-app/commit/c1b220f0078db8d07b3d58a7ac146d8af159a17d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* force location sync storage effect on interval ([7f30ef9](https://github.com/alerte-secours/as-app/commit/7f30ef9abf11bbdade5c3db3bfd7269dc212b39c))
|
||||||
|
* menu paramètres ([754e149](https://github.com/alerte-secours/as-app/commit/754e14946c0fa397558e5983a39c082c9072fe5c))
|
||||||
|
* message_permission_required ([894d26d](https://github.com/alerte-secours/as-app/commit/894d26dad12c4736cde827232764ed5c8d77df4e))
|
||||||
|
* **notification:** android background-geolocation-lost ([aea3a26](https://github.com/alerte-secours/as-app/commit/aea3a2609639f01b1d7aa414c67e3bdb99a1a47c))
|
||||||
|
* title_permission_required ([48b6637](https://github.com/alerte-secours/as-app/commit/48b663799d4bb4ef69671f0291a77a65418e2544))
|
||||||
|
* wip ([b635f29](https://github.com/alerte-secours/as-app/commit/b635f29f45928210bc9a9a8c5d201d91a6b48a07))
|
||||||
|
|
||||||
|
## [1.10.9](https://github.com/alerte-secours/as-app/compare/v1.10.7...v1.10.9) (2025-07-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bg location force sync all 12 hours ([732dc3d](https://github.com/alerte-secours/as-app/commit/732dc3df7b6ab635c70d02d657d066e6c72a49c4))
|
||||||
|
* **ios:** upgrade bundling ([23ecee5](https://github.com/alerte-secours/as-app/commit/23ecee5061e5e7324407223e7c1ce9a815f96521))
|
||||||
|
|
||||||
|
## [1.10.8](https://github.com/alerte-secours/as-app/compare/v1.10.7...v1.10.8) (2025-07-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bg location force sync all 12 hours ([732dc3d](https://github.com/alerte-secours/as-app/commit/732dc3df7b6ab635c70d02d657d066e6c72a49c4))
|
||||||
|
|
||||||
|
## [1.10.7](https://github.com/alerte-secours/as-app/compare/v1.10.1...v1.10.7) (2025-07-04)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** foreground service ([0ac2851](https://github.com/alerte-secours/as-app/commit/0ac28515dff752af30e45ff2f88d52da46c591aa))
|
||||||
|
* back to stateless refresh (sync endpoint) ([6af5875](https://github.com/alerte-secours/as-app/commit/6af58755c1b3e8c7be0d117d6fc07278406a0459))
|
||||||
|
* **battery-opti-disable:** integrate permissions view ([d9b5d10](https://github.com/alerte-secours/as-app/commit/d9b5d10684446a01873810382e501e45cd19da1b))
|
||||||
|
* **battery-opti-disable:** wip ([7082161](https://github.com/alerte-secours/as-app/commit/7082161b7f004b1052adbce2cf3ac9f74b2eee03))
|
||||||
|
* bettery optimization glitch ([b4b7441](https://github.com/alerte-secours/as-app/commit/b4b7441bacc3728d684a6e2219c84b91c75461af))
|
||||||
|
* bg location lost notif ([2fa7b48](https://github.com/alerte-secours/as-app/commit/2fa7b4839aedec61a5fdf2e5292440b3b0bb9c6b))
|
||||||
|
* don't handle refresh in headless mode anymore ([b10ff5a](https://github.com/alerte-secours/as-app/commit/b10ff5a6e735df6323d70a3c5b0d651d1cbd01cc))
|
||||||
|
* **headless-task:** wip ([c947d49](https://github.com/alerte-secours/as-app/commit/c947d4915ab19377cd2263a86bc9f902c41defdc))
|
||||||
|
* **headless-task:** wip ([6c290f2](https://github.com/alerte-secours/as-app/commit/6c290f21b4d57513aa90dabcc68e9a7b074a6430))
|
||||||
|
* **headless:** async-storage in memory first ([9f6452d](https://github.com/alerte-secours/as-app/commit/9f6452d5e368c82e0f93afd06c354756dd7913db))
|
||||||
|
* **headless:** secure store in memory first ([4280820](https://github.com/alerte-secours/as-app/commit/4280820e0169c4f9e36793d3f0ad460e9bde558f))
|
||||||
|
* **headless:** use axios instead of apollo for auth ([6444801](https://github.com/alerte-secours/as-app/commit/644480182d8da2ca1197c919262d5a5df0817d25))
|
||||||
|
* **headless:** use fetch instead of axios for auth ([09ea8cd](https://github.com/alerte-secours/as-app/commit/09ea8cd5634f46c1daeeb24ad4230ee508bd8293))
|
||||||
|
* import typo generateAlertEmergencyInfoContent ([d780fb4](https://github.com/alerte-secours/as-app/commit/d780fb4190acc24ceb9811682f8684ffb232a716))
|
||||||
|
* improve error handling ([010aa2c](https://github.com/alerte-secours/as-app/commit/010aa2c2fc0cf80c6afd3eb4fe954f94c94b8bea))
|
||||||
|
* **ios:** up ios version for rn compat ([8b1c529](https://github.com/alerte-secours/as-app/commit/8b1c5291d4ce0b042d745f76fc6dc62cf8bd6ca4))
|
||||||
|
* known keys ([be0cd62](https://github.com/alerte-secours/as-app/commit/be0cd62cb943fb4d74243f78df2fb1fc29e97e76))
|
||||||
|
* memoryAsyncStorage ([6e290bd](https://github.com/alerte-secours/as-app/commit/6e290bdb6997dd06ee8d674087368ad68b97a881))
|
||||||
|
* prevent race condition ([6a77336](https://github.com/alerte-secours/as-app/commit/6a773367d49ef66a8a896ba18e63c660f30eb143))
|
||||||
|
* **profile:** button enregistrer should be greysed when stored or no change ([9cfb40e](https://github.com/alerte-secours/as-app/commit/9cfb40e510584567400a6ea6f4871b345d723ac6))
|
||||||
|
* re-enable expo-updates ([16332cb](https://github.com/alerte-secours/as-app/commit/16332cbb764be5f067aa7cb3c2713399d1959f7c))
|
||||||
|
* re-up ([8331029](https://github.com/alerte-secours/as-app/commit/8331029e91220cc9c679dce1058ffc5f2cbb0577))
|
||||||
|
* reduce tracesSampleRate ([b5ae235](https://github.com/alerte-secours/as-app/commit/b5ae235ba4f093451bb7bd41b19aa77a8a0bda96))
|
||||||
|
* remove debuggin ([b70d6ed](https://github.com/alerte-secours/as-app/commit/b70d6ed9a070e9c01fa0d5206db5cc1a115e074e))
|
||||||
|
* sentry + re-enable expo-updates + disable debug ([8e8e120](https://github.com/alerte-secours/as-app/commit/8e8e120391f197e7826edaef47817ff8fb14a6b8))
|
||||||
|
* sentry tracing ([e6924ac](https://github.com/alerte-secours/as-app/commit/e6924ac9ff22b114eff4293f007becebe44e286d))
|
||||||
|
* sentry tracing ([4a0f3ab](https://github.com/alerte-secours/as-app/commit/4a0f3ab7effd79f75efd976f5fd9e1d1531e5b19))
|
||||||
|
* sentry tracing ([8ba4056](https://github.com/alerte-secours/as-app/commit/8ba4056187e6bd9ebf8ceec881197d6dc0aaaebc))
|
||||||
|
* theming ([4e2ea42](https://github.com/alerte-secours/as-app/commit/4e2ea4219501c16f0e74fcd2a5d48dca9845b3ee))
|
||||||
|
* undefined error ([47f11d1](https://github.com/alerte-secours/as-app/commit/47f11d1b888f4372fcb03db9bd1e839495455df3))
|
||||||
|
* undefined error ([d4de0b4](https://github.com/alerte-secours/as-app/commit/d4de0b4541fedb29b3e5161f91fc65b3253308e0))
|
||||||
|
* up android target sdk version ([7918e74](https://github.com/alerte-secours/as-app/commit/7918e74184165509b7a76c2420c24fcf6629a5fa))
|
||||||
|
* **upgrade:** expo 52 + rn 0.76.9 ([a1ed6cf](https://github.com/alerte-secours/as-app/commit/a1ed6cfca6217ba2068908f12e78af990f91b9c5))
|
||||||
|
* **wizard hero:** battery opti red inside parameters bubble ([dccf361](https://github.com/alerte-secours/as-app/commit/dccf361dbcb23239d28ea7f860ddeae3377349c0))
|
||||||
|
* bg location force sync all 12 hours ([71b73e7](https://github.com/alerte-secours/as-app/commit/71b73e7ba4733bd46345d8f73a2a33bb8786726a))
|
||||||
|
* **ios:** up ios version for rn compat ([8b1c529](https://github.com/alerte-secours/as-app/commit/8b1c5291d4ce0b042d745f76fc6dc62cf8bd6ca4))
|
||||||
|
|
||||||
|
|
||||||
|
## [1.10.6](https://github.com/alerte-secours/as-app/compare/v1.10.5...v1.10.6) (2025-07-04)
|
||||||
|
|
||||||
|
## [1.10.5](https://github.com/alerte-secours/as-app/compare/v1.10.4...v1.10.5) (2025-07-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bg location lost notif ([2fa7b48](https://github.com/alerte-secours/as-app/commit/2fa7b4839aedec61a5fdf2e5292440b3b0bb9c6b))
|
||||||
|
* theming ([4e2ea42](https://github.com/alerte-secours/as-app/commit/4e2ea4219501c16f0e74fcd2a5d48dca9845b3ee))
|
||||||
|
|
||||||
|
## [1.10.4](https://github.com/alerte-secours/as-app/compare/v1.10.3...v1.10.4) (2025-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove debuggin ([b70d6ed](https://github.com/alerte-secours/as-app/commit/b70d6ed9a070e9c01fa0d5206db5cc1a115e074e))
|
||||||
|
|
||||||
|
## [1.10.3](https://github.com/alerte-secours/as-app/compare/v1.10.2...v1.10.3) (2025-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **profile:** button enregistrer should be greysed when stored or no change ([9cfb40e](https://github.com/alerte-secours/as-app/commit/9cfb40e510584567400a6ea6f4871b345d723ac6))
|
||||||
|
* up android target sdk version ([7918e74](https://github.com/alerte-secours/as-app/commit/7918e74184165509b7a76c2420c24fcf6629a5fa))
|
||||||
|
* **wizard hero:** battery opti red inside parameters bubble ([dccf361](https://github.com/alerte-secours/as-app/commit/dccf361dbcb23239d28ea7f860ddeae3377349c0))
|
||||||
|
|
||||||
|
## [1.10.2](https://github.com/alerte-secours/as-app/compare/v1.10.1...v1.10.2) (2025-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **android:** foreground service ([0ac2851](https://github.com/alerte-secours/as-app/commit/0ac28515dff752af30e45ff2f88d52da46c591aa))
|
||||||
|
* back to stateless refresh (sync endpoint) ([6af5875](https://github.com/alerte-secours/as-app/commit/6af58755c1b3e8c7be0d117d6fc07278406a0459))
|
||||||
|
* **battery-opti-disable:** integrate permissions view ([d9b5d10](https://github.com/alerte-secours/as-app/commit/d9b5d10684446a01873810382e501e45cd19da1b))
|
||||||
|
* **battery-opti-disable:** wip ([7082161](https://github.com/alerte-secours/as-app/commit/7082161b7f004b1052adbce2cf3ac9f74b2eee03))
|
||||||
|
* bettery optimization glitch ([b4b7441](https://github.com/alerte-secours/as-app/commit/b4b7441bacc3728d684a6e2219c84b91c75461af))
|
||||||
|
* don't handle refresh in headless mode anymore ([b10ff5a](https://github.com/alerte-secours/as-app/commit/b10ff5a6e735df6323d70a3c5b0d651d1cbd01cc))
|
||||||
|
* **headless-task:** wip ([c947d49](https://github.com/alerte-secours/as-app/commit/c947d4915ab19377cd2263a86bc9f902c41defdc))
|
||||||
|
* **headless-task:** wip ([6c290f2](https://github.com/alerte-secours/as-app/commit/6c290f21b4d57513aa90dabcc68e9a7b074a6430))
|
||||||
|
* **headless:** async-storage in memory first ([9f6452d](https://github.com/alerte-secours/as-app/commit/9f6452d5e368c82e0f93afd06c354756dd7913db))
|
||||||
|
* **headless:** secure store in memory first ([4280820](https://github.com/alerte-secours/as-app/commit/4280820e0169c4f9e36793d3f0ad460e9bde558f))
|
||||||
|
* **headless:** use axios instead of apollo for auth ([6444801](https://github.com/alerte-secours/as-app/commit/644480182d8da2ca1197c919262d5a5df0817d25))
|
||||||
|
* **headless:** use fetch instead of axios for auth ([09ea8cd](https://github.com/alerte-secours/as-app/commit/09ea8cd5634f46c1daeeb24ad4230ee508bd8293))
|
||||||
|
* import typo generateAlertEmergencyInfoContent ([d780fb4](https://github.com/alerte-secours/as-app/commit/d780fb4190acc24ceb9811682f8684ffb232a716))
|
||||||
|
* improve error handling ([010aa2c](https://github.com/alerte-secours/as-app/commit/010aa2c2fc0cf80c6afd3eb4fe954f94c94b8bea))
|
||||||
|
* known keys ([be0cd62](https://github.com/alerte-secours/as-app/commit/be0cd62cb943fb4d74243f78df2fb1fc29e97e76))
|
||||||
|
* memoryAsyncStorage ([6e290bd](https://github.com/alerte-secours/as-app/commit/6e290bdb6997dd06ee8d674087368ad68b97a881))
|
||||||
|
* prevent race condition ([6a77336](https://github.com/alerte-secours/as-app/commit/6a773367d49ef66a8a896ba18e63c660f30eb143))
|
||||||
|
* re-enable expo-updates ([16332cb](https://github.com/alerte-secours/as-app/commit/16332cbb764be5f067aa7cb3c2713399d1959f7c))
|
||||||
|
* re-up ([8331029](https://github.com/alerte-secours/as-app/commit/8331029e91220cc9c679dce1058ffc5f2cbb0577))
|
||||||
|
* reduce tracesSampleRate ([b5ae235](https://github.com/alerte-secours/as-app/commit/b5ae235ba4f093451bb7bd41b19aa77a8a0bda96))
|
||||||
|
* sentry + re-enable expo-updates + disable debug ([8e8e120](https://github.com/alerte-secours/as-app/commit/8e8e120391f197e7826edaef47817ff8fb14a6b8))
|
||||||
|
* sentry tracing ([e6924ac](https://github.com/alerte-secours/as-app/commit/e6924ac9ff22b114eff4293f007becebe44e286d))
|
||||||
|
* sentry tracing ([4a0f3ab](https://github.com/alerte-secours/as-app/commit/4a0f3ab7effd79f75efd976f5fd9e1d1531e5b19))
|
||||||
|
* sentry tracing ([8ba4056](https://github.com/alerte-secours/as-app/commit/8ba4056187e6bd9ebf8ceec881197d6dc0aaaebc))
|
||||||
|
* undefined error ([47f11d1](https://github.com/alerte-secours/as-app/commit/47f11d1b888f4372fcb03db9bd1e839495455df3))
|
||||||
|
* undefined error ([d4de0b4](https://github.com/alerte-secours/as-app/commit/d4de0b4541fedb29b3e5161f91fc65b3253308e0))
|
||||||
|
* **upgrade:** expo 52 + rn 0.76.9 ([a1ed6cf](https://github.com/alerte-secours/as-app/commit/a1ed6cfca6217ba2068908f12e78af990f91b9c5))
|
||||||
|
|
||||||
## [1.10.1](https://github.com/alerte-secours/as-app/compare/v1.10.0...v1.10.1) (2025-06-01)
|
## [1.10.1](https://github.com/alerte-secours/as-app/compare/v1.10.0...v1.10.1) (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -83,8 +83,8 @@ android {
|
||||||
applicationId 'com.alertesecours'
|
applicationId 'com.alertesecours'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 181
|
versionCode 201
|
||||||
versionName "1.10.1"
|
versionName "1.11.11"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
|
@ -64,6 +64,10 @@
|
||||||
<action android:name="com.alertesecours.OPEN_RELATIVES"/>
|
<action android:name="com.alertesecours.OPEN_RELATIVES"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.alertesecours.OPEN_BACKGROUND_GEOLOCATION_SETTINGS"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true" data-generated="true">
|
<intent-filter android:autoVerify="true" data-generated="true">
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<data android:scheme="https" android:host="app.alertesecours.fr" android:pathPrefix="/"/>
|
<data android:scheme="https" android:host="app.alertesecours.fr" android:pathPrefix="/"/>
|
||||||
|
|
7
android/app/src/main/res/values-fr/strings.xml
Normal file
7
android/app/src/main/res/values-fr/strings.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Alerte Secours</string>
|
||||||
|
|
||||||
|
<!-- French permission message for background geolocation -->
|
||||||
|
<string name="message_permission_required">Alerte Secours nécessite la localisation en arrière-plan pour les alertes de proximité.</string>
|
||||||
|
<string name="title_permission_required">Autorisation requise</string>
|
||||||
|
</resources>
|
|
@ -4,4 +4,6 @@
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
||||||
<string name="expo_runtime_version">1.0.0</string>
|
<string name="expo_runtime_version">1.0.0</string>
|
||||||
|
<string name="message_permission_required">Alerte Secours nécessite la localisation en arrière-plan pour les alertes de proximité.</string>
|
||||||
|
<string name="title_permission_required">Autorisation requise</string>
|
||||||
</resources>
|
</resources>
|
|
@ -9,7 +9,7 @@ buildscript {
|
||||||
// buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
|
// buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
|
||||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
|
||||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
||||||
|
|
||||||
ndkVersion = "26.1.10909125"
|
ndkVersion = "26.1.10909125"
|
||||||
|
|
|
@ -131,13 +131,14 @@ let config = {
|
||||||
"tel",
|
"tel",
|
||||||
"telprompt",
|
"telprompt",
|
||||||
],
|
],
|
||||||
},
|
|
||||||
UIBackgroundModes: ["location", "fetch", "processing"],
|
|
||||||
BGTaskSchedulerPermittedIdentifiers: [
|
BGTaskSchedulerPermittedIdentifiers: [
|
||||||
|
"com.transistorsoft",
|
||||||
"com.transistorsoft.fetch",
|
"com.transistorsoft.fetch",
|
||||||
"com.transistorsoft.customtask",
|
"com.transistorsoft.customtask",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
UIBackgroundModes: ["location", "fetch", "processing"],
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
"react-native-background-geolocation",
|
"react-native-background-geolocation",
|
||||||
|
|
|
@ -97,3 +97,72 @@ You can run this script directly:
|
||||||
export DEVICE=emulator-5554
|
export DEVICE=emulator-5554
|
||||||
./install-android.sh
|
./install-android.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# enable USB debug mode
|
||||||
|
|
||||||
|
Add udev rules for your device
|
||||||
|
|
||||||
|
1. First, find your device's vendor ID:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lsusb
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for your phone manufacturer (e.g., Google, Samsung, OnePlus). Note the ID like 18d1:4ee7
|
||||||
|
|
||||||
|
2. Create/edit the udev rules file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo micro /etc/udev/rules.d/51-android.rules
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add a line for your device. Here are common manufacturers:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Google
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"
|
||||||
|
# Samsung
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", MODE="0666", GROUP="plugdev"
|
||||||
|
# OnePlus
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="2a70", MODE="0666", GROUP="plugdev"
|
||||||
|
# Xiaomi
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="2717", MODE="0666", GROUP="plugdev"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Set proper permissions and reload rules:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo chmod a+r /etc/udev/rules.d/51-android.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo service udev restart
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Add yourself to the plugdev group:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo usermod -aG plugdev $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Force the authorization prompt
|
||||||
|
|
||||||
|
1. Kill adb server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb kill-server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Unplug your device
|
||||||
|
3. On your phone:
|
||||||
|
- Go to Developer options
|
||||||
|
- Revoke USB debugging authorizations
|
||||||
|
- Toggle USB debugging OFF then ON
|
||||||
|
|
||||||
|
4. Plug the device back in
|
||||||
|
5. Start adb with proper permissions:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb start-server
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
6. The authorization prompt should now appear on your phone
|
449
index.js
449
index.js
|
@ -5,6 +5,9 @@ import "./warnFilter";
|
||||||
import "expo-splash-screen";
|
import "expo-splash-screen";
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import BackgroundFetch from "react-native-background-fetch";
|
||||||
|
|
||||||
import notifee from "@notifee/react-native";
|
import notifee from "@notifee/react-native";
|
||||||
import messaging from "@react-native-firebase/messaging";
|
import messaging from "@react-native-firebase/messaging";
|
||||||
|
|
||||||
|
@ -18,8 +21,7 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
|
||||||
import onMessageReceived from "~/notifications/onMessageReceived";
|
import onMessageReceived from "~/notifications/onMessageReceived";
|
||||||
|
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import * as Sentry from "@sentry/react-native";
|
import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
|
|
||||||
// setup notification, this have to stay in index.js
|
// setup notification, this have to stay in index.js
|
||||||
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
||||||
|
@ -30,405 +32,82 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
|
||||||
// the environment is set up appropriately
|
// the environment is set up appropriately
|
||||||
registerRootComponent(App);
|
registerRootComponent(App);
|
||||||
|
|
||||||
// Constants for persistence
|
|
||||||
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
|
|
||||||
const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
// Helper functions for persisting sync time
|
|
||||||
const getLastSyncTime = async () => {
|
|
||||||
try {
|
|
||||||
const value = await AsyncStorage.getItem(LAST_SYNC_TIME_KEY);
|
|
||||||
return value ? parseInt(value, 10) : Date.now();
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: { module: "headless-task", operation: "get-last-sync-time" },
|
|
||||||
});
|
|
||||||
return Date.now();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLastSyncTime = async (time) => {
|
|
||||||
try {
|
|
||||||
await AsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString());
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: { module: "headless-task", operation: "set-last-sync-time" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
|
||||||
const getCurrentPosition = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Add timeout protection
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
resolve({ code: -1, message: "getCurrentPosition timeout" });
|
|
||||||
}, 15000); // 15 second timeout
|
|
||||||
|
|
||||||
BackgroundGeolocation.getCurrentPosition(
|
|
||||||
{
|
|
||||||
samples: 1,
|
|
||||||
persist: true,
|
|
||||||
extras: { background: true },
|
|
||||||
timeout: 10, // 10 second timeout in the plugin itself
|
|
||||||
},
|
|
||||||
(location) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(location);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const geolocBgLogger = createLogger({
|
const geolocBgLogger = createLogger({
|
||||||
service: "background-geolocation",
|
service: "background-geolocation",
|
||||||
task: "headless",
|
task: "headless",
|
||||||
});
|
});
|
||||||
|
|
||||||
const HeadlessTask = async (event) => {
|
const HeadlessTask = async (event) => {
|
||||||
// Add timeout protection for the entire headless task
|
|
||||||
const taskTimeout = setTimeout(() => {
|
|
||||||
geolocBgLogger.error("HeadlessTask timeout", { event });
|
|
||||||
|
|
||||||
Sentry.captureException(new Error("HeadlessTask timeout"), {
|
|
||||||
tags: {
|
|
||||||
module: "background-geolocation",
|
|
||||||
operation: "headless-task-timeout",
|
|
||||||
eventName: event?.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 60000); // 60 second timeout
|
|
||||||
|
|
||||||
// Simple performance tracking without deprecated APIs
|
|
||||||
const taskStartTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate event structure
|
switch (event?.name) {
|
||||||
if (!event || typeof event !== "object") {
|
|
||||||
throw new Error("Invalid event object received");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, params } = event;
|
|
||||||
|
|
||||||
if (!name || typeof name !== "string") {
|
|
||||||
throw new Error("Invalid event name received");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add initial breadcrumb
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "HeadlessTask started",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
eventName: name,
|
|
||||||
params: params ? JSON.stringify(params) : null,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.info("HeadlessTask event received", { name, params });
|
|
||||||
|
|
||||||
switch (name) {
|
|
||||||
case "heartbeat":
|
case "heartbeat":
|
||||||
// Add breadcrumb for heartbeat event
|
await executeHeartbeatSync();
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Heartbeat event received",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
timestamp: Date.now() / 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get persisted last sync time
|
|
||||||
const lastSyncTime = await getLastSyncTime();
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastSync = now - lastSyncTime;
|
|
||||||
|
|
||||||
// Add context about sync timing
|
|
||||||
Sentry.setContext("sync-timing", {
|
|
||||||
lastSyncTime: new Date(lastSyncTime).toISOString(),
|
|
||||||
currentTime: new Date(now).toISOString(),
|
|
||||||
timeSinceLastSync: timeSinceLastSync,
|
|
||||||
timeSinceLastSyncHours: (
|
|
||||||
timeSinceLastSync /
|
|
||||||
(1000 * 60 * 60)
|
|
||||||
).toFixed(2),
|
|
||||||
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Sync timing calculated",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
timeSinceLastSyncHours: (
|
|
||||||
timeSinceLastSync /
|
|
||||||
(1000 * 60 * 60)
|
|
||||||
).toFixed(2),
|
|
||||||
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get current position with performance tracking
|
|
||||||
const locationStartTime = Date.now();
|
|
||||||
const location = await getCurrentPosition();
|
|
||||||
const locationDuration = Date.now() - locationStartTime;
|
|
||||||
|
|
||||||
const isLocationError = location && location.code !== undefined;
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "getCurrentPosition completed",
|
|
||||||
category: "headless-task",
|
|
||||||
level: isLocationError ? "warning" : "info",
|
|
||||||
data: {
|
|
||||||
success: !isLocationError,
|
|
||||||
error: isLocationError ? location : undefined,
|
|
||||||
coords: !isLocationError ? location?.coords : undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.debug("getCurrentPosition result", { location });
|
|
||||||
|
|
||||||
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
|
||||||
geolocBgLogger.info("Forcing location sync after 24h");
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Force sync triggered",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
timeSinceLastSyncHours: (
|
|
||||||
timeSinceLastSync /
|
|
||||||
(1000 * 60 * 60)
|
|
||||||
).toFixed(2),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get pending records count before sync with timeout
|
|
||||||
const pendingCount = await Promise.race([
|
|
||||||
BackgroundGeolocation.getCount(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("getCount timeout")), 10000),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Pending records count",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: { pendingCount },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change pace to ensure location updates with timeout
|
|
||||||
await Promise.race([
|
|
||||||
BackgroundGeolocation.changePace(true),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("changePace timeout")),
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "changePace completed",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform sync with timeout
|
|
||||||
const syncResult = await Promise.race([
|
|
||||||
BackgroundGeolocation.sync(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("sync timeout")), 20000),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Sync completed successfully",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
syncResult: Array.isArray(syncResult)
|
|
||||||
? `${syncResult.length} records`
|
|
||||||
: "completed",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last sync time after successful sync
|
|
||||||
await setLastSyncTime(now);
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Last sync time updated",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: { newSyncTime: new Date(now).toISOString() },
|
|
||||||
});
|
|
||||||
} catch (syncError) {
|
|
||||||
Sentry.captureException(syncError, {
|
|
||||||
tags: {
|
|
||||||
module: "headless-task",
|
|
||||||
operation: "force-sync",
|
|
||||||
eventName: name,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
syncAttempt: {
|
|
||||||
timeSinceLastSync: timeSinceLastSync,
|
|
||||||
lastSyncTime: new Date(lastSyncTime).toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.error("Force sync failed", { error: syncError });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Force sync not needed",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
timeSinceLastSyncHours: (
|
|
||||||
timeSinceLastSync /
|
|
||||||
(1000 * 60 * 60)
|
|
||||||
).toFixed(2),
|
|
||||||
nextSyncInHours: (
|
|
||||||
(FORCE_SYNC_INTERVAL - timeSinceLastSync) /
|
|
||||||
(1000 * 60 * 60)
|
|
||||||
).toFixed(2),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "location":
|
|
||||||
// Validate location parameters
|
|
||||||
if (!params || typeof params !== "object") {
|
|
||||||
geolocBgLogger.warn("Invalid location params", { params });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Location update received",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
coords: params.location?.coords,
|
|
||||||
activity: params.location?.activity,
|
|
||||||
hasLocation: !!params.location,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.debug("Location update received", {
|
|
||||||
location: params.location,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "http":
|
|
||||||
// Validate HTTP parameters
|
|
||||||
if (!params || typeof params !== "object" || !params.response) {
|
|
||||||
geolocBgLogger.warn("Invalid HTTP params", { params });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpStatus = params.response?.status;
|
|
||||||
const isHttpSuccess = httpStatus === 200;
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "HTTP response received",
|
|
||||||
category: "headless-task",
|
|
||||||
level: isHttpSuccess ? "info" : "warning",
|
|
||||||
data: {
|
|
||||||
status: httpStatus,
|
|
||||||
success: params.response?.success,
|
|
||||||
hasResponse: !!params.response,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.debug("HTTP response received", {
|
|
||||||
response: params.response,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last sync time on successful HTTP response
|
|
||||||
if (isHttpSuccess) {
|
|
||||||
try {
|
|
||||||
const now = Date.now();
|
|
||||||
await setLastSyncTime(now);
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Last sync time updated (HTTP success)",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: { newSyncTime: new Date(now).toISOString() },
|
|
||||||
});
|
|
||||||
} catch (syncTimeError) {
|
|
||||||
geolocBgLogger.error("Failed to update sync time", {
|
|
||||||
error: syncTimeError,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(syncTimeError, {
|
|
||||||
tags: {
|
|
||||||
module: "headless-task",
|
|
||||||
operation: "update-sync-time-http",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Sentry.addBreadcrumb({
|
break;
|
||||||
message: "Unknown event type",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "warning",
|
|
||||||
data: { eventName: name },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task completed successfully
|
|
||||||
const taskDuration = Date.now() - taskStartTime;
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "HeadlessTask completed successfully",
|
|
||||||
category: "headless-task",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
eventName: name,
|
|
||||||
duration: taskDuration,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const taskDuration = Date.now() - taskStartTime;
|
|
||||||
|
|
||||||
// Capture any unexpected errors
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "headless-task",
|
|
||||||
eventName: event?.name || "unknown",
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
duration: taskDuration,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.error("HeadlessTask error", {
|
geolocBgLogger.error("HeadlessTask error", {
|
||||||
error,
|
error,
|
||||||
event,
|
event,
|
||||||
duration: taskDuration,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Clear the timeout
|
|
||||||
clearTimeout(taskTimeout);
|
|
||||||
|
|
||||||
const finalDuration = Date.now() - taskStartTime;
|
|
||||||
geolocBgLogger.debug("HeadlessTask completed", {
|
|
||||||
event: event?.name,
|
|
||||||
duration: finalDuration,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
if (Platform.OS === "android") {
|
||||||
|
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
||||||
|
} else if (Platform.OS === "ios") {
|
||||||
|
BackgroundGeolocation.onLocation(async (_location) => {
|
||||||
|
await executeHeartbeatSync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure BackgroundFetch for iOS (iOS-specific configuration)
|
||||||
|
BackgroundFetch.configure(
|
||||||
|
{
|
||||||
|
minimumFetchInterval: 15, // Only valid option for iOS - gives best chance of execution
|
||||||
|
},
|
||||||
|
// Event callback
|
||||||
|
async (taskId) => {
|
||||||
|
let syncResult = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the shared heartbeat logic and get result
|
||||||
|
syncResult = await executeHeartbeatSync();
|
||||||
|
} catch (error) {
|
||||||
|
// silent error
|
||||||
|
} finally {
|
||||||
|
// CRITICAL: Always call finish with appropriate result
|
||||||
|
try {
|
||||||
|
if (taskId) {
|
||||||
|
let fetchResult;
|
||||||
|
|
||||||
|
if (syncResult?.error || !syncResult?.syncSuccessful) {
|
||||||
|
// Task failed
|
||||||
|
fetchResult = BackgroundFetch.FETCH_RESULT_FAILED;
|
||||||
|
} else if (
|
||||||
|
syncResult?.syncPerformed &&
|
||||||
|
syncResult?.syncSuccessful
|
||||||
|
) {
|
||||||
|
// Force sync was performed successfully - new data
|
||||||
|
fetchResult = BackgroundFetch.FETCH_RESULT_NEW_DATA;
|
||||||
|
} else {
|
||||||
|
// No sync was needed - no new data
|
||||||
|
fetchResult = BackgroundFetch.FETCH_RESULT_NO_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackgroundFetch.finish(taskId, fetchResult);
|
||||||
|
}
|
||||||
|
} catch (finishError) {
|
||||||
|
// silent error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Timeout callback (REQUIRED by BackgroundFetch API)
|
||||||
|
async (taskId) => {
|
||||||
|
// CRITICAL: Must call finish on timeout with FAILED result
|
||||||
|
BackgroundFetch.finish(taskId, BackgroundFetch.FETCH_RESULT_FAILED);
|
||||||
|
},
|
||||||
|
).catch(() => {
|
||||||
|
// silent error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
249
install-ios.sh
Executable file
249
install-ios.sh
Executable file
|
@ -0,0 +1,249 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}Error: $1${NC}" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}$1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}Warning: $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}$1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_plain() {
|
||||||
|
echo -e "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if Xcode command line tools are available
|
||||||
|
if ! command -v xcrun &> /dev/null; then
|
||||||
|
print_error "Xcode command line tools are not installed."
|
||||||
|
print_info "Please install them by running: xcode-select --install"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Define IPA path
|
||||||
|
IPA_PATH="ios/build/AlerteSecours.ipa"
|
||||||
|
|
||||||
|
# Check if IPA file exists
|
||||||
|
if [ ! -f "$IPA_PATH" ]; then
|
||||||
|
print_error "IPA file not found at: $IPA_PATH"
|
||||||
|
print_info "Please build the iOS bundle first by running:"
|
||||||
|
print_info " yarn bundle:ios"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "Found IPA file: $IPA_PATH"
|
||||||
|
|
||||||
|
# Function to get connected physical iOS devices
|
||||||
|
get_physical_devices() {
|
||||||
|
xcrun devicectl list devices 2>/dev/null | awk 'NR>2 && $4=="available" {print $3}' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get available simulators (booted ones)
|
||||||
|
get_booted_simulators() {
|
||||||
|
xcrun simctl list devices | grep -E "\(Booted\)" | sed -E 's/.*\(([A-F0-9-]{36})\) \(Booted\)/\1/' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get simulator name by UDID
|
||||||
|
get_simulator_name() {
|
||||||
|
local udid="$1"
|
||||||
|
xcrun simctl list devices | grep "$udid" | sed -E 's/^[[:space:]]*([^(]+).*/\1/' | xargs
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get device name by UDID (for physical devices)
|
||||||
|
get_device_name() {
|
||||||
|
local udid="$1"
|
||||||
|
xcrun devicectl list devices 2>/dev/null | awk -v target_udid="$udid" 'NR>2 && $3==target_udid {print $1}' || echo "Unknown Device"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to validate physical device UDID
|
||||||
|
validate_physical_device() {
|
||||||
|
local device_id="$1"
|
||||||
|
local devices=$(get_physical_devices)
|
||||||
|
|
||||||
|
if [ -z "$devices" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$devices" | grep -q "^$device_id$"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to validate simulator UDID
|
||||||
|
validate_simulator() {
|
||||||
|
local simulator_id="$1"
|
||||||
|
local simulators=$(get_booted_simulators)
|
||||||
|
|
||||||
|
if [ -z "$simulators" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$simulators" | grep -q "^$simulator_id$"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install on physical device
|
||||||
|
install_on_device() {
|
||||||
|
local device_id="$1"
|
||||||
|
local device_name=$(get_device_name "$device_id")
|
||||||
|
|
||||||
|
print_info "Installing on physical device: $device_name ($device_id)"
|
||||||
|
|
||||||
|
if xcrun devicectl device install app --device "$device_id" "$IPA_PATH"; then
|
||||||
|
print_success "Installation completed successfully on $device_name!"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Installation failed on $device_name"
|
||||||
|
print_info "Common solutions:"
|
||||||
|
print_info " 1. Make sure the device is unlocked and trusted"
|
||||||
|
print_info " 2. Check that the provisioning profile matches the device"
|
||||||
|
print_info " 3. Verify the device has enough storage space"
|
||||||
|
print_info " 4. Try disconnecting and reconnecting the device"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install on simulator
|
||||||
|
install_on_simulator() {
|
||||||
|
local simulator_id="$1"
|
||||||
|
local simulator_name=$(get_simulator_name "$simulator_id")
|
||||||
|
|
||||||
|
print_info "Installing on simulator: $simulator_name ($simulator_id)"
|
||||||
|
|
||||||
|
if xcrun simctl install "$simulator_id" "$IPA_PATH"; then
|
||||||
|
print_success "Installation completed successfully on $simulator_name!"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Installation failed on $simulator_name"
|
||||||
|
print_info "Make sure the simulator is booted and try again"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation logic
|
||||||
|
if [ -n "$IOS_DEVICE" ]; then
|
||||||
|
# Specific physical device requested
|
||||||
|
print_info "Using specified physical device: $IOS_DEVICE"
|
||||||
|
|
||||||
|
if ! validate_physical_device "$IOS_DEVICE"; then
|
||||||
|
print_error "Physical device $IOS_DEVICE is not connected or not found."
|
||||||
|
print_info "Connected physical devices:"
|
||||||
|
physical_devices=$(get_physical_devices)
|
||||||
|
if [ -n "$physical_devices" ]; then
|
||||||
|
echo "$physical_devices" | while read -r device; do
|
||||||
|
device_name=$(get_device_name "$device")
|
||||||
|
print_info " - $device ($device_name)"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
print_info " No physical devices found"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_on_device "$IOS_DEVICE"
|
||||||
|
|
||||||
|
elif [ -n "$IOS_SIMULATOR" ]; then
|
||||||
|
# Specific simulator requested
|
||||||
|
print_info "Using specified simulator: $IOS_SIMULATOR"
|
||||||
|
|
||||||
|
if ! validate_simulator "$IOS_SIMULATOR"; then
|
||||||
|
print_error "Simulator $IOS_SIMULATOR is not booted or not found."
|
||||||
|
print_info "Booted simulators:"
|
||||||
|
booted_simulators=$(get_booted_simulators)
|
||||||
|
if [ -n "$booted_simulators" ]; then
|
||||||
|
echo "$booted_simulators" | while read -r sim; do
|
||||||
|
sim_name=$(get_simulator_name "$sim")
|
||||||
|
print_info " - $sim ($sim_name)"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
print_info " No booted simulators found"
|
||||||
|
print_info " Start a simulator from Xcode or run: xcrun simctl boot <simulator-udid>"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_on_simulator "$IOS_SIMULATOR"
|
||||||
|
|
||||||
|
else
|
||||||
|
# Auto-detect: prefer physical devices, fallback to simulators
|
||||||
|
print_info "Auto-detecting iOS targets..."
|
||||||
|
|
||||||
|
# Try physical devices first
|
||||||
|
physical_devices=$(get_physical_devices)
|
||||||
|
if [ -n "$physical_devices" ]; then
|
||||||
|
target_device=$(echo "$physical_devices" | head -n 1)
|
||||||
|
device_count=$(echo "$physical_devices" | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$device_count" -gt 1 ]; then
|
||||||
|
print_warning "Multiple physical devices found. Using first device: $target_device"
|
||||||
|
print_info "Available physical devices:"
|
||||||
|
echo "$physical_devices" | while read -r device; do
|
||||||
|
device_name=$(get_device_name "$device")
|
||||||
|
if [ "$device" = "$target_device" ]; then
|
||||||
|
print_info " - $device ($device_name) [selected]"
|
||||||
|
else
|
||||||
|
print_info " - $device ($device_name)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
print_info "To use a specific device, run: IOS_DEVICE=<device-udid> yarn install:ios"
|
||||||
|
else
|
||||||
|
device_name=$(get_device_name "$target_device")
|
||||||
|
print_info "Using physical device: $device_name ($target_device)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_on_device "$target_device"
|
||||||
|
else
|
||||||
|
# No physical devices, try simulators
|
||||||
|
print_info "No physical devices found. Looking for booted simulators..."
|
||||||
|
|
||||||
|
booted_simulators=$(get_booted_simulators)
|
||||||
|
if [ -n "$booted_simulators" ]; then
|
||||||
|
target_simulator=$(echo "$booted_simulators" | head -n 1)
|
||||||
|
simulator_count=$(echo "$booted_simulators" | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$simulator_count" -gt 1 ]; then
|
||||||
|
print_warning "Multiple booted simulators found. Using first simulator: $target_simulator"
|
||||||
|
print_info "Available booted simulators:"
|
||||||
|
echo "$booted_simulators" | while read -r sim; do
|
||||||
|
sim_name=$(get_simulator_name "$sim")
|
||||||
|
if [ "$sim" = "$target_simulator" ]; then
|
||||||
|
print_info " - $sim ($sim_name) [selected]"
|
||||||
|
else
|
||||||
|
print_info " - $sim ($sim_name)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
print_info "To use a specific simulator, run: IOS_SIMULATOR=<simulator-udid> yarn install:ios"
|
||||||
|
else
|
||||||
|
simulator_name=$(get_simulator_name "$target_simulator")
|
||||||
|
print_info "Using simulator: $simulator_name ($target_simulator)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_on_simulator "$target_simulator"
|
||||||
|
else
|
||||||
|
print_error "No iOS devices or booted simulators found."
|
||||||
|
print_info "Please either:"
|
||||||
|
print_info " 1. Connect and trust an iOS device, or"
|
||||||
|
print_info " 2. Boot a simulator from Xcode"
|
||||||
|
print_info ""
|
||||||
|
print_info "Usage examples:"
|
||||||
|
print_info " Auto-detect: yarn install:ios"
|
||||||
|
print_info " Specific device: IOS_DEVICE=<device-udid> yarn install:ios"
|
||||||
|
print_info " Specific simulator: IOS_SIMULATOR=<simulator-udid> yarn install:ios"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
|
@ -1,16 +1,11 @@
|
||||||
# This is used by the React Native CLI to control various options
|
# This `.xcode.env` file is versioned and is used to source the environment
|
||||||
# Configuration name to load
|
# used when running script phases inside Xcode.
|
||||||
CONFIGURATION_NAME=Debug
|
# To customize your local environment, you can create an `.xcode.env.local`
|
||||||
# Path to the Xcode project
|
# file that is not versioned.
|
||||||
PROJECT_PATH="AlerteSecours.xcodeproj"
|
|
||||||
# Whether to enable the New Architecture
|
|
||||||
RCT_NEW_ARCH_ENABLED=0
|
|
||||||
# Whether to enable Hermes
|
|
||||||
USE_HERMES=1
|
|
||||||
|
|
||||||
# Sentry Configuration
|
# NODE_BINARY variable contains the PATH to the node executable.
|
||||||
export SENTRY_PROPERTIES="ios/sentry.properties"
|
#
|
||||||
export AUTO_RELEASE=true
|
# Customize the NODE_BINARY variable here.
|
||||||
export SENTRY_CLI_EXTRA_ARGS="--log-level debug"
|
# For example, to use nvm with brew, add the following line
|
||||||
export SENTRY_CLI_RN_XCODE_EXTRA_ARGS="--allow-fetch"
|
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||||
export SENTRY_INCLUDE_NATIVE_SOURCES=true
|
export NODE_BINARY=$(command -v node)
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -44,7 +44,7 @@
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "1"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import <Firebase/Firebase.h>
|
|
||||||
// @generated begin react-native-background-fetch-import - expo prebuild (DO NOT MODIFY) sync-fb890e6efd6cc6e67ebbda1087e0a6d7e0bcc527
|
// @generated begin react-native-background-fetch-import - expo prebuild (DO NOT MODIFY) sync-fb890e6efd6cc6e67ebbda1087e0a6d7e0bcc527
|
||||||
#import <TSBackgroundFetch/TSBackgroundFetch.h>
|
#import <TSBackgroundFetch/TSBackgroundFetch.h>
|
||||||
// @generated end react-native-background-fetch-import
|
// @generated end react-native-background-fetch-import
|
||||||
|
#import <Firebase/Firebase.h>
|
||||||
|
|
||||||
#import <React/RCTBundleURLProvider.h>
|
#import <React/RCTBundleURLProvider.h>
|
||||||
#import <React/RCTLinkingManager.h>
|
#import <React/RCTLinkingManager.h>
|
||||||
|
|
||||||
#import <Firebase.h>
|
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||||
{
|
{
|
||||||
|
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
|
||||||
|
[FIRApp configure];
|
||||||
|
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
|
||||||
self.moduleName = @"main";
|
self.moduleName = @"main";
|
||||||
|
|
||||||
// see https://github.com/invertase/react-native-firebase/issues/7788#issuecomment-2211820768
|
|
||||||
// and https://rnfirebase.io/#configure-react-native-firebase-modules
|
|
||||||
[FIRApp configure];
|
|
||||||
|
|
||||||
// You can add your custom initial props in the dictionary below.
|
// You can add your custom initial props in the dictionary below.
|
||||||
// They will be passed down to the ViewController used by React Native.
|
// They will be passed down to the ViewController used by React Native.
|
||||||
self.initialProps = @{};
|
self.initialProps = @{};
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>API_KEY</key>
|
|
||||||
<string>YOUR_API_KEY_HERE</string>
|
|
||||||
<key>GCM_SENDER_ID</key>
|
|
||||||
<string>YOUR_GCM_SENDER_ID_HERE</string>
|
|
||||||
<key>PLIST_VERSION</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>BUNDLE_ID</key>
|
|
||||||
<string>com.alertesecours.alertesecours</string>
|
|
||||||
<key>PROJECT_ID</key>
|
|
||||||
<string>YOUR_PROJECT_ID_HERE</string>
|
|
||||||
<key>STORAGE_BUCKET</key>
|
|
||||||
<string>YOUR_STORAGE_BUCKET_HERE</string>
|
|
||||||
<key>IS_ADS_ENABLED</key>
|
|
||||||
<false></false>
|
|
||||||
<key>IS_ANALYTICS_ENABLED</key>
|
|
||||||
<false></false>
|
|
||||||
<key>IS_APPINVITE_ENABLED</key>
|
|
||||||
<true></true>
|
|
||||||
<key>IS_GCM_ENABLED</key>
|
|
||||||
<true></true>
|
|
||||||
<key>IS_SIGNIN_ENABLED</key>
|
|
||||||
<true></true>
|
|
||||||
<key>GOOGLE_APP_ID</key>
|
|
||||||
<string>YOUR_GOOGLE_APP_ID_HERE</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"filename": "image.png",
|
|
||||||
"scale": "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"version": 1,
|
|
||||||
"author": "expo"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 160 KiB |
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"filename": "image.png",
|
|
||||||
"scale": "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"scale": "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"version": 1,
|
|
||||||
"author": "expo"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 70 B |
|
@ -2,6 +2,12 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>com.transistorsoft</string>
|
||||||
|
<string>com.transistorsoft.fetch</string>
|
||||||
|
<string>com.transistorsoft.customtask</string>
|
||||||
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
@ -19,7 +25,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.10.1</string>
|
<string>1.11.11</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
@ -42,7 +48,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>181</string>
|
<string>201</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
@ -89,23 +95,14 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>alertesecours.fr</key>
|
<key>alertesecours.fr</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSIncludesSubdomains</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSExceptionRequiresForwardSecrecy</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSExceptionMinimumTLSVersion</key>
|
<key>NSExceptionMinimumTLSVersion</key>
|
||||||
<string>TLSv1.0</string>
|
<string>TLSv1.0</string>
|
||||||
</dict>
|
<key>NSExceptionRequiresForwardSecrecy</key>
|
||||||
<key>sentry.io</key>
|
<false/>
|
||||||
<dict>
|
|
||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExceptionRequiresForwardSecrecy</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSExceptionMinimumTLSVersion</key>
|
|
||||||
<string>TLSv1.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>localhost</key>
|
<key>localhost</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -114,6 +111,15 @@
|
||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>sentry.io</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionMinimumTLSVersion</key>
|
||||||
|
<string>TLSv1.0</string>
|
||||||
|
<key>NSExceptionRequiresForwardSecrecy</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
|
@ -145,6 +151,7 @@
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>location</string>
|
<string>location</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
|
||||||
<device id="retina5_5" orientation="portrait" appearance="light"/>
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -12,34 +13,29 @@
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
|
||||||
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
|
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" image="SplashScreenBackground" translatesAutoresizingMaskIntoConstraints="NO" id="EXPO-SplashScreenBackground" userLabel="SplashScreenBackground">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
|
|
||||||
</imageView>
|
|
||||||
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
|
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
|
||||||
<rect key="frame" x="0" y="0" width="414" height="736"/>
|
<rect key="frame" x="0" y="0" width="414" height="736"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
|
||||||
<color key="backgroundColor" name="SplashScreenBackground"/>
|
<color key="backgroundColor" name="SplashScreenBackground"/>
|
||||||
</view>
|
</view>
|
||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="140.625" y="129.4921875"/>
|
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="SplashScreenBackground" width="1" height="1"/>
|
|
||||||
<image name="SplashScreen" width="414" height="736"/>
|
|
||||||
<image name="SplashScreenLogo" width="414" height="736"/>
|
<image name="SplashScreenLogo" width="414" height="736"/>
|
||||||
<namedColor name="SplashScreenBackground">
|
<namedColor name="SplashScreenBackground">
|
||||||
<color alpha="1.000" blue="0.780392156862745" green="0.309803921568627" red="0.211764705882353" customColorSpace="sRGB" colorSpace="custom"/>
|
<color alpha="1.000" blue="0.780392156862745" green="0.309803921568627" red="0.211764705882353" customColorSpace="sRGB" colorSpace="custom"/>
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>EXUpdatesCheckOnLaunch</key>
|
|
||||||
<string>ERROR_RECOVERY_ONLY</string>
|
|
||||||
<key>EXUpdatesCodeSigningCertificate</key>
|
|
||||||
<string>YOUR_CODE_SIGNING_CERTIFICATE_HERE</string>
|
|
||||||
<key>EXUpdatesCodeSigningMetadata</key>
|
|
||||||
<dict>
|
|
||||||
<key>keyid</key>
|
|
||||||
<string>main</string>
|
|
||||||
<key>alg</key>
|
|
||||||
<string>rsa-v1_5-sha256</string>
|
|
||||||
</dict>
|
|
||||||
<key>EXUpdatesEnabled</key>
|
|
||||||
<true/>
|
|
||||||
<key>EXUpdatesLaunchWaitMs</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
<key>EXUpdatesRuntimeVersion</key>
|
|
||||||
<string>1.0.0</string>
|
|
||||||
<key>EXUpdatesURL</key>
|
|
||||||
<string>https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&channel=release</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>API_KEY</key>
|
|
||||||
<string>YOUR_API_KEY_HERE</string>
|
|
||||||
<key>GCM_SENDER_ID</key>
|
|
||||||
<string>YOUR_GCM_SENDER_ID_HERE</string>
|
|
||||||
<key>PLIST_VERSION</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>BUNDLE_ID</key>
|
|
||||||
<string>com.alertesecours.alertesecours</string>
|
|
||||||
<key>PROJECT_ID</key>
|
|
||||||
<string>YOUR_PROJECT_ID_HERE</string>
|
|
||||||
<key>STORAGE_BUCKET</key>
|
|
||||||
<string>YOUR_STORAGE_BUCKET_HERE</string>
|
|
||||||
<key>IS_ADS_ENABLED</key>
|
|
||||||
<false></false>
|
|
||||||
<key>IS_ANALYTICS_ENABLED</key>
|
|
||||||
<false></false>
|
|
||||||
<key>IS_APPINVITE_ENABLED</key>
|
|
||||||
<true></true>
|
|
||||||
<key>IS_GCM_ENABLED</key>
|
|
||||||
<true></true>
|
|
||||||
<key>IS_SIGNIN_ENABLED</key>
|
|
||||||
<true></true>
|
|
||||||
<key>GOOGLE_APP_ID</key>
|
|
||||||
<string>YOUR_GOOGLE_APP_ID_HERE</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
100
ios/Podfile
100
ios/Podfile
|
@ -1,19 +1,5 @@
|
||||||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||||
|
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||||
|
|
||||||
# see https://github.com/zoontek/react-native-permissions?tab=readme-ov-file#ios
|
|
||||||
# require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
|
||||||
def node_require(script)
|
|
||||||
# Resolve script with node to allow for hoisting
|
|
||||||
require Pod::Executable.execute_command('node', ['-p',
|
|
||||||
"require.resolve(
|
|
||||||
'#{script}',
|
|
||||||
{paths: [process.argv[1]]},
|
|
||||||
)", __dir__]).strip
|
|
||||||
end
|
|
||||||
# Use it to require both react-native's and this package's scripts:
|
|
||||||
node_require('react-native/scripts/react_native_pods.rb')
|
|
||||||
node_require('react-native-permissions/scripts/setup.rb')
|
|
||||||
|
|
||||||
require 'json'
|
require 'json'
|
||||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||||
|
@ -21,11 +7,18 @@ podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties
|
||||||
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
|
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
|
||||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
|
|
||||||
use_autolinking_method_symbol = ('use' + '_native' + '_modules!').to_sym
|
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||||
origin_autolinking_method = self.method(use_autolinking_method_symbol)
|
install! 'cocoapods',
|
||||||
self.define_singleton_method(use_autolinking_method_symbol) do |*args|
|
:deterministic_uuids => false
|
||||||
if ENV['EXPO_UNSTABLE_CORE_AUTOLINKING'] == '1'
|
|
||||||
Pod::UI.puts('Using expo-modules-autolinking as core autolinking source'.green)
|
prepare_react_native_project!
|
||||||
|
|
||||||
|
target 'AlerteSecours' do
|
||||||
|
use_expo_modules!
|
||||||
|
|
||||||
|
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||||
|
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||||
|
else
|
||||||
config_command = [
|
config_command = [
|
||||||
'node',
|
'node',
|
||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
|
@ -36,53 +29,13 @@ self.define_singleton_method(use_autolinking_method_symbol) do |*args|
|
||||||
'--platform',
|
'--platform',
|
||||||
'ios'
|
'ios'
|
||||||
]
|
]
|
||||||
origin_autolinking_method.call(config_command)
|
|
||||||
else
|
|
||||||
origin_autolinking_method.call()
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
platform :ios, '13.4'
|
config = use_native_modules!(config_command)
|
||||||
install! 'cocoapods',
|
|
||||||
:deterministic_uuids => false
|
|
||||||
|
|
||||||
prepare_react_native_project!
|
|
||||||
|
|
||||||
# ⬇️ uncomment the permissions you need
|
|
||||||
setup_permissions([
|
|
||||||
# 'AppTrackingTransparency',
|
|
||||||
# 'Bluetooth',
|
|
||||||
# 'Calendars',
|
|
||||||
# 'CalendarsWriteOnly',
|
|
||||||
'Camera',
|
|
||||||
'Contacts',
|
|
||||||
# 'FaceID',
|
|
||||||
'LocationAccuracy',
|
|
||||||
'LocationAlways',
|
|
||||||
'LocationWhenInUse',
|
|
||||||
# 'MediaLibrary',
|
|
||||||
'Microphone',
|
|
||||||
# 'Motion',
|
|
||||||
'Notifications',
|
|
||||||
'PhotoLibrary',
|
|
||||||
# 'PhotoLibraryAddOnly',
|
|
||||||
# 'Reminders',
|
|
||||||
# 'Siri',
|
|
||||||
# 'SpeechRecognition',
|
|
||||||
# 'StoreKit',
|
|
||||||
])
|
|
||||||
|
|
||||||
target 'AlerteSecours' do
|
|
||||||
use_expo_modules!
|
|
||||||
config = use_native_modules!
|
|
||||||
|
|
||||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||||
|
|
||||||
# see https://rnfirebase.io/#configure-react-native-firebase-modules
|
|
||||||
use_frameworks! :linkage => :static
|
|
||||||
$RNFirebaseAsStaticFramework = true
|
|
||||||
|
|
||||||
use_react_native!(
|
use_react_native!(
|
||||||
:path => config[:reactNativePath],
|
:path => config[:reactNativePath],
|
||||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||||
|
@ -112,30 +65,5 @@ target 'AlerteSecours' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set deployment target for all pods
|
|
||||||
installer.pods_project.targets.each do |target|
|
|
||||||
target.build_configurations.each do |config|
|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.4'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
installer.pods_project.build_configurations.each do |config|
|
|
||||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post_integrate do |installer|
|
|
||||||
begin
|
|
||||||
expo_patch_react_imports!(installer)
|
|
||||||
rescue => e
|
|
||||||
Pod::UI.warn e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# pod 'Firebase', :modular_headers => true
|
|
||||||
# pod 'FirebaseCoreInternal', :modular_headers => true
|
|
||||||
# pod 'GoogleUtilities', :modular_headers => true
|
|
||||||
# pod 'FirebaseCore', :modular_headers => true
|
|
||||||
# pod 'FirebaseMessaging', :modular_headers => true
|
|
||||||
|
|
3271
ios/Podfile.lock
3271
ios/Podfile.lock
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"expo.jsEngine": "hermes",
|
"expo.jsEngine": "hermes",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
|
"newArchEnabled": "false",
|
||||||
"ios.useFrameworks": "static",
|
"ios.useFrameworks": "static",
|
||||||
"apple.extraPods": "[]",
|
"apple.extraPods": "[]",
|
||||||
"apple.ccacheEnabled": "false",
|
"apple.ccacheEnabled": "false",
|
||||||
"apple.privacyManifestAggregationEnabled": "true",
|
"apple.privacyManifestAggregationEnabled": "true"
|
||||||
"newArchEnabled": "false"
|
|
||||||
}
|
}
|
||||||
|
|
22
ios/RNBackgroundFetch+AppDelegate.m
Normal file
22
ios/RNBackgroundFetch+AppDelegate.m
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// RNBackgroundGeolocation+AppDelegate.m
|
||||||
|
// RNBackgroundGeolocationSample
|
||||||
|
//
|
||||||
|
// Created by Christopher Scott on 2016-08-01.
|
||||||
|
// Copyright © 2016 Facebook. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
#import <TSBackgroundFetch/TSBackgroundFetch.h>
|
||||||
|
|
||||||
|
@implementation AppDelegate(AppDelegate)
|
||||||
|
|
||||||
|
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
|
||||||
|
{
|
||||||
|
NSLog(@"RNBackgroundFetch AppDelegate received fetch event");
|
||||||
|
TSBackgroundFetch *fetchManager = [TSBackgroundFetch sharedInstance];
|
||||||
|
[fetchManager performFetchWithCompletionHandler:completionHandler applicationState:application.applicationState];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>IDEDidComputeMac32BitWarning</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
13
package.json
13
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "alerte-secours",
|
"name": "alerte-secours",
|
||||||
"version": "1.10.1",
|
"version": "1.11.11",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
"postinstall": "link-module-alias",
|
"postinstall": "link-module-alias",
|
||||||
"prepare": "husky || true",
|
"prepare": "husky || true",
|
||||||
"deep-clean": "./node_modules/.bin/react-native-clean-project --keep-node-modules --remove-iOS-build --keep-brew --keep-pods --remove-iOS-pods --remove-android-build && yarn clean",
|
"deep-clean": "./node_modules/.bin/react-native-clean-project --keep-node-modules --remove-iOS-build --keep-brew --keep-pods --remove-iOS-pods --remove-android-build && yarn clean",
|
||||||
"clean": "\\rm -fr ./node_modules && \\rm -fr dist/* && \\rm -fr ios/build ios/Pods ios/KScoreApp.xcarchive && \\rm -fr android/build android/app/build",
|
"clean": "rm -rf ./node_modules dist/* ios/build ios/Pods ios/KScoreApp.xcarchive android/build android/app/build || true",
|
||||||
"prebuild": "expo prebuild && yarn prebuild:hackfix",
|
"prebuild": "expo prebuild && yarn prebuild:hackfix",
|
||||||
"prebuild:hackfix": "node scripts/removeDuplicates.js",
|
"prebuild:hackfix": "node scripts/removeDuplicates.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
@ -37,6 +37,7 @@
|
||||||
"e2e:test": "detox test --configuration android.emu.debug",
|
"e2e:test": "detox test --configuration android.emu.debug",
|
||||||
"e2e:run": "yarn start & yarn e2e:build && yarn e2e:deploy && yarn e2e:test",
|
"e2e:run": "yarn start & yarn e2e:build && yarn e2e:deploy && yarn e2e:test",
|
||||||
"install:android": "./install-android.sh",
|
"install:android": "./install-android.sh",
|
||||||
|
"install:ios": "./install-ios.sh",
|
||||||
"log:android": "adb -s $DEVICE logcat | grep -E 'ReactNativeJS: '",
|
"log:android": "adb -s $DEVICE logcat | grep -E 'ReactNativeJS: '",
|
||||||
"log:ios:simulator": "xcrun simctl spawn booted log stream --level debug --predicate 'subsystem contains \"com.facebook.react.log\" and processImagePath contains \"AlerteSecours\"'",
|
"log:ios:simulator": "xcrun simctl spawn booted log stream --level debug --predicate 'subsystem contains \"com.facebook.react.log\" and processImagePath contains \"AlerteSecours\"'",
|
||||||
"log:ios": "idevicesyslog | grep -i 'AlerteSecours\\|ReactNative'",
|
"log:ios": "idevicesyslog | grep -i 'AlerteSecours\\|ReactNative'",
|
||||||
|
@ -49,8 +50,8 @@
|
||||||
"screenshot:android": "scripts/screenshot-android.sh"
|
"screenshot:android": "scripts/screenshot-android.sh"
|
||||||
},
|
},
|
||||||
"customExpoVersioning": {
|
"customExpoVersioning": {
|
||||||
"versionCode": 181,
|
"versionCode": 201,
|
||||||
"buildNumber": 181
|
"buildNumber": 201
|
||||||
},
|
},
|
||||||
"commit-and-tag-version": {
|
"commit-and-tag-version": {
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -108,8 +109,8 @@
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
"ajv-keywords": "^5.1.0",
|
"ajv-keywords": "^5.1.0",
|
||||||
"apollo-link-sentry": "^4.0.0",
|
"apollo-link-sentry": "^4.0.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.10.0",
|
||||||
"axios-retry": "^3.5.1",
|
"axios-retry": "^4.5.0",
|
||||||
"base62str": "^1.0.10",
|
"base62str": "^1.0.10",
|
||||||
"country-codes-list": "^1.6.11",
|
"country-codes-list": "^1.6.11",
|
||||||
"delay": "^6.0.0",
|
"delay": "^6.0.0",
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { ErrorUtils } from "react-native";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
import { authActions, permissionWizardActions } from "~/stores";
|
import { authActions, permissionWizardActions, paramsActions } from "~/stores";
|
||||||
|
import { secureStore } from "~/storage/memorySecureStore";
|
||||||
|
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
|
||||||
import "~/lib/mapbox";
|
import "~/lib/mapbox";
|
||||||
import "~/i18n";
|
import "~/i18n";
|
||||||
|
@ -42,7 +44,7 @@ const initializeStores = () => {
|
||||||
// Initialize each store with error handling
|
// Initialize each store with error handling
|
||||||
const initializeStore = async (name, initFn) => {
|
const initializeStore = async (name, initFn) => {
|
||||||
try {
|
try {
|
||||||
await Promise.resolve(initFn());
|
await initFn();
|
||||||
appLogger.debug(`${name} initialized successfully`);
|
appLogger.debug(`${name} initialized successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lifecycleLogger.error(`Failed to initialize ${name}`, {
|
lifecycleLogger.error(`Failed to initialize ${name}`, {
|
||||||
|
@ -53,9 +55,14 @@ const initializeStores = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize stores sequentially to maintain order
|
// Initialize memory stores first
|
||||||
|
initializeStore("memorySecureStore", secureStore.init);
|
||||||
|
initializeStore("memoryAsyncStorage", memoryAsyncStorage.init);
|
||||||
|
|
||||||
|
// Then initialize other stores sequentially
|
||||||
initializeStore("authActions", authActions.init);
|
initializeStore("authActions", authActions.init);
|
||||||
initializeStore("permissionWizard", permissionWizardActions.init);
|
initializeStore("permissionWizard", permissionWizardActions.init);
|
||||||
|
initializeStore("paramsActions", paramsActions.init);
|
||||||
initializeStore("storeSubscriptions", storeSubscriptions.init);
|
initializeStore("storeSubscriptions", storeSubscriptions.init);
|
||||||
|
|
||||||
appLogger.info("Core initialization complete");
|
appLogger.info("Core initialization complete");
|
||||||
|
|
|
@ -39,13 +39,18 @@ export async function loginUserToken({ authToken }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function storeFcmToken({ deviceId, fcmToken }) {
|
export async function storeFcmToken({ deviceId, fcmToken }) {
|
||||||
const { data } = await network.apolloClient.mutate({
|
const { data, errors } = await network.apolloClient.mutate({
|
||||||
mutation: STORE_FCM_TOKEN_MUTATION,
|
mutation: STORE_FCM_TOKEN_MUTATION,
|
||||||
variables: {
|
variables: {
|
||||||
deviceId,
|
deviceId,
|
||||||
fcmToken,
|
fcmToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
// Concatenate all error messages
|
||||||
|
const message = errors.map((err) => err.message).join("; ");
|
||||||
|
throw new Error(`GraphQL Error: ${message}`);
|
||||||
|
}
|
||||||
const { updatedAt } = data.updateOneDevice;
|
const { updatedAt } = data.updateOneDevice;
|
||||||
return { updatedAt };
|
return { updatedAt };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,50 @@
|
||||||
import { secureStore } from "~/lib/secureStore";
|
import { secureStore } from "~/storage/memorySecureStore";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import uuidGenerator from "react-native-uuid";
|
import uuidGenerator from "react-native-uuid";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const deviceLogger = createLogger({
|
||||||
|
module: FEATURE_SCOPES.AUTH,
|
||||||
|
feature: "device-uuid",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutex lock for atomic UUID generation
|
||||||
|
let uuidGenerationPromise = null;
|
||||||
|
|
||||||
async function getDeviceUuid() {
|
async function getDeviceUuid() {
|
||||||
let deviceUuid = await secureStore.getItemAsync("deviceUuid");
|
// If a UUID generation is already in progress, wait for it
|
||||||
if (!deviceUuid) {
|
if (uuidGenerationPromise) {
|
||||||
deviceUuid = uuidGenerator.v4();
|
deviceLogger.debug("UUID generation already in progress, waiting...");
|
||||||
await secureStore.setItemAsync("deviceUuid", deviceUuid);
|
return await uuidGenerationPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new promise for this generation attempt
|
||||||
|
uuidGenerationPromise = (async () => {
|
||||||
|
try {
|
||||||
|
let deviceUuid = await secureStore.getItemAsync(STORAGE_KEYS.DEVICE_UUID);
|
||||||
|
|
||||||
|
if (!deviceUuid) {
|
||||||
|
deviceLogger.info("No device UUID found, generating new one");
|
||||||
|
deviceUuid = uuidGenerator.v4();
|
||||||
|
await secureStore.setItemAsync(STORAGE_KEYS.DEVICE_UUID, deviceUuid);
|
||||||
|
deviceLogger.info("New device UUID generated and stored", {
|
||||||
|
uuid: deviceUuid.substring(0, 8) + "...",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
deviceLogger.debug("Device UUID retrieved", {
|
||||||
|
uuid: deviceUuid.substring(0, 8) + "...",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return deviceUuid;
|
return deviceUuid;
|
||||||
|
} finally {
|
||||||
|
// Clear the promise so future calls can proceed
|
||||||
|
uuidGenerationPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return await uuidGenerationPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getDeviceUuid };
|
export { getDeviceUuid };
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { View, ScrollView, StyleSheet, Platform } from "react-native";
|
import { View, ScrollView, StyleSheet, Platform } from "react-native";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
|
|
||||||
|
@ -64,14 +65,12 @@ Ce Contrat constitue l'intégralité de l'accord entre vous et nous concernant l
|
||||||
Si vous avez des questions concernant ce Contrat, veuillez nous contacter à :
|
Si vous avez des questions concernant ce Contrat, veuillez nous contacter à :
|
||||||
Email : contact@alertesecours.fr`;
|
Email : contact@alertesecours.fr`;
|
||||||
|
|
||||||
const EULA_STORAGE_KEY = "@eula_accepted";
|
|
||||||
|
|
||||||
const EULA = ({ onAccept, visible = true }) => {
|
const EULA = ({ onAccept, visible = true }) => {
|
||||||
if (!visible || Platform.OS !== "ios") return null;
|
if (!visible || Platform.OS !== "ios") return null;
|
||||||
|
|
||||||
const handleAccept = async () => {
|
const handleAccept = async () => {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(EULA_STORAGE_KEY, "true");
|
await AsyncStorage.setItem(STORAGE_KEYS.EULA_ACCEPTED, "true");
|
||||||
onAccept();
|
onAccept();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving EULA acceptance:", error);
|
console.error("Error saving EULA acceptance:", error);
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {
|
||||||
usePermissionWizardState,
|
usePermissionWizardState,
|
||||||
useNetworkState,
|
useNetworkState,
|
||||||
} from "~/stores";
|
} from "~/stores";
|
||||||
|
import { secureStore } from "~/storage/memorySecureStore";
|
||||||
|
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
|
||||||
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
||||||
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
|
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
|
||||||
|
@ -211,6 +213,23 @@ const AppLifecycleListener = () => {
|
||||||
);
|
);
|
||||||
checkPermissions(completed);
|
checkPermissions(completed);
|
||||||
|
|
||||||
|
// Sync memory stores back to persistent storage
|
||||||
|
lifecycleLogger.info("Syncing memory stores to persistent storage");
|
||||||
|
|
||||||
|
// Sync secure store
|
||||||
|
secureStore.syncToSecureStore().catch((error) => {
|
||||||
|
lifecycleLogger.error("Failed to sync memory secure store", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync async storage
|
||||||
|
memoryAsyncStorage.syncToAsyncStorage().catch((error) => {
|
||||||
|
lifecycleLogger.error("Failed to sync memory async storage", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Then handle WebSocket reconnection with proper error handling
|
// Then handle WebSocket reconnection with proper error handling
|
||||||
activeTimeout.current = setTimeout(() => {
|
activeTimeout.current = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -256,15 +256,7 @@ const HeroMode = () => {
|
||||||
"Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.",
|
"Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
// Battery optimization warning is now handled in the Android settings box
|
||||||
Platform.OS === "android" &&
|
|
||||||
batteryOptimizationEnabled &&
|
|
||||||
batteryOptAttempted
|
|
||||||
) {
|
|
||||||
warnings.push(
|
|
||||||
"L'optimisation de la batterie est encore activée. L'application pourrait ne pas fonctionner correctement en arrière-plan.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return warnings.length > 0 ? (
|
return warnings.length > 0 ? (
|
||||||
<View style={styles.warningsContainer}>
|
<View style={styles.warningsContainer}>
|
||||||
{warnings.map((warning, index) => (
|
{warnings.map((warning, index) => (
|
||||||
|
@ -280,12 +272,36 @@ const HeroMode = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAndroidPermissionWarning = () => {
|
const renderAndroidPermissionWarning = () => {
|
||||||
|
const hasBatteryOptimizationIssue =
|
||||||
|
batteryOptimizationEnabled && batteryOptAttempted;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.androidWarning}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.androidWarning,
|
||||||
|
hasBatteryOptimizationIssue && styles.androidWarningCritical,
|
||||||
|
]}
|
||||||
|
>
|
||||||
<View style={styles.androidWarningHeader}>
|
<View style={styles.androidWarningHeader}>
|
||||||
<Ionicons name="warning" size={24} color={theme.colors.warn} />
|
<Ionicons name="warning" size={24} color={theme.colors.warn} />
|
||||||
<Text style={styles.androidWarningTitle}>Paramètres Android</Text>
|
<Text style={styles.androidWarningTitle}>Paramètres Android</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{hasBatteryOptimizationIssue && (
|
||||||
|
<View style={styles.batteryOptimizationAlert}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.batteryOptimizationAlertText,
|
||||||
|
{ color: theme.colors.error },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Ionicons name="warning" size={16} /> L'optimisation de la
|
||||||
|
batterie est encore activée. L'application pourrait ne pas
|
||||||
|
fonctionner correctement en arrière-plan.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={styles.androidWarningDescription}>
|
<Text style={styles.androidWarningDescription}>
|
||||||
Sur Android, les permissions peuvent être automatiquement révoquées si
|
Sur Android, les permissions peuvent être automatiquement révoquées si
|
||||||
l'application n'est pas utilisée pendant une longue période.
|
l'application n'est pas utilisée pendant une longue période.
|
||||||
|
@ -306,9 +322,14 @@ const HeroMode = () => {
|
||||||
version d'Android)
|
version d'Android)
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{batteryOptimizationEnabled && batteryOptAttempted && (
|
{hasBatteryOptimizationIssue && (
|
||||||
<View style={styles.androidWarningSteps}>
|
<View style={styles.androidWarningSteps}>
|
||||||
<Text style={styles.androidWarningText}>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.androidWarningText,
|
||||||
|
styles.batteryOptimizationText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
Pour désactiver l'optimisation de la batterie :
|
Pour désactiver l'optimisation de la batterie :
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.androidWarningStep}>
|
<Text style={styles.androidWarningStep}>
|
||||||
|
@ -627,6 +648,10 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.warn,
|
borderColor: colors.warn,
|
||||||
},
|
},
|
||||||
|
androidWarningCritical: {
|
||||||
|
borderColor: colors.error,
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
androidWarningHeader: {
|
androidWarningHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
@ -664,6 +689,23 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
color: colors.primary,
|
color: colors.primary,
|
||||||
},
|
},
|
||||||
|
batteryOptimizationAlert: {
|
||||||
|
backgroundColor: colors.surfaceVariant,
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 15,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.error,
|
||||||
|
},
|
||||||
|
batteryOptimizationAlertText: {
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: 20,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
batteryOptimizationText: {
|
||||||
|
fontWeight: "600",
|
||||||
|
color: colors.error,
|
||||||
|
},
|
||||||
// iOS styles
|
// iOS styles
|
||||||
iosWarning: {
|
iosWarning: {
|
||||||
backgroundColor: colors.surfaceVariant,
|
backgroundColor: colors.surfaceVariant,
|
||||||
|
|
12
src/env.js
12
src/env.js
|
@ -1,8 +1,6 @@
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { secureStore } from "~/lib/secureStore";
|
import { secureStore } from "~/storage/memorySecureStore";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
// Key for storing staging setting in secureStore
|
|
||||||
const STAGING_SETTING_KEY = "env.isStaging";
|
|
||||||
|
|
||||||
// Logging configuration
|
// Logging configuration
|
||||||
const LOG_SCOPES = process.env.APP_LOG_SCOPES;
|
const LOG_SCOPES = process.env.APP_LOG_SCOPES;
|
||||||
|
@ -97,7 +95,7 @@ export const setStaging = async (enabled) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the staging setting
|
// Persist the staging setting
|
||||||
await secureStore.setItemAsync(STAGING_SETTING_KEY, String(enabled));
|
await secureStore.setItemAsync(STORAGE_KEYS.ENV_IS_STAGING, String(enabled));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize with default values
|
// Initialize with default values
|
||||||
|
@ -106,7 +104,9 @@ const env = { ...envMap };
|
||||||
// Load the staging setting from secureStore
|
// Load the staging setting from secureStore
|
||||||
export const initializeEnv = async () => {
|
export const initializeEnv = async () => {
|
||||||
try {
|
try {
|
||||||
const storedStaging = await secureStore.getItemAsync(STAGING_SETTING_KEY);
|
const storedStaging = await secureStore.getItemAsync(
|
||||||
|
STORAGE_KEYS.ENV_IS_STAGING,
|
||||||
|
);
|
||||||
if (storedStaging !== null) {
|
if (storedStaging !== null) {
|
||||||
const isStaging = storedStaging === "true";
|
const isStaging = storedStaging === "true";
|
||||||
if (isStaging) {
|
if (isStaging) {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
const EULA_STORAGE_KEY = "@eula_accepted";
|
|
||||||
|
|
||||||
export const useEULA = () => {
|
export const useEULA = () => {
|
||||||
const [eulaAccepted, setEulaAccepted] = useState(true);
|
const [eulaAccepted, setEulaAccepted] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -16,7 +15,7 @@ export const useEULA = () => {
|
||||||
|
|
||||||
const checkEULA = async () => {
|
const checkEULA = async () => {
|
||||||
try {
|
try {
|
||||||
const accepted = await AsyncStorage.getItem(EULA_STORAGE_KEY);
|
const accepted = await AsyncStorage.getItem(STORAGE_KEYS.EULA_ACCEPTED);
|
||||||
setEulaAccepted(!!accepted);
|
setEulaAccepted(!!accepted);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking EULA status:", error);
|
console.error("Error checking EULA status:", error);
|
||||||
|
|
93
src/location/backgroundTask.js
Normal file
93
src/location/backgroundTask.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
|
||||||
|
// Constants for persistence
|
||||||
|
const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
|
||||||
|
// const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
|
||||||
|
|
||||||
|
const geolocBgLogger = createLogger({
|
||||||
|
service: "background-task",
|
||||||
|
task: "headless",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions for persisting sync time
|
||||||
|
const getLastSyncTime = async () => {
|
||||||
|
try {
|
||||||
|
const value = await memoryAsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
||||||
|
);
|
||||||
|
return value ? parseInt(value, 10) : Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLastSyncTime = async (time) => {
|
||||||
|
try {
|
||||||
|
await memoryAsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
||||||
|
time.toString(),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// silent error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shared heartbeat logic - mutualized between Android and iOS
|
||||||
|
const executeSync = async () => {
|
||||||
|
let syncPerformed = false;
|
||||||
|
let syncSuccessful = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
syncPerformed = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Change pace to ensure location updates
|
||||||
|
await BackgroundGeolocation.changePace(true);
|
||||||
|
|
||||||
|
// Perform sync
|
||||||
|
await BackgroundGeolocation.sync();
|
||||||
|
|
||||||
|
syncSuccessful = true;
|
||||||
|
} catch (syncError) {
|
||||||
|
syncSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result information for BackgroundFetch
|
||||||
|
return {
|
||||||
|
syncPerformed,
|
||||||
|
syncSuccessful,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Return error result for BackgroundFetch
|
||||||
|
return {
|
||||||
|
syncPerformed,
|
||||||
|
syncSuccessful: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const executeHeartbeatSync = async () => {
|
||||||
|
const lastSyncTime = await getLastSyncTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastSync = now - lastSyncTime;
|
||||||
|
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
||||||
|
geolocBgLogger.info("Forcing location sync");
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
async () => {
|
||||||
|
await executeSync();
|
||||||
|
},
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("changePace timeout")), 10000),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await setLastSyncTime(now);
|
||||||
|
} catch (syncError) {
|
||||||
|
geolocBgLogger.error("Force sync failed", { error: syncError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,10 +1,9 @@
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
const EMULATOR_MODE_KEY = "emulator_mode_enabled";
|
|
||||||
|
|
||||||
// Global variables
|
// Global variables
|
||||||
let emulatorIntervalId = null;
|
let emulatorIntervalId = null;
|
||||||
let isEmulatorModeEnabled = false;
|
let isEmulatorModeEnabled = false;
|
||||||
|
@ -18,7 +17,9 @@ const emulatorLogger = createLogger({
|
||||||
// Initialize emulator mode based on stored preference
|
// Initialize emulator mode based on stored preference
|
||||||
export const initEmulatorMode = async () => {
|
export const initEmulatorMode = async () => {
|
||||||
try {
|
try {
|
||||||
const storedValue = await AsyncStorage.getItem(EMULATOR_MODE_KEY);
|
const storedValue = await AsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.EMULATOR_MODE_ENABLED,
|
||||||
|
);
|
||||||
emulatorLogger.debug("Initializing emulator mode", { storedValue });
|
emulatorLogger.debug("Initializing emulator mode", { storedValue });
|
||||||
|
|
||||||
if (storedValue === "true") {
|
if (storedValue === "true") {
|
||||||
|
@ -58,7 +59,7 @@ export const enableEmulatorMode = async () => {
|
||||||
isEmulatorModeEnabled = true;
|
isEmulatorModeEnabled = true;
|
||||||
|
|
||||||
// Persist the setting
|
// Persist the setting
|
||||||
await AsyncStorage.setItem(EMULATOR_MODE_KEY, "true");
|
await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "true");
|
||||||
emulatorLogger.debug("Emulator mode setting saved");
|
emulatorLogger.debug("Emulator mode setting saved");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
emulatorLogger.error("Failed to enable emulator mode", {
|
emulatorLogger.error("Failed to enable emulator mode", {
|
||||||
|
@ -81,7 +82,7 @@ export const disableEmulatorMode = async () => {
|
||||||
|
|
||||||
// Persist the setting
|
// Persist the setting
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(EMULATOR_MODE_KEY, "false");
|
await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "false");
|
||||||
emulatorLogger.debug("Emulator mode setting saved");
|
emulatorLogger.debug("Emulator mode setting saved");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
emulatorLogger.error("Failed to save emulator mode setting", {
|
emulatorLogger.error("Failed to save emulator mode setting", {
|
||||||
|
|
|
@ -4,17 +4,8 @@ import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import { initEmulatorMode } from "./emulatorService";
|
import { initEmulatorMode } from "./emulatorService";
|
||||||
import * as Sentry from "@sentry/react-native";
|
|
||||||
import { SPAN_STATUS_OK, SPAN_STATUS_ERROR } from "@sentry/react-native";
|
|
||||||
|
|
||||||
import throttle from "lodash.throttle";
|
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
|
||||||
|
|
||||||
import {
|
|
||||||
getAuthState,
|
|
||||||
subscribeAuthState,
|
|
||||||
authActions,
|
|
||||||
permissionsActions,
|
|
||||||
} from "~/stores";
|
|
||||||
|
|
||||||
import setLocationState from "~/location/setLocationState";
|
import setLocationState from "~/location/setLocationState";
|
||||||
import { storeLocation } from "~/utils/location/storage";
|
import { storeLocation } from "~/utils/location/storage";
|
||||||
|
@ -76,9 +67,6 @@ export default async function trackLocation() {
|
||||||
isStaging: env.IS_STAGING,
|
isStaging: env.IS_STAGING,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Throttling configuration for auth reload only
|
|
||||||
const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle
|
|
||||||
|
|
||||||
// Handle auth function - no throttling or cooldown
|
// Handle auth function - no throttling or cooldown
|
||||||
async function handleAuth(userToken) {
|
async function handleAuth(userToken) {
|
||||||
locationLogger.info("Handling auth token update", {
|
locationLogger.info("Handling auth token update", {
|
||||||
|
@ -108,25 +96,6 @@ export default async function trackLocation() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the current configuration
|
|
||||||
try {
|
|
||||||
const currentConfig = await BackgroundGeolocation.getConfig();
|
|
||||||
locationLogger.debug("Current background geolocation config", {
|
|
||||||
hasHeaders: !!currentConfig.headers,
|
|
||||||
headerKeys: currentConfig.headers
|
|
||||||
? Object.keys(currentConfig.headers)
|
|
||||||
: [],
|
|
||||||
authHeader: currentConfig.headers?.Authorization
|
|
||||||
? currentConfig.headers.Authorization.substring(0, 15) + "..."
|
|
||||||
: "Not set",
|
|
||||||
url: currentConfig.url,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
locationLogger.error("Failed to get background geolocation config", {
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = await BackgroundGeolocation.getState();
|
const state = await BackgroundGeolocation.getState();
|
||||||
try {
|
try {
|
||||||
const decodedToken = jwtDecode(userToken);
|
const decodedToken = jwtDecode(userToken);
|
||||||
|
@ -163,7 +132,7 @@ export default async function trackLocation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundGeolocation.onLocation((location) => {
|
BackgroundGeolocation.onLocation(async (location) => {
|
||||||
locationLogger.debug("Location update received", {
|
locationLogger.debug("Location update received", {
|
||||||
coords: location.coords,
|
coords: location.coords,
|
||||||
timestamp: location.timestamp,
|
timestamp: location.timestamp,
|
||||||
|
@ -171,19 +140,6 @@ export default async function trackLocation() {
|
||||||
battery: location.battery,
|
battery: location.battery,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add Sentry breadcrumb for location updates
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Location update in trackLocation",
|
|
||||||
category: "geolocation",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
coords: location.coords,
|
|
||||||
activity: location.activity?.type,
|
|
||||||
battery: location.battery?.level,
|
|
||||||
isMoving: location.isMoving,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
location.coords &&
|
location.coords &&
|
||||||
location.coords.latitude &&
|
location.coords.latitude &&
|
||||||
|
@ -195,100 +151,12 @@ export default async function trackLocation() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The core auth reload function that will be throttled
|
BackgroundGeolocation.onHttp(async (response) => {
|
||||||
function _reloadAuth() {
|
// log status code and response
|
||||||
locationLogger.info("Refreshing authentication token");
|
|
||||||
authActions.reload(); // should retriger sync in handleAuth via subscribeAuthState when done
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create throttled version of auth reload with lodash
|
|
||||||
const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, {
|
|
||||||
leading: true,
|
|
||||||
trailing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
BackgroundGeolocation.onHttp((response) => {
|
|
||||||
// Log the full response including headers if available
|
|
||||||
locationLogger.debug("HTTP response received", {
|
locationLogger.debug("HTTP response received", {
|
||||||
status: response?.status,
|
status: response?.status,
|
||||||
success: response?.success,
|
|
||||||
responseText: response?.responseText,
|
|
||||||
url: response?.url,
|
|
||||||
method: response?.method,
|
|
||||||
isSync: response?.isSync,
|
|
||||||
requestHeaders:
|
|
||||||
response?.request?.headers || "Headers not available in response",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Sentry breadcrumb for HTTP responses
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Background geolocation HTTP response",
|
|
||||||
category: "geolocation-http",
|
|
||||||
level: response?.status === 200 ? "info" : "warning",
|
|
||||||
data: {
|
|
||||||
status: response?.status,
|
|
||||||
success: response?.success,
|
|
||||||
url: response?.url,
|
|
||||||
isSync: response?.isSync,
|
|
||||||
recordCount: response?.count,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the current auth token for comparison
|
|
||||||
const { userToken } = getAuthState();
|
|
||||||
locationLogger.debug("Current auth state token", {
|
|
||||||
tokenAvailable: !!userToken,
|
|
||||||
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusCode = response?.status;
|
|
||||||
|
|
||||||
switch (statusCode) {
|
|
||||||
case 410:
|
|
||||||
// Token expired, logout
|
|
||||||
locationLogger.info("Auth token expired (410), logging out");
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auth token expired - logging out",
|
|
||||||
category: "geolocation-auth",
|
|
||||||
level: "warning",
|
|
||||||
});
|
|
||||||
authActions.logout();
|
|
||||||
break;
|
|
||||||
case 401:
|
|
||||||
// Unauthorized, use throttled reload
|
|
||||||
locationLogger.info("Unauthorized (401), attempting to refresh token");
|
|
||||||
|
|
||||||
// Add more detailed logging of the error response
|
|
||||||
try {
|
|
||||||
const errorBody = response?.responseText
|
|
||||||
? JSON.parse(response.responseText)
|
|
||||||
: null;
|
|
||||||
locationLogger.debug("Unauthorized error details", {
|
|
||||||
errorBody,
|
|
||||||
errorType: errorBody?.error?.type,
|
|
||||||
errorMessage: errorBody?.error?.message,
|
|
||||||
errorPath: errorBody?.error?.errors?.[0]?.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Unauthorized - refreshing token",
|
|
||||||
category: "geolocation-auth",
|
|
||||||
level: "warning",
|
|
||||||
data: {
|
|
||||||
errorType: errorBody?.error?.type,
|
|
||||||
errorMessage: errorBody?.error?.message,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
locationLogger.debug("Failed to parse error response", {
|
|
||||||
error: e.message,
|
|
||||||
responseText: response?.responseText,
|
responseText: response?.responseText,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
reloadAuth();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -18,8 +18,8 @@ export default function DrawerItemList(props) {
|
||||||
const { routes } = state;
|
const { routes } = state;
|
||||||
|
|
||||||
const section1 = routes.slice(0, 5);
|
const section1 = routes.slice(0, 5);
|
||||||
const section2 = routes.slice(5, 8);
|
const section2 = routes.slice(5, 9);
|
||||||
const section3 = routes.slice(8, routes.length);
|
const section3 = routes.slice(9, routes.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -9,15 +9,15 @@ export default function getStatusCode({ networkError, graphQLErrors }) {
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
let code;
|
let code;
|
||||||
for (const err of graphQLErrors) {
|
for (const err of graphQLErrors) {
|
||||||
if (err.extensions.http) {
|
if (err.extensions?.http) {
|
||||||
code = err.extensions.http;
|
code = err.extensions.http;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (err.extensions.statusCode) {
|
if (err.extensions?.statusCode) {
|
||||||
code = err.extensions.statusCode;
|
code = err.extensions.statusCode;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (err.extensions.code) {
|
if (err.extensions?.code) {
|
||||||
code = err.extensions.code;
|
code = err.extensions.code;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { navActions } from "~/stores";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const backgroundGeolocationLogger = createLogger({
|
||||||
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
feature: "action-open-background-geolocation-settings",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function actionOpenBackgroundGeolocationSettings({ data }) {
|
||||||
|
backgroundGeolocationLogger.debug(
|
||||||
|
"actionOpenBackgroundGeolocationSettings called",
|
||||||
|
);
|
||||||
|
|
||||||
|
navActions.setNextNavigation([
|
||||||
|
{
|
||||||
|
name: "Params",
|
||||||
|
params: {
|
||||||
|
anchor: "permissions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
26
src/notifications/actions/actionOpenSettings.js
Normal file
26
src/notifications/actions/actionOpenSettings.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { navActions } from "~/stores";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const settingsLogger = createLogger({
|
||||||
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
feature: "action-open-settings",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function actionOpenSettings({ data }) {
|
||||||
|
settingsLogger.debug("actionOpenSettings called", {
|
||||||
|
data,
|
||||||
|
hasData: !!data,
|
||||||
|
dataKeys: data ? Object.keys(data) : [],
|
||||||
|
});
|
||||||
|
|
||||||
|
navActions.setNextNavigation([
|
||||||
|
{
|
||||||
|
name: "Params",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
settingsLogger.debug("Navigation set to Params screen", {
|
||||||
|
navigationTarget: "Params",
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,364 +0,0 @@
|
||||||
import notifee from "@notifee/react-native";
|
|
||||||
import BackgroundFetch from "react-native-background-fetch";
|
|
||||||
import * as Sentry from "@sentry/react-native";
|
|
||||||
|
|
||||||
import useMount from "~/hooks/useMount";
|
|
||||||
import { createLogger } from "~/lib/logger";
|
|
||||||
|
|
||||||
const logger = createLogger({
|
|
||||||
service: "notifications",
|
|
||||||
task: "auto-cancel-expired",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Background task to cancel expired notifications
|
|
||||||
const backgroundTask = async () => {
|
|
||||||
await Sentry.startSpan(
|
|
||||||
{
|
|
||||||
name: "auto-cancel-expired-notifications",
|
|
||||||
op: "background-task",
|
|
||||||
},
|
|
||||||
async (span) => {
|
|
||||||
try {
|
|
||||||
logger.info("Starting auto-cancel expired notifications task");
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auto-cancel task started",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get displayed notifications with timeout protection
|
|
||||||
let notifications;
|
|
||||||
await Sentry.startSpan(
|
|
||||||
{
|
|
||||||
op: "get-displayed-notifications",
|
|
||||||
description: "Getting displayed notifications",
|
|
||||||
},
|
|
||||||
async (getNotificationsSpan) => {
|
|
||||||
try {
|
|
||||||
// Add timeout protection for the API call
|
|
||||||
notifications = await Promise.race([
|
|
||||||
notifee.getDisplayedNotifications(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout getting notifications")),
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
getNotificationsSpan.setStatus("ok");
|
|
||||||
} catch (error) {
|
|
||||||
getNotificationsSpan.setStatus("internal_error");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Array.isArray(notifications)) {
|
|
||||||
logger.warn("No notifications array received", { notifications });
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "No notifications array received",
|
|
||||||
category: "notifications",
|
|
||||||
level: "warning",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = Math.round(new Date() / 1000);
|
|
||||||
let cancelledCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
logger.info("Processing notifications", {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
currentTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Processing notifications",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
currentTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process notifications with individual error handling
|
|
||||||
for (const notification of notifications) {
|
|
||||||
try {
|
|
||||||
if (!notification || !notification.id) {
|
|
||||||
logger.warn("Invalid notification object", { notification });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expires = notification.data?.expires;
|
|
||||||
if (!expires) {
|
|
||||||
continue; // Skip notifications without expiry
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expires !== "number" || expires < currentTime) {
|
|
||||||
logger.debug("Cancelling expired notification", {
|
|
||||||
notificationId: notification.id,
|
|
||||||
expires,
|
|
||||||
currentTime,
|
|
||||||
expired: expires < currentTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel notification with timeout protection
|
|
||||||
await Promise.race([
|
|
||||||
notifee.cancelNotification(notification.id),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout cancelling notification")),
|
|
||||||
5000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
cancelledCount++;
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Notification cancelled",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
notificationId: notification.id,
|
|
||||||
expires,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (notificationError) {
|
|
||||||
errorCount++;
|
|
||||||
logger.error("Failed to process notification", {
|
|
||||||
error: notificationError,
|
|
||||||
notificationId: notification?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(notificationError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "cancel-notification",
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
notification: {
|
|
||||||
id: notification?.id,
|
|
||||||
expires: notification?.data?.expires,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Auto-cancel task completed", {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
cancelledCount,
|
|
||||||
errorCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auto-cancel task completed",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
cancelledCount,
|
|
||||||
errorCount,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
span.setStatus("ok");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Auto-cancel task failed", { error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-task",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
span.setStatus("internal_error");
|
|
||||||
throw error; // Re-throw to be handled by caller
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAutoCancelExpired = () => {
|
|
||||||
useMount(() => {
|
|
||||||
// Initialize background fetch
|
|
||||||
BackgroundFetch.configure(
|
|
||||||
{
|
|
||||||
minimumFetchInterval: 180, // Fetch interval in minutes
|
|
||||||
stopOnTerminate: false,
|
|
||||||
startOnBoot: true,
|
|
||||||
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_NONE,
|
|
||||||
enableHeadless: true,
|
|
||||||
},
|
|
||||||
async (taskId) => {
|
|
||||||
logger.info("BackgroundFetch task started", { taskId });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await backgroundTask();
|
|
||||||
logger.info("BackgroundFetch task completed successfully", {
|
|
||||||
taskId,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("BackgroundFetch task failed", { taskId, error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-fetch-task",
|
|
||||||
taskId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// CRITICAL: Always call finish, even on error
|
|
||||||
try {
|
|
||||||
if (taskId) {
|
|
||||||
BackgroundFetch.finish(taskId);
|
|
||||||
logger.debug("BackgroundFetch task finished", { taskId });
|
|
||||||
} else {
|
|
||||||
logger.error("Cannot finish BackgroundFetch task - no taskId");
|
|
||||||
}
|
|
||||||
} catch (finishError) {
|
|
||||||
// This is a critical error - the native side might be in a bad state
|
|
||||||
logger.error("CRITICAL: BackgroundFetch.finish() failed", {
|
|
||||||
taskId,
|
|
||||||
error: finishError,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(finishError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-fetch-finish",
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
task: { taskId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
logger.error("BackgroundFetch failed to start", { error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-fetch-configure",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
BackgroundFetch.stop();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register headless task
|
|
||||||
BackgroundFetch.registerHeadlessTask(async (event) => {
|
|
||||||
const taskId = event?.taskId;
|
|
||||||
|
|
||||||
logger.info("Headless task started", { taskId, event });
|
|
||||||
|
|
||||||
// Add timeout protection for the entire headless task
|
|
||||||
const taskTimeout = setTimeout(() => {
|
|
||||||
logger.error("Headless task timeout", { taskId });
|
|
||||||
|
|
||||||
Sentry.captureException(new Error("Headless task timeout"), {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task-timeout",
|
|
||||||
taskId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force finish the task to prevent native side hanging
|
|
||||||
try {
|
|
||||||
if (taskId) {
|
|
||||||
BackgroundFetch.finish(taskId);
|
|
||||||
logger.debug("Headless task force-finished due to timeout", { taskId });
|
|
||||||
}
|
|
||||||
} catch (finishError) {
|
|
||||||
logger.error("CRITICAL: Failed to force-finish timed out headless task", {
|
|
||||||
taskId,
|
|
||||||
error: finishError,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(finishError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task-timeout-finish",
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
task: { taskId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 30000); // 30 second timeout
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!taskId) {
|
|
||||||
throw new Error("No taskId provided in headless task event");
|
|
||||||
}
|
|
||||||
|
|
||||||
await backgroundTask();
|
|
||||||
logger.info("Headless task completed successfully", { taskId });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Headless task failed", { taskId, error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task",
|
|
||||||
taskId,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
event: {
|
|
||||||
taskId,
|
|
||||||
eventData: JSON.stringify(event),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Clear the timeout
|
|
||||||
clearTimeout(taskTimeout);
|
|
||||||
|
|
||||||
// CRITICAL: Always call finish, even on error
|
|
||||||
try {
|
|
||||||
if (taskId) {
|
|
||||||
BackgroundFetch.finish(taskId);
|
|
||||||
logger.debug("Headless task finished", { taskId });
|
|
||||||
} else {
|
|
||||||
logger.error("Cannot finish headless task - no taskId", { event });
|
|
||||||
}
|
|
||||||
} catch (finishError) {
|
|
||||||
// This is a critical error - the native side might be in a bad state
|
|
||||||
logger.error(
|
|
||||||
"CRITICAL: BackgroundFetch.finish() failed in headless task",
|
|
||||||
{
|
|
||||||
taskId,
|
|
||||||
error: finishError,
|
|
||||||
event,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Sentry.captureException(finishError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task-finish",
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
task: { taskId },
|
|
||||||
event: { eventData: JSON.stringify(event) },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
67
src/notifications/channels/notifBackgroundGeolocationLost.js
Normal file
67
src/notifications/channels/notifBackgroundGeolocationLost.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
import { Light } from "~/theme/app";
|
||||||
|
import { displayNotification } from "../helpers";
|
||||||
|
import { generateBackgroundGeolocationLostContent } from "../content";
|
||||||
|
|
||||||
|
const { colors } = Light;
|
||||||
|
|
||||||
|
const backgroundGeolocationLogger = createLogger({
|
||||||
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
feature: "background-geolocation-channel",
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelId = "system";
|
||||||
|
|
||||||
|
export default async function notifBackgroundGeolocationLost(data) {
|
||||||
|
backgroundGeolocationLogger.debug(
|
||||||
|
"Displaying background geolocation lost notification",
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// DEBUG: Log notification configuration for diagnosis
|
||||||
|
backgroundGeolocationLogger.info(
|
||||||
|
"DEBUG: Background geolocation notification config",
|
||||||
|
{
|
||||||
|
channelId,
|
||||||
|
pressActionId: "open-settings",
|
||||||
|
launchActivity: "default",
|
||||||
|
hasData: !!data,
|
||||||
|
dataKeys: data ? Object.keys(data) : [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate notification content
|
||||||
|
const { title, body, bigText } =
|
||||||
|
generateBackgroundGeolocationLostContent(data);
|
||||||
|
|
||||||
|
await displayNotification({
|
||||||
|
channelId,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data,
|
||||||
|
color: colors.warning || colors.primary,
|
||||||
|
bigText,
|
||||||
|
android: {
|
||||||
|
pressAction: {
|
||||||
|
id: "open-background-geolocation-settings",
|
||||||
|
launchActivity: "default",
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
title: "Paramètres",
|
||||||
|
pressAction: {
|
||||||
|
id: "open-background-geolocation-settings",
|
||||||
|
launchActivity: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundGeolocationLogger.info(
|
||||||
|
"Background geolocation lost notification displayed successfully",
|
||||||
|
);
|
||||||
|
}
|
38
src/notifications/channels/notifGeolocationHeartbeatSync.js
Normal file
38
src/notifications/channels/notifGeolocationHeartbeatSync.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||||
|
|
||||||
|
const heartbeatLogger = createLogger({
|
||||||
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
feature: "geolocation-heartbeat-sync",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function notifGeolocationHeartbeatSync(data) {
|
||||||
|
try {
|
||||||
|
heartbeatLogger.info(
|
||||||
|
"Received iOS geolocation heartbeat sync notification",
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is a silent notification - no visible notification is displayed
|
||||||
|
// Instead, we trigger the geolocation heartbeat sync directly
|
||||||
|
|
||||||
|
heartbeatLogger.info("Triggering geolocation heartbeat sync");
|
||||||
|
|
||||||
|
// Execute the heartbeat sync to force location update
|
||||||
|
await executeHeartbeatSync();
|
||||||
|
|
||||||
|
heartbeatLogger.info("Geolocation heartbeat sync completed successfully");
|
||||||
|
} catch (error) {
|
||||||
|
heartbeatLogger.error("Failed to execute geolocation heartbeat sync", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't throw the error - this is a background operation
|
||||||
|
// and we don't want to crash the notification handler
|
||||||
|
}
|
||||||
|
}
|
10
src/notifications/channels/notifSystem.js
Normal file
10
src/notifications/channels/notifSystem.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { createChannel } from "../helpers";
|
||||||
|
|
||||||
|
const channelId = "system";
|
||||||
|
|
||||||
|
export async function createNotificationChannel() {
|
||||||
|
await createChannel({
|
||||||
|
id: channelId,
|
||||||
|
name: "Paramètres",
|
||||||
|
});
|
||||||
|
}
|
|
@ -88,9 +88,9 @@ export const generateSuggestKeepOpenContent = (data) => {
|
||||||
|
|
||||||
export const generateBackgroundGeolocationLostContent = (data) => {
|
export const generateBackgroundGeolocationLostContent = (data) => {
|
||||||
return {
|
return {
|
||||||
title: `Localisation en arrière-plan désactivée`,
|
title: `Alerte-Secours ne peut plus accéder à votre position`,
|
||||||
body: `Votre localisation en arrière-plan a été désactivée. Veuillez vérifier les paramètres de l'application.`,
|
body: `Vous ne pouvez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
||||||
bigText: `Votre localisation en arrière-plan a été désactivée. Pour continuer à utiliser pleinement l'application, veuillez vérifier les paramètres de votre appareil.`,
|
bigText: `Alerte-Secours ne peut plus accéder à votre position en arrière-plan. Vous ne pouvez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ import notifSuggestClose from "./channels/notifSuggestClose";
|
||||||
import notifSuggestKeepOpen from "./channels/notifSuggestKeepOpen";
|
import notifSuggestKeepOpen from "./channels/notifSuggestKeepOpen";
|
||||||
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
||||||
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
||||||
|
import notifBackgroundGeolocationLost from "./channels/notifBackgroundGeolocationLost";
|
||||||
|
import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync";
|
||||||
|
|
||||||
const displayLogger = createLogger({
|
const displayLogger = createLogger({
|
||||||
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
@ -20,6 +22,8 @@ const SUPPORTED_ACTIONS = {
|
||||||
"suggest-keep-open": notifSuggestKeepOpen,
|
"suggest-keep-open": notifSuggestKeepOpen,
|
||||||
"relative-allow-ask": notifRelativeAllowAsk,
|
"relative-allow-ask": notifRelativeAllowAsk,
|
||||||
"relative-invitation": notifRelativeInvitation,
|
"relative-invitation": notifRelativeInvitation,
|
||||||
|
"background-geolocation-lost": notifBackgroundGeolocationLost,
|
||||||
|
"geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function displayNotificationHandler(data) {
|
export default async function displayNotificationHandler(data) {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
import useMount from "~/hooks/useMount";
|
import useMount from "~/hooks/useMount";
|
||||||
import setActionCategories from "./setActionCategories";
|
import setActionCategories from "./setActionCategories";
|
||||||
import onMessageReceived from "./onMessageReceived";
|
import onMessageReceived from "./onMessageReceived";
|
||||||
import { useAutoCancelExpired } from "./autoCancelExpired";
|
|
||||||
import { requestFcmPermission, setupFcm } from "./firebase";
|
import { requestFcmPermission, setupFcm } from "./firebase";
|
||||||
import {
|
import {
|
||||||
requestNotifeePermission,
|
requestNotifeePermission,
|
||||||
|
@ -204,6 +203,4 @@ export function useFcm() {
|
||||||
notifLogger.debug("Badge count reset");
|
notifLogger.debug("Badge count reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useAutoCancelExpired();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { VirtualNotificationTypes } from "./virtualNotifications";
|
import { VirtualNotificationTypes } from "./virtualNotifications";
|
||||||
import { getNotificationContent } from "./content";
|
import { getNotificationContent } from "./content";
|
||||||
import openSettings from "~/lib/native/openSettings";
|
import openSettings from "~/lib/native/openSettings";
|
||||||
|
import { navActions } from "~/stores";
|
||||||
|
|
||||||
export const getNotificationColor = (notification, theme) => {
|
export const getNotificationColor = (notification, theme) => {
|
||||||
const { colors } = theme;
|
const { colors } = theme;
|
||||||
|
@ -83,7 +84,16 @@ export const createNotificationHandlers = (handlers) => {
|
||||||
suggest_keep_open: async (data) => await openAlert({ data }),
|
suggest_keep_open: async (data) => await openAlert({ data }),
|
||||||
relative_invitation: async (data) => await openRelatives({ data }),
|
relative_invitation: async (data) => await openRelatives({ data }),
|
||||||
relative_allow_ask: async (data) => await openRelatives({ data }),
|
relative_allow_ask: async (data) => await openRelatives({ data }),
|
||||||
background_geolocation_lost: async (data) => openSettings(),
|
background_geolocation_lost: async (_data) => {
|
||||||
|
navActions.setNextNavigation([
|
||||||
|
{
|
||||||
|
name: "Params",
|
||||||
|
params: {
|
||||||
|
anchor: "permissions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -14,6 +14,8 @@ import actionRelativeAllowAccept from "./actions/actionRelativeAllowAccept";
|
||||||
import actionRelativeAllowReject from "./actions/actionRelativeAllowReject";
|
import actionRelativeAllowReject from "./actions/actionRelativeAllowReject";
|
||||||
import actionRelativeInvitationAccept from "./actions/actionRelativeInvitationAccept";
|
import actionRelativeInvitationAccept from "./actions/actionRelativeInvitationAccept";
|
||||||
import actionRelativeInvitationReject from "./actions/actionRelativeInvitationReject";
|
import actionRelativeInvitationReject from "./actions/actionRelativeInvitationReject";
|
||||||
|
import actionOpenSettings from "./actions/actionOpenSettings";
|
||||||
|
import actionOpenBackgroundGeolocationSettings from "./actions/actionOpenBackgroundGeolocationSettings";
|
||||||
|
|
||||||
import { navActions } from "~/stores";
|
import { navActions } from "~/stores";
|
||||||
|
|
||||||
|
@ -96,11 +98,12 @@ export const onNotificationOpenedAppEvent = async (remoteMessage) => {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
try {
|
try {
|
||||||
eventLogger.info("Processing background notification tap", {
|
eventLogger.debug("Processing background notification tap", {
|
||||||
messageId: remoteMessage?.messageId,
|
messageId: remoteMessage?.messageId,
|
||||||
data: remoteMessage?.data,
|
data: remoteMessage?.data,
|
||||||
notification: remoteMessage?.notification,
|
notification: remoteMessage?.notification,
|
||||||
clickAction: remoteMessage?.notification?.android?.clickAction,
|
clickAction: remoteMessage?.notification?.android?.clickAction,
|
||||||
|
notificationType: remoteMessage?.data?.type || "unknown",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!remoteMessage?.notification) {
|
if (!remoteMessage?.notification) {
|
||||||
|
@ -273,5 +276,28 @@ export const onEvent = async ({ type, notification, pressAction }) => {
|
||||||
await actionRelativeInvitationReject({ data });
|
await actionRelativeInvitationReject({ data });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "open-settings": {
|
||||||
|
eventLogger.debug("Processing open-settings action", {
|
||||||
|
data,
|
||||||
|
actionId,
|
||||||
|
notificationId: notification?.id,
|
||||||
|
launchActivity: pressAction?.launchActivity,
|
||||||
|
});
|
||||||
|
await actionOpenSettings({ data });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "open-background-geolocation-settings": {
|
||||||
|
eventLogger.debug(
|
||||||
|
"Processing open-background-geolocation-settings action",
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
actionId,
|
||||||
|
notificationId: notification?.id,
|
||||||
|
launchActivity: pressAction?.launchActivity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await actionOpenBackgroundGeolocationSettings({ data });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { createNotificationChannel as createSuggestCloseChannel } from "./channe
|
||||||
import { createNotificationChannel as createSuggestKeepOpenChannel } from "./channels/notifSuggestKeepOpen";
|
import { createNotificationChannel as createSuggestKeepOpenChannel } from "./channels/notifSuggestKeepOpen";
|
||||||
import { createNotificationChannel as createRelativeAllowAskChannel } from "./channels/notifRelativeAllowAsk";
|
import { createNotificationChannel as createRelativeAllowAskChannel } from "./channels/notifRelativeAllowAsk";
|
||||||
import { createNotificationChannel as createRelativeInvitationChannel } from "./channels/notifRelativeInvitation";
|
import { createNotificationChannel as createRelativeInvitationChannel } from "./channels/notifRelativeInvitation";
|
||||||
|
import { createNotificationChannel as createSystemChannel } from "./channels/notifSystem";
|
||||||
|
|
||||||
export default async function setActionCategories() {
|
export default async function setActionCategories() {
|
||||||
// Create all notification channels
|
// Create all notification channels
|
||||||
|
@ -17,6 +18,7 @@ export default async function setActionCategories() {
|
||||||
createSuggestKeepOpenChannel(),
|
createSuggestKeepOpenChannel(),
|
||||||
createRelativeAllowAskChannel(),
|
createRelativeAllowAskChannel(),
|
||||||
createRelativeInvitationChannel(),
|
createRelativeInvitationChannel(),
|
||||||
|
createSystemChannel(),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorData = {
|
const errorData = {
|
||||||
|
@ -113,5 +115,20 @@ export default async function setActionCategories() {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "system",
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: "open-settings",
|
||||||
|
title: "Paramètres",
|
||||||
|
foreground: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "open-background-geolocation-settings",
|
||||||
|
title: "Paramètres",
|
||||||
|
foreground: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
78
src/scenes/Params/SentryOptOut.js
Normal file
78
src/scenes/Params/SentryOptOut.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { Title, Switch } from "react-native-paper";
|
||||||
|
import { createStyles } from "~/theme";
|
||||||
|
import { useParamsState, paramsActions } from "~/stores";
|
||||||
|
import Text from "~/components/Text";
|
||||||
|
import { setSentryEnabled } from "~/sentry";
|
||||||
|
|
||||||
|
function SentryOptOut() {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { sentryEnabled } = useParamsState(["sentryEnabled"]);
|
||||||
|
|
||||||
|
const handleToggle = useCallback(async () => {
|
||||||
|
const newValue = !sentryEnabled;
|
||||||
|
await paramsActions.setSentryEnabled(newValue);
|
||||||
|
|
||||||
|
// Dynamically enable/disable Sentry
|
||||||
|
setSentryEnabled(newValue);
|
||||||
|
}, [sentryEnabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Title style={styles.title}>Rapport d'erreurs</Title>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<View style={styles.switchContainer}>
|
||||||
|
<Text style={styles.label}>Envoyer les rapports d'erreurs</Text>
|
||||||
|
<Switch
|
||||||
|
value={sentryEnabled}
|
||||||
|
onValueChange={handleToggle}
|
||||||
|
style={styles.switch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Les rapports d'erreurs nous aident à améliorer l'application en nous
|
||||||
|
permettant de mieux identifier et corriger les problèmes techniques.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ theme: { colors } }) => ({
|
||||||
|
container: {
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginVertical: 15,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
switchContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 5,
|
||||||
|
paddingVertical: 5,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 16,
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 15,
|
||||||
|
},
|
||||||
|
switch: {
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
textAlign: "left",
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default SentryOptOut;
|
|
@ -1,17 +1,40 @@
|
||||||
import React from "react";
|
import React, { useCallback, useRef } from "react";
|
||||||
import { View, ScrollView } from "react-native";
|
import { View, ScrollView, InteractionManager } from "react-native";
|
||||||
import { createStyles } from "~/theme";
|
import { createStyles } from "~/theme";
|
||||||
import ParamsNotifications from "./Notifications";
|
import ParamsNotifications from "./Notifications";
|
||||||
import ParamsRadius from "./Radius";
|
import ParamsRadius from "./Radius";
|
||||||
import ParamsEmergencyCall from "./EmergencyCall";
|
import ParamsEmergencyCall from "./EmergencyCall";
|
||||||
import ThemeSwitcher from "./ThemeSwitcher";
|
import ThemeSwitcher from "./ThemeSwitcher";
|
||||||
import Permissions from "./Permissions";
|
import Permissions from "./Permissions";
|
||||||
|
import SentryOptOut from "./SentryOptOut";
|
||||||
|
import { useRoute, useFocusEffect } from "@react-navigation/native";
|
||||||
|
|
||||||
export default function ParamsView({ data }) {
|
export default function ParamsView({ data }) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
const { params } = useRoute();
|
||||||
|
const didScroll = useRef(false);
|
||||||
|
|
||||||
|
const recordLayout = useCallback(
|
||||||
|
(key) =>
|
||||||
|
({
|
||||||
|
nativeEvent: {
|
||||||
|
layout: { y },
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
if (didScroll.current || params?.anchor !== key) return;
|
||||||
|
|
||||||
|
InteractionManager.runAfterInteractions(() => {
|
||||||
|
scrollRef.current?.scrollTo({ y, animated: true });
|
||||||
|
didScroll.current = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[params?.anchor],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={styles.scrollView}>
|
<ScrollView ref={scrollRef} style={styles.scrollView}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
|
@ -26,6 +49,9 @@ export default function ParamsView({ data }) {
|
||||||
<ParamsRadius data={data} />
|
<ParamsRadius data={data} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
|
<SentryOptOut />
|
||||||
|
</View>
|
||||||
|
<View onLayout={recordLayout("permissions")} style={styles.section}>
|
||||||
<Permissions />
|
<Permissions />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -114,11 +114,14 @@ export default function Form({
|
||||||
username: data.username,
|
username: data.username,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
methods.reset(data);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
checkEmailIsRegistered,
|
checkEmailIsRegistered,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
email,
|
email,
|
||||||
|
methods,
|
||||||
saveProfileMutation,
|
saveProfileMutation,
|
||||||
setError,
|
setError,
|
||||||
userId,
|
userId,
|
||||||
|
|
|
@ -3,6 +3,15 @@ import { Platform } from "react-native";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
import packageJson from "../../package.json";
|
import packageJson from "../../package.json";
|
||||||
|
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const sentryLogger = createLogger({
|
||||||
|
module: SYSTEM_SCOPES.APP,
|
||||||
|
feature: "sentry",
|
||||||
|
});
|
||||||
|
|
||||||
// Get the build number from native code
|
// Get the build number from native code
|
||||||
const getBuildNumber = () => {
|
const getBuildNumber = () => {
|
||||||
|
@ -23,9 +32,55 @@ const getReleaseVersion = () => {
|
||||||
return `com.alertesecours@${version}+${buildNumber}`;
|
return `com.alertesecours@${version}+${buildNumber}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
Sentry.init({
|
// Check if Sentry is enabled by user preference
|
||||||
|
const checkSentryEnabled = async () => {
|
||||||
|
try {
|
||||||
|
// Wait for memory storage to be initialized
|
||||||
|
let retries = 0;
|
||||||
|
const maxRetries = 10;
|
||||||
|
|
||||||
|
while (retries < maxRetries) {
|
||||||
|
try {
|
||||||
|
const stored = await memoryAsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.SENTRY_ENABLED,
|
||||||
|
);
|
||||||
|
if (stored !== null) {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
}
|
||||||
|
break; // Storage is ready, no preference stored
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error.message?.includes("not initialized") &&
|
||||||
|
retries < maxRetries - 1
|
||||||
|
) {
|
||||||
|
// Wait a bit and retry if storage not initialized
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
retries++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sentryLogger.warn("Failed to check Sentry preference", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
sentryLogger.warn("Failed to check Sentry preference", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Default to enabled if no preference stored or error occurred
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Sentry with user preference check
|
||||||
|
const initializeSentry = async () => {
|
||||||
|
const isEnabled = await checkSentryEnabled();
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
dsn: env.SENTRY_DSN,
|
dsn: env.SENTRY_DSN,
|
||||||
tracesSampleRate: 1.0,
|
enabled: isEnabled,
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
debug: __DEV__,
|
debug: __DEV__,
|
||||||
// Configure release to match ios-archive.sh format
|
// Configure release to match ios-archive.sh format
|
||||||
release: getReleaseVersion(),
|
release: getReleaseVersion(),
|
||||||
|
@ -79,4 +134,37 @@ Sentry.init({
|
||||||
// maskAllVectors: false,
|
// maskAllVectors: false,
|
||||||
// }),
|
// }),
|
||||||
],
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Sentry asynchronously
|
||||||
|
initializeSentry().catch((error) => {
|
||||||
|
sentryLogger.warn("Failed to initialize Sentry", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Export function to dynamically control Sentry
|
||||||
|
export const setSentryEnabled = (enabled) => {
|
||||||
|
try {
|
||||||
|
// Use the newer Sentry API
|
||||||
|
const client = Sentry.getClient();
|
||||||
|
if (client) {
|
||||||
|
const options = client.getOptions();
|
||||||
|
options.enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
// Clear any pending events when disabling
|
||||||
|
Sentry.withScope((scope) => {
|
||||||
|
scope.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sentryLogger.info("Sentry state toggled", { enabled });
|
||||||
|
} else {
|
||||||
|
sentryLogger.warn("Sentry client not available for toggling");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
sentryLogger.warn("Failed to toggle Sentry state", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
283
src/storage/memoryAsyncStorage.js
Normal file
283
src/storage/memoryAsyncStorage.js
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
import { getAsyncStorageKeys } from "./storageKeys";
|
||||||
|
|
||||||
|
const storageLogger = createLogger({
|
||||||
|
module: SYSTEM_SCOPES.STORAGE,
|
||||||
|
feature: "memory-async-storage",
|
||||||
|
});
|
||||||
|
|
||||||
|
// In-memory cache for AsyncStorage values
|
||||||
|
const memoryCache = new Map();
|
||||||
|
|
||||||
|
// Track if we've loaded from AsyncStorage
|
||||||
|
let isInitialized = false;
|
||||||
|
const initPromise = new Promise((resolve) => {
|
||||||
|
global.__memoryAsyncStorageInitResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory-first AsyncStorage wrapper that maintains an in-memory cache
|
||||||
|
* for headless/background mode access when AsyncStorage is unavailable
|
||||||
|
*/
|
||||||
|
export const memoryAsyncStorage = {
|
||||||
|
/**
|
||||||
|
* Initialize the memory cache by loading all known keys from AsyncStorage
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
storageLogger.info("Initializing memory async storage");
|
||||||
|
|
||||||
|
// Get all registered AsyncStorage keys from the registry
|
||||||
|
const knownKeys = getAsyncStorageKeys();
|
||||||
|
|
||||||
|
// Load all known keys into memory
|
||||||
|
for (const key of knownKeys) {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(key);
|
||||||
|
if (value !== null) {
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Loaded key into memory", {
|
||||||
|
key,
|
||||||
|
hasValue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to load key from AsyncStorage", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also load any keys that might exist with getAllKeys
|
||||||
|
try {
|
||||||
|
const allKeys = await AsyncStorage.getAllKeys();
|
||||||
|
for (const key of allKeys) {
|
||||||
|
if (!memoryCache.has(key)) {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(key);
|
||||||
|
if (value !== null) {
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Loaded additional key into memory", { key });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to load additional key", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to get all keys from AsyncStorage", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
if (global.__memoryAsyncStorageInitResolve) {
|
||||||
|
global.__memoryAsyncStorageInitResolve();
|
||||||
|
delete global.__memoryAsyncStorageInitResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
storageLogger.info("Memory async storage initialized", {
|
||||||
|
cachedKeys: Array.from(memoryCache.keys()),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure initialization is complete before operations
|
||||||
|
*/
|
||||||
|
async ensureInitialized() {
|
||||||
|
if (!isInitialized) {
|
||||||
|
await initPromise;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get item from memory first, fallback to AsyncStorage
|
||||||
|
*/
|
||||||
|
async getItem(key) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Try memory first
|
||||||
|
if (memoryCache.has(key)) {
|
||||||
|
const value = memoryCache.get(key);
|
||||||
|
storageLogger.debug("Retrieved from memory cache", {
|
||||||
|
key,
|
||||||
|
hasValue: !!value,
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to AsyncStorage
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(key);
|
||||||
|
if (value !== null) {
|
||||||
|
// Cache for future use
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Retrieved from AsyncStorage and cached", { key });
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn(
|
||||||
|
"Failed to retrieve from AsyncStorage, returning null",
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// In headless mode, AsyncStorage might not be accessible
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set item in both memory and AsyncStorage
|
||||||
|
*/
|
||||||
|
async setItem(key, value) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Always set in memory first
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Set in memory cache", { key });
|
||||||
|
|
||||||
|
// Try to persist to AsyncStorage
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(key, value);
|
||||||
|
storageLogger.debug("Persisted to AsyncStorage", { key });
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn(
|
||||||
|
"Failed to persist to AsyncStorage, kept in memory only",
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove item from both memory and AsyncStorage
|
||||||
|
*/
|
||||||
|
async removeItem(key) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Delete from memory
|
||||||
|
memoryCache.delete(key);
|
||||||
|
storageLogger.debug("Deleted from memory cache", { key });
|
||||||
|
|
||||||
|
// Try to delete from AsyncStorage
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.removeItem(key);
|
||||||
|
storageLogger.debug("Deleted from AsyncStorage", { key });
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to delete from AsyncStorage", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
// Continue - at least removed from memory
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all keys from memory cache
|
||||||
|
*/
|
||||||
|
async getAllKeys() {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
return Array.from(memoryCache.keys());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple items
|
||||||
|
*/
|
||||||
|
async multiGet(keys) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
const result = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = await this.getItem(key);
|
||||||
|
result.push([key, value]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple items
|
||||||
|
*/
|
||||||
|
async multiSet(keyValuePairs) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
for (const [key, value] of keyValuePairs) {
|
||||||
|
await this.setItem(key, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove multiple items
|
||||||
|
*/
|
||||||
|
async multiRemove(keys) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
for (const key of keys) {
|
||||||
|
await this.removeItem(key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all items (use with caution)
|
||||||
|
*/
|
||||||
|
async clear() {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Clear memory
|
||||||
|
memoryCache.clear();
|
||||||
|
storageLogger.info("Cleared memory cache");
|
||||||
|
|
||||||
|
// Try to clear AsyncStorage
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.clear();
|
||||||
|
storageLogger.info("Cleared AsyncStorage");
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to clear AsyncStorage", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync memory cache back to AsyncStorage (useful when returning from background)
|
||||||
|
*/
|
||||||
|
async syncToAsyncStorage() {
|
||||||
|
storageLogger.info("Syncing memory cache to AsyncStorage");
|
||||||
|
|
||||||
|
const syncResults = {
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of memoryCache.entries()) {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(key, value);
|
||||||
|
syncResults.success++;
|
||||||
|
} catch (error) {
|
||||||
|
syncResults.failed++;
|
||||||
|
storageLogger.warn("Failed to sync key to AsyncStorage", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storageLogger.info("Memory cache sync completed", syncResults);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export as default to match the AsyncStorage interface
|
||||||
|
export default memoryAsyncStorage;
|
191
src/storage/memorySecureStore.js
Normal file
191
src/storage/memorySecureStore.js
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import { secureStore as originalSecureStore } from "./secureStore";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
import { getSecureStoreKeys } from "./storageKeys";
|
||||||
|
|
||||||
|
const storageLogger = createLogger({
|
||||||
|
module: SYSTEM_SCOPES.STORAGE,
|
||||||
|
feature: "memory-secure-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
// In-memory cache for secure store values
|
||||||
|
const memoryCache = new Map();
|
||||||
|
|
||||||
|
// Track if we've loaded from secure store
|
||||||
|
let isInitialized = false;
|
||||||
|
const initPromise = new Promise((resolve) => {
|
||||||
|
global.__memorySecureStoreInitResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory-first secure store wrapper that maintains an in-memory cache
|
||||||
|
* for headless/background mode access when secure store is unavailable
|
||||||
|
*/
|
||||||
|
export const memorySecureStore = {
|
||||||
|
/**
|
||||||
|
* Initialize the memory cache by loading all known keys from secure store
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
storageLogger.info("Initializing memory secure store");
|
||||||
|
|
||||||
|
// Get all registered secure store keys from the registry
|
||||||
|
const knownKeys = getSecureStoreKeys();
|
||||||
|
|
||||||
|
// Load all known keys into memory
|
||||||
|
for (const key of knownKeys) {
|
||||||
|
try {
|
||||||
|
const value = await originalSecureStore.getItemAsync(key);
|
||||||
|
if (value !== null) {
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Loaded key into memory", {
|
||||||
|
key,
|
||||||
|
hasValue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to load key from secure store", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
if (global.__memorySecureStoreInitResolve) {
|
||||||
|
global.__memorySecureStoreInitResolve();
|
||||||
|
delete global.__memorySecureStoreInitResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
storageLogger.info("Memory secure store initialized", {
|
||||||
|
cachedKeys: Array.from(memoryCache.keys()),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure initialization is complete before operations
|
||||||
|
*/
|
||||||
|
async ensureInitialized() {
|
||||||
|
if (!isInitialized) {
|
||||||
|
await initPromise;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get item from memory first, fallback to secure store
|
||||||
|
*/
|
||||||
|
async getItemAsync(key) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Try memory first
|
||||||
|
if (memoryCache.has(key)) {
|
||||||
|
const value = memoryCache.get(key);
|
||||||
|
storageLogger.debug("Retrieved from memory cache", {
|
||||||
|
key,
|
||||||
|
hasValue: !!value,
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to secure store
|
||||||
|
try {
|
||||||
|
const value = await originalSecureStore.getItemAsync(key);
|
||||||
|
if (value !== null) {
|
||||||
|
// Cache for future use
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Retrieved from secure store and cached", { key });
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn(
|
||||||
|
"Failed to retrieve from secure store, returning null",
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// In headless mode, secure store might not be accessible
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set item in both memory and secure store
|
||||||
|
*/
|
||||||
|
async setItemAsync(key, value) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Always set in memory first
|
||||||
|
memoryCache.set(key, value);
|
||||||
|
storageLogger.debug("Set in memory cache", { key });
|
||||||
|
|
||||||
|
// Try to persist to secure store
|
||||||
|
try {
|
||||||
|
await originalSecureStore.setItemAsync(key, value);
|
||||||
|
storageLogger.debug("Persisted to secure store", { key });
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn(
|
||||||
|
"Failed to persist to secure store, kept in memory only",
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Continue - value is at least in memory
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete item from both memory and secure store
|
||||||
|
*/
|
||||||
|
async deleteItemAsync(key) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
// Delete from memory
|
||||||
|
memoryCache.delete(key);
|
||||||
|
storageLogger.debug("Deleted from memory cache", { key });
|
||||||
|
|
||||||
|
// Try to delete from secure store
|
||||||
|
try {
|
||||||
|
await originalSecureStore.deleteItemAsync(key);
|
||||||
|
storageLogger.debug("Deleted from secure store", { key });
|
||||||
|
} catch (error) {
|
||||||
|
storageLogger.warn("Failed to delete from secure store", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
// Continue - at least removed from memory
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync memory cache back to secure store (useful when returning from background)
|
||||||
|
*/
|
||||||
|
async syncToSecureStore() {
|
||||||
|
storageLogger.info("Syncing memory cache to secure store");
|
||||||
|
|
||||||
|
const syncResults = {
|
||||||
|
success: 0,
|
||||||
|
failed: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of memoryCache.entries()) {
|
||||||
|
try {
|
||||||
|
await originalSecureStore.setItemAsync(key, value);
|
||||||
|
syncResults.success++;
|
||||||
|
} catch (error) {
|
||||||
|
syncResults.failed++;
|
||||||
|
storageLogger.warn("Failed to sync key to secure store", {
|
||||||
|
key,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storageLogger.info("Memory cache sync completed", syncResults);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export as default to match the original secureStore interface
|
||||||
|
export const secureStore = memorySecureStore;
|
84
src/storage/storageKeys.js
Normal file
84
src/storage/storageKeys.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Storage Keys Registry
|
||||||
|
*
|
||||||
|
* This file maintains a registry of all storage keys used throughout the application.
|
||||||
|
* By defining keys as constants here, they are automatically included in memory storage
|
||||||
|
* initialization, eliminating the need for manual maintenance of key lists.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const secureStoreKeys = new Set();
|
||||||
|
const asyncStorageKeys = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a secure store key and return it as a constant
|
||||||
|
* @param {string} key - The storage key to register for secure store
|
||||||
|
* @returns {string} The same key, now registered for secure store
|
||||||
|
*/
|
||||||
|
export const registerSecureStoreKey = (key) => {
|
||||||
|
secureStoreKeys.add(key);
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an AsyncStorage key and return it as a constant
|
||||||
|
* @param {string} key - The storage key to register for AsyncStorage
|
||||||
|
* @returns {string} The same key, now registered for AsyncStorage
|
||||||
|
*/
|
||||||
|
export const registerAsyncStorageKey = (key) => {
|
||||||
|
asyncStorageKeys.add(key);
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all secure store keys
|
||||||
|
* @returns {string[]} Array of secure store keys
|
||||||
|
*/
|
||||||
|
export const getSecureStoreKeys = () => Array.from(secureStoreKeys);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all AsyncStorage keys
|
||||||
|
* @returns {string[]} Array of AsyncStorage keys
|
||||||
|
*/
|
||||||
|
export const getAsyncStorageKeys = () => Array.from(asyncStorageKeys);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered storage keys (both types)
|
||||||
|
* @returns {string[]} Array of all registered keys
|
||||||
|
*/
|
||||||
|
export const getAllRegisteredKeys = () => [
|
||||||
|
...Array.from(secureStoreKeys),
|
||||||
|
...Array.from(asyncStorageKeys),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage key constants
|
||||||
|
* All storage keys used throughout the application should be defined here.
|
||||||
|
*/
|
||||||
|
export const STORAGE_KEYS = {
|
||||||
|
// Secure Store Keys - Authentication & Security
|
||||||
|
DEVICE_UUID: registerSecureStoreKey("deviceUuid"),
|
||||||
|
AUTH_TOKEN: registerSecureStoreKey("authToken"),
|
||||||
|
USER_TOKEN: registerSecureStoreKey("userToken"),
|
||||||
|
DEV_AUTH_TOKEN: registerSecureStoreKey("dev.authToken"),
|
||||||
|
DEV_USER_TOKEN: registerSecureStoreKey("dev.userToken"),
|
||||||
|
ANON_AUTH_TOKEN: registerSecureStoreKey("anon.authToken"),
|
||||||
|
ANON_USER_TOKEN: registerSecureStoreKey("anon.userToken"),
|
||||||
|
FCM_TOKEN_STORED: registerSecureStoreKey("fcmTokenStored"),
|
||||||
|
FCM_TOKEN_STORED_DEVICE_ID: registerSecureStoreKey("fcmTokenStoredDeviceId"),
|
||||||
|
ENV_IS_STAGING: registerSecureStoreKey("env.isStaging"),
|
||||||
|
|
||||||
|
// AsyncStorage Keys - App State & Preferences
|
||||||
|
GEOLOCATION_LAST_SYNC_TIME: registerAsyncStorageKey(
|
||||||
|
"@geolocation_last_sync_time",
|
||||||
|
),
|
||||||
|
EULA_ACCEPTED: registerAsyncStorageKey("@eula_accepted"),
|
||||||
|
OVERRIDE_MESSAGES: registerAsyncStorageKey("@override_messages"),
|
||||||
|
PERMISSION_WIZARD_COMPLETED: registerAsyncStorageKey(
|
||||||
|
"@permission_wizard_completed",
|
||||||
|
),
|
||||||
|
LAST_UPDATE_CHECK_TIME: registerAsyncStorageKey("lastUpdateCheckTime"),
|
||||||
|
LAST_KNOWN_LOCATION: registerAsyncStorageKey("@last_known_location"),
|
||||||
|
EULA_ACCEPTED_SIMPLE: registerAsyncStorageKey("eula_accepted"),
|
||||||
|
EMULATOR_MODE_ENABLED: registerAsyncStorageKey("emulator_mode_enabled"),
|
||||||
|
SENTRY_ENABLED: registerAsyncStorageKey("@sentry_enabled"),
|
||||||
|
};
|
|
@ -1,8 +1,7 @@
|
||||||
import { createAtom } from "~/lib/atomic-zustand";
|
import { createAtom } from "~/lib/atomic-zustand";
|
||||||
import debounce from "lodash.debounce";
|
import debounce from "lodash.debounce";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
const OVERRIDE_MESSAGES_STORAGE_KEY = "@override_messages";
|
|
||||||
|
|
||||||
export default createAtom(({ merge, set, get, reset }) => {
|
export default createAtom(({ merge, set, get, reset }) => {
|
||||||
const overrideMessagesCache = {};
|
const overrideMessagesCache = {};
|
||||||
|
@ -10,7 +9,7 @@ export default createAtom(({ merge, set, get, reset }) => {
|
||||||
const initCache = async () => {
|
const initCache = async () => {
|
||||||
try {
|
try {
|
||||||
const storedData = await AsyncStorage.getItem(
|
const storedData = await AsyncStorage.getItem(
|
||||||
OVERRIDE_MESSAGES_STORAGE_KEY,
|
STORAGE_KEYS.OVERRIDE_MESSAGES,
|
||||||
);
|
);
|
||||||
const storedMessages = storedData ? JSON.parse(storedData) : {};
|
const storedMessages = storedData ? JSON.parse(storedData) : {};
|
||||||
Object.entries(storedMessages).forEach(([messageId, data]) => {
|
Object.entries(storedMessages).forEach(([messageId, data]) => {
|
||||||
|
@ -24,7 +23,7 @@ export default createAtom(({ merge, set, get, reset }) => {
|
||||||
const saveOverrideMessagesToStorage = async () => {
|
const saveOverrideMessagesToStorage = async () => {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(
|
await AsyncStorage.setItem(
|
||||||
OVERRIDE_MESSAGES_STORAGE_KEY,
|
STORAGE_KEYS.OVERRIDE_MESSAGES,
|
||||||
JSON.stringify(overrideMessagesCache),
|
JSON.stringify(overrideMessagesCache),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { secureStore } from "~/lib/secureStore";
|
import { secureStore } from "~/storage/memorySecureStore";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
|
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
@ -10,13 +11,13 @@ import isExpired from "~/lib/time/isExpired";
|
||||||
import { registerUser, loginUserToken } from "~/auth/actions";
|
import { registerUser, loginUserToken } from "~/auth/actions";
|
||||||
|
|
||||||
// DEV
|
// DEV
|
||||||
// SecureStore.deleteItemAsync("userToken");
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN);
|
||||||
// SecureStore.deleteItemAsync("authToken");
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN);
|
||||||
// SecureStore.deleteItemAsync("dev.userToken");
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN);
|
||||||
// SecureStore.deleteItemAsync("dev.authToken");
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN);
|
||||||
// SecureStore.deleteItemAsync("anon.userToken");
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN);
|
||||||
// SecureStore.deleteItemAsync("anon.authToken");
|
// SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN);
|
||||||
// SecureStore.getItemAsync("userToken").then((t) => authLogger.debug("User token", { token: t }));
|
// SecureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN).then((t) => authLogger.debug("User token", { token: t }));
|
||||||
|
|
||||||
const authLogger = createLogger({
|
const authLogger = createLogger({
|
||||||
module: FEATURE_SCOPES.AUTH,
|
module: FEATURE_SCOPES.AUTH,
|
||||||
|
@ -68,7 +69,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
authLogger.info("Attempting to login with auth token");
|
authLogger.info("Attempting to login with auth token");
|
||||||
const { userToken } = await loginUserToken({ authToken });
|
const { userToken } = await loginUserToken({ authToken });
|
||||||
authLogger.info("Successfully obtained user token");
|
authLogger.info("Successfully obtained user token");
|
||||||
await secureStore.setItemAsync("userToken", userToken);
|
await secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, userToken);
|
||||||
endLoading({
|
endLoading({
|
||||||
userToken,
|
userToken,
|
||||||
});
|
});
|
||||||
|
@ -81,8 +82,8 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
"Auth token expired, clearing tokens and reinitializing",
|
"Auth token expired, clearing tokens and reinitializing",
|
||||||
);
|
);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.deleteItemAsync("authToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
||||||
secureStore.deleteItemAsync("userToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
return init();
|
return init();
|
||||||
}
|
}
|
||||||
|
@ -93,8 +94,8 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
authLogger.debug("Initializing auth state");
|
authLogger.debug("Initializing auth state");
|
||||||
let { userToken, authToken } = await promiseObject({
|
let { userToken, authToken } = await promiseObject({
|
||||||
userToken: secureStore.getItemAsync("userToken"),
|
userToken: secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
|
||||||
authToken: secureStore.getItemAsync("authToken"),
|
authToken: secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
||||||
});
|
});
|
||||||
// await delay(5);
|
// await delay(5);
|
||||||
// authLogger.debug("Auth tokens", { userToken, authToken });
|
// authLogger.debug("Auth tokens", { userToken, authToken });
|
||||||
|
@ -121,7 +122,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
const res = await registerUser();
|
const res = await registerUser();
|
||||||
authLogger.info("Successfully registered new user");
|
authLogger.info("Successfully registered new user");
|
||||||
authToken = res.authToken;
|
authToken = res.authToken;
|
||||||
await secureStore.setItemAsync("authToken", authToken);
|
await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userToken && authToken) {
|
if (!userToken && authToken) {
|
||||||
|
@ -153,6 +154,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading()) {
|
if (isLoading()) {
|
||||||
|
authLogger.info("Auth is already loading, waiting for completion");
|
||||||
await loadingPromise;
|
await loadingPromise;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -162,9 +164,15 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startLoading();
|
startLoading();
|
||||||
await secureStore.deleteItemAsync("userToken");
|
|
||||||
|
authLogger.debug("Deleting userToken for refresh");
|
||||||
|
await secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN);
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
authLogger.error("Auth reload failed", { error: error.message });
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Clear reloading state even if there was an error
|
// Clear reloading state even if there was an error
|
||||||
merge({ isReloading: false });
|
merge({ isReloading: false });
|
||||||
|
@ -176,7 +184,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
const { onReloadAuthToken: authToken } = get();
|
const { onReloadAuthToken: authToken } = get();
|
||||||
|
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
await secureStore.setItemAsync("authToken", authToken);
|
await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken);
|
||||||
await loadUserJWT(authToken);
|
await loadUserJWT(authToken);
|
||||||
} else {
|
} else {
|
||||||
await init();
|
await init();
|
||||||
|
@ -197,12 +205,12 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
// backup anon tokens
|
// backup anon tokens
|
||||||
const [anonAuthToken, anonUserToken] = await Promise.all([
|
const [anonAuthToken, anonUserToken] = await Promise.all([
|
||||||
secureStore.getItemAsync("authToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
||||||
secureStore.getItemAsync("userToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.setItemAsync("anon.authToken", anonAuthToken),
|
secureStore.setItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN, anonAuthToken),
|
||||||
secureStore.setItemAsync("anon.userToken", anonUserToken),
|
secureStore.setItemAsync(STORAGE_KEYS.ANON_USER_TOKEN, anonUserToken),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
merge({ onReloadAuthToken: authTokenJwt });
|
merge({ onReloadAuthToken: authTokenJwt });
|
||||||
|
@ -212,12 +220,12 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
const impersonate = async ({ authTokenJwt }) => {
|
const impersonate = async ({ authTokenJwt }) => {
|
||||||
authLogger.info("Starting impersonation");
|
authLogger.info("Starting impersonation");
|
||||||
const [anonAuthToken, anonUserToken] = await Promise.all([
|
const [anonAuthToken, anonUserToken] = await Promise.all([
|
||||||
secureStore.getItemAsync("authToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
||||||
secureStore.getItemAsync("userToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.setItemAsync("dev.authToken", anonAuthToken),
|
secureStore.setItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN, anonAuthToken),
|
||||||
secureStore.setItemAsync("dev.userToken", anonUserToken),
|
secureStore.setItemAsync(STORAGE_KEYS.DEV_USER_TOKEN, anonUserToken),
|
||||||
]);
|
]);
|
||||||
merge({ onReloadAuthToken: authTokenJwt });
|
merge({ onReloadAuthToken: authTokenJwt });
|
||||||
triggerReload();
|
triggerReload();
|
||||||
|
@ -227,29 +235,29 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
authLogger.info("Initiating logout");
|
authLogger.info("Initiating logout");
|
||||||
const [devAuthToken, devUserToken, anonAuthToken, anonUserToken] =
|
const [devAuthToken, devUserToken, anonAuthToken, anonUserToken] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.getItemAsync("dev.authToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN),
|
||||||
secureStore.getItemAsync("dev.userToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.DEV_USER_TOKEN),
|
||||||
secureStore.getItemAsync("anon.authToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN),
|
||||||
secureStore.getItemAsync("anon.userToken"),
|
secureStore.getItemAsync(STORAGE_KEYS.ANON_USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
if (devAuthToken && devUserToken) {
|
if (devAuthToken && devUserToken) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.setItemAsync("authToken", devAuthToken),
|
secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, devAuthToken),
|
||||||
secureStore.setItemAsync("userToken", devUserToken),
|
secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, devUserToken),
|
||||||
secureStore.deleteItemAsync("dev.authToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN),
|
||||||
secureStore.deleteItemAsync("dev.userToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
} else if (anonAuthToken && anonUserToken) {
|
} else if (anonAuthToken && anonUserToken) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.setItemAsync("authToken", anonAuthToken),
|
secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, anonAuthToken),
|
||||||
secureStore.setItemAsync("userToken", anonUserToken),
|
secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, anonUserToken),
|
||||||
secureStore.deleteItemAsync("anon.authToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN),
|
||||||
secureStore.deleteItemAsync("anon.userToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
secureStore.deleteItemAsync("authToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN),
|
||||||
secureStore.deleteItemAsync("userToken"),
|
secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN),
|
||||||
]);
|
]);
|
||||||
merge({
|
merge({
|
||||||
userOffMode: true,
|
userOffMode: true,
|
||||||
|
@ -268,6 +276,31 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
triggerReload();
|
triggerReload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setUserToken = async (userToken) => {
|
||||||
|
authLogger.info("Setting user token", {
|
||||||
|
hasToken: !!userToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update secure storage
|
||||||
|
await secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, userToken);
|
||||||
|
|
||||||
|
// Update in-memory state
|
||||||
|
merge({ userToken });
|
||||||
|
|
||||||
|
// Update session from JWT
|
||||||
|
if (userToken) {
|
||||||
|
const jwtData = jwtDecode(userToken);
|
||||||
|
sessionActions.loadSessionFromJWT(jwtData);
|
||||||
|
}
|
||||||
|
|
||||||
|
authLogger.debug("User token updated successfully");
|
||||||
|
} catch (error) {
|
||||||
|
authLogger.error("Failed to set user token", { error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
userToken: null,
|
userToken: null,
|
||||||
|
@ -287,6 +320,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
logout,
|
logout,
|
||||||
onReload,
|
onReload,
|
||||||
userOnMode,
|
userOnMode,
|
||||||
|
setUserToken,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createAtom } from "~/lib/atomic-zustand";
|
import { createAtom } from "~/lib/atomic-zustand";
|
||||||
import { secureStore } from "~/lib/secureStore";
|
import { secureStore } from "~/storage/memorySecureStore";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
|
||||||
export default createAtom(({ merge, reset }) => {
|
export default createAtom(({ merge, reset }) => {
|
||||||
const setFcmToken = (token) => {
|
const setFcmToken = (token) => {
|
||||||
|
@ -9,8 +10,11 @@ export default createAtom(({ merge, reset }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setFcmTokenStored = ({ fcmToken, deviceId }) => {
|
const setFcmTokenStored = ({ fcmToken, deviceId }) => {
|
||||||
secureStore.setItemAsync("fcmTokenStored", fcmToken);
|
secureStore.setItemAsync(STORAGE_KEYS.FCM_TOKEN_STORED, fcmToken);
|
||||||
secureStore.setItemAsync("fcmTokenStoredDeviceId", deviceId.toString());
|
secureStore.setItemAsync(
|
||||||
|
STORAGE_KEYS.FCM_TOKEN_STORED_DEVICE_ID,
|
||||||
|
deviceId.toString(),
|
||||||
|
);
|
||||||
merge({
|
merge({
|
||||||
fcmTokenStored: fcmToken,
|
fcmTokenStored: fcmToken,
|
||||||
deviceId,
|
deviceId,
|
||||||
|
@ -18,9 +22,11 @@ export default createAtom(({ merge, reset }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const fcmTokenStored = await secureStore.getItemAsync("fcmTokenStored");
|
const fcmTokenStored = await secureStore.getItemAsync(
|
||||||
|
STORAGE_KEYS.FCM_TOKEN_STORED,
|
||||||
|
);
|
||||||
const fcmTokenStoredDeviceId = await secureStore.getItemAsync(
|
const fcmTokenStoredDeviceId = await secureStore.getItemAsync(
|
||||||
"fcmTokenStoredDeviceId",
|
STORAGE_KEYS.FCM_TOKEN_STORED_DEVICE_ID,
|
||||||
);
|
);
|
||||||
const deviceId = fcmTokenStoredDeviceId
|
const deviceId = fcmTokenStoredDeviceId
|
||||||
? parseInt(fcmTokenStoredDeviceId, 10)
|
? parseInt(fcmTokenStoredDeviceId, 10)
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default createAtom(({ get, merge, reset }) => {
|
||||||
...m,
|
...m,
|
||||||
routeName,
|
routeName,
|
||||||
});
|
});
|
||||||
navLogger.info("Route updated", { routeName });
|
navLogger.debug("Route updated", { routeName });
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
@ -78,11 +78,11 @@ export default createAtom(({ get, merge, reset }) => {
|
||||||
default: initialValues,
|
default: initialValues,
|
||||||
actions: {
|
actions: {
|
||||||
reset: () => {
|
reset: () => {
|
||||||
navLogger.info("Resetting navigation state to initial values");
|
navLogger.debug("Resetting navigation state to initial values");
|
||||||
reset();
|
reset();
|
||||||
},
|
},
|
||||||
updateRouteFromRootStack: (state) => {
|
updateRouteFromRootStack: (state) => {
|
||||||
navLogger.info("Updating route from root stack", { state });
|
navLogger.debug("Updating route from root stack", { state });
|
||||||
const { index, routeNames } = state;
|
const { index, routeNames } = state;
|
||||||
const rootRouteName = routeNames[index];
|
const rootRouteName = routeNames[index];
|
||||||
updateRoute({
|
updateRoute({
|
||||||
|
@ -90,7 +90,7 @@ export default createAtom(({ get, merge, reset }) => {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateRouteFromDrawer: (state) => {
|
updateRouteFromDrawer: (state) => {
|
||||||
navLogger.info("Updating route from drawer", { state });
|
navLogger.debug("Updating route from drawer", { state });
|
||||||
const { index, routeNames } = state;
|
const { index, routeNames } = state;
|
||||||
const drawerRouteName = routeNames[index];
|
const drawerRouteName = routeNames[index];
|
||||||
updateRoute({
|
updateRoute({
|
||||||
|
@ -98,7 +98,7 @@ export default createAtom(({ get, merge, reset }) => {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateRouteFromMain: (state) => {
|
updateRouteFromMain: (state) => {
|
||||||
navLogger.info("Updating route from main", { state });
|
navLogger.debug("Updating route from main", { state });
|
||||||
const { index, routeNames } = state;
|
const { index, routeNames } = state;
|
||||||
const mainRouteName = routeNames[index];
|
const mainRouteName = routeNames[index];
|
||||||
updateRoute({
|
updateRoute({
|
||||||
|
@ -106,13 +106,13 @@ export default createAtom(({ get, merge, reset }) => {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setNextNavigation: (nextNavigation) => {
|
setNextNavigation: (nextNavigation) => {
|
||||||
navLogger.info("Setting next navigation", { nextNavigation });
|
navLogger.debug("Setting next navigation", { nextNavigation });
|
||||||
merge({
|
merge({
|
||||||
nextNavigation,
|
nextNavigation,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setMessageViewFocus: (isFocused, alertId = null) => {
|
setMessageViewFocus: (isFocused, alertId = null) => {
|
||||||
navLogger.info("Setting message view focus", { isFocused, alertId });
|
navLogger.debug("Setting message view focus", { isFocused, alertId });
|
||||||
merge({
|
merge({
|
||||||
isOnMessageView: isFocused,
|
isOnMessageView: isFocused,
|
||||||
currentMessageAlertId: isFocused ? alertId : null,
|
currentMessageAlertId: isFocused ? alertId : null,
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
import { createAtom } from "~/lib/atomic-zustand";
|
import { createAtom } from "~/lib/atomic-zustand";
|
||||||
|
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const paramsLogger = createLogger({
|
||||||
|
module: SYSTEM_SCOPES.APP,
|
||||||
|
feature: "params",
|
||||||
|
});
|
||||||
|
|
||||||
export default createAtom(({ merge, reset }) => {
|
export default createAtom(({ merge, reset }) => {
|
||||||
const setDevModeEnabled = (b) => {
|
const setDevModeEnabled = (b) => {
|
||||||
|
@ -37,6 +46,45 @@ export default createAtom(({ merge, reset }) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSentryEnabled = async (sentryEnabled) => {
|
||||||
|
merge({
|
||||||
|
sentryEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Persist to storage
|
||||||
|
try {
|
||||||
|
await memoryAsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.SENTRY_ENABLED,
|
||||||
|
JSON.stringify(sentryEnabled),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
paramsLogger.warn("Failed to persist Sentry preference", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSentryEnabled = async () => {
|
||||||
|
try {
|
||||||
|
const stored = await memoryAsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.SENTRY_ENABLED,
|
||||||
|
);
|
||||||
|
if (stored !== null) {
|
||||||
|
const sentryEnabled = JSON.parse(stored);
|
||||||
|
merge({ sentryEnabled });
|
||||||
|
return sentryEnabled;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
paramsLogger.warn("Failed to load Sentry preference", {
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
await initSentryEnabled();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
// devModeEnabled: false,
|
// devModeEnabled: false,
|
||||||
|
@ -46,6 +94,7 @@ export default createAtom(({ merge, reset }) => {
|
||||||
mapColorScheme: "auto",
|
mapColorScheme: "auto",
|
||||||
hasRegisteredRelatives: null,
|
hasRegisteredRelatives: null,
|
||||||
alertListSortBy: "location",
|
alertListSortBy: "location",
|
||||||
|
sentryEnabled: true,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
reset,
|
reset,
|
||||||
|
@ -55,6 +104,8 @@ export default createAtom(({ merge, reset }) => {
|
||||||
setMapColorScheme,
|
setMapColorScheme,
|
||||||
setHasRegisteredRelatives,
|
setHasRegisteredRelatives,
|
||||||
setAlertListSortBy,
|
setAlertListSortBy,
|
||||||
|
setSentryEnabled,
|
||||||
|
init,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { createAtom } from "~/lib/atomic-zustand";
|
import { createAtom } from "~/lib/atomic-zustand";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
const WIZARD_COMPLETED_KEY = "@permission_wizard_completed";
|
|
||||||
|
|
||||||
export default createAtom(({ set, get }) => {
|
export default createAtom(({ set, get }) => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
const wizardCompleted = await AsyncStorage.getItem(WIZARD_COMPLETED_KEY);
|
const wizardCompleted = await AsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.PERMISSION_WIZARD_COMPLETED,
|
||||||
|
);
|
||||||
if (wizardCompleted === "true") {
|
if (wizardCompleted === "true") {
|
||||||
set("completed", true);
|
set("completed", true);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +28,10 @@ export default createAtom(({ set, get }) => {
|
||||||
setCompleted: (completed) => {
|
setCompleted: (completed) => {
|
||||||
set("completed", completed);
|
set("completed", completed);
|
||||||
if (completed) {
|
if (completed) {
|
||||||
AsyncStorage.setItem(WIZARD_COMPLETED_KEY, "true").catch((error) => {
|
AsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.PERMISSION_WIZARD_COMPLETED,
|
||||||
|
"true",
|
||||||
|
).catch((error) => {
|
||||||
console.error("Error saving permission wizard status:", error);
|
console.error("Error saving permission wizard status:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ const ThemeLight = {
|
||||||
|
|
||||||
error: "#fa5252",
|
error: "#fa5252",
|
||||||
onError: "#ffffff",
|
onError: "#ffffff",
|
||||||
errorContainer: "##fa5252",
|
errorContainer: "#fa5252",
|
||||||
onErrorContainer: "#ffffff",
|
onErrorContainer: "#ffffff",
|
||||||
|
|
||||||
warn: "#f59f00",
|
warn: "#f59f00",
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import * as Updates from "expo-updates";
|
import * as Updates from "expo-updates";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import useNow from "~/hooks/useNow";
|
import useNow from "~/hooks/useNow";
|
||||||
import * as Sentry from "@sentry/react-native";
|
import * as Sentry from "@sentry/react-native";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
import { treeActions } from "~/stores";
|
import { treeActions } from "~/stores";
|
||||||
|
|
||||||
const LAST_UPDATE_CHECK_KEY = "lastUpdateCheckTime";
|
|
||||||
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const applyUpdate = async () => {
|
const applyUpdate = async () => {
|
||||||
|
@ -28,12 +27,17 @@ const checkForUpdate = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const lastCheckString = await AsyncStorage.getItem(LAST_UPDATE_CHECK_KEY);
|
const lastCheckString = await AsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.LAST_UPDATE_CHECK_TIME,
|
||||||
|
);
|
||||||
const lastCheck = lastCheckString ? new Date(lastCheckString) : null;
|
const lastCheck = lastCheckString ? new Date(lastCheckString) : null;
|
||||||
const nowDate = new Date();
|
const nowDate = new Date();
|
||||||
|
|
||||||
if (!lastCheck || nowDate - lastCheck > UPDATE_CHECK_INTERVAL) {
|
if (!lastCheck || nowDate - lastCheck > UPDATE_CHECK_INTERVAL) {
|
||||||
await AsyncStorage.setItem(LAST_UPDATE_CHECK_KEY, nowDate.toISOString());
|
await AsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.LAST_UPDATE_CHECK_TIME,
|
||||||
|
nowDate.toISOString(),
|
||||||
|
);
|
||||||
|
|
||||||
const update = await Updates.checkForUpdateAsync();
|
const update = await Updates.checkForUpdateAsync();
|
||||||
if (!update.isAvailable) {
|
if (!update.isAvailable) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
@ -7,8 +8,6 @@ const storageLogger = createLogger({
|
||||||
feature: "location-cache",
|
feature: "location-cache",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LOCATION_STORAGE_KEY = "@last_known_location";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores location data in AsyncStorage with timestamp
|
* Stores location data in AsyncStorage with timestamp
|
||||||
* @param {Object} coords - Location coordinates object
|
* @param {Object} coords - Location coordinates object
|
||||||
|
@ -36,7 +35,7 @@ export async function storeLocation(
|
||||||
});
|
});
|
||||||
|
|
||||||
await AsyncStorage.setItem(
|
await AsyncStorage.setItem(
|
||||||
LOCATION_STORAGE_KEY,
|
STORAGE_KEYS.LAST_KNOWN_LOCATION,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
coords,
|
coords,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -58,7 +57,7 @@ export async function storeLocation(
|
||||||
export async function getStoredLocation() {
|
export async function getStoredLocation() {
|
||||||
try {
|
try {
|
||||||
storageLogger.debug("Retrieving stored location data");
|
storageLogger.debug("Retrieving stored location data");
|
||||||
const stored = await AsyncStorage.getItem(LOCATION_STORAGE_KEY);
|
const stored = await AsyncStorage.getItem(STORAGE_KEYS.LAST_KNOWN_LOCATION);
|
||||||
|
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
storageLogger.debug("No stored location data found");
|
storageLogger.debug("No stored location data found");
|
||||||
|
|
35
yarn.lock
35
yarn.lock
|
@ -3128,7 +3128,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.8.4":
|
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.8.4":
|
||||||
version: 7.22.6
|
version: 7.22.6
|
||||||
resolution: "@babel/runtime@npm:7.22.6"
|
resolution: "@babel/runtime@npm:7.22.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6948,8 +6948,8 @@ __metadata:
|
||||||
ajv-formats: "npm:^2.1.1"
|
ajv-formats: "npm:^2.1.1"
|
||||||
ajv-keywords: "npm:^5.1.0"
|
ajv-keywords: "npm:^5.1.0"
|
||||||
apollo-link-sentry: "npm:^4.0.0"
|
apollo-link-sentry: "npm:^4.0.0"
|
||||||
axios: "npm:^1.4.0"
|
axios: "npm:^1.10.0"
|
||||||
axios-retry: "npm:^3.5.1"
|
axios-retry: "npm:^4.5.0"
|
||||||
babel-eslint: "npm:^10.1.0"
|
babel-eslint: "npm:^10.1.0"
|
||||||
babel-plugin-module-resolver: "npm:^5.0.0"
|
babel-plugin-module-resolver: "npm:^5.0.0"
|
||||||
babel-plugin-root-import: "npm:^6.6.0"
|
babel-plugin-root-import: "npm:^6.6.0"
|
||||||
|
@ -7460,24 +7460,25 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"axios-retry@npm:^3.5.1":
|
"axios-retry@npm:^4.5.0":
|
||||||
version: 3.5.1
|
version: 4.5.0
|
||||||
resolution: "axios-retry@npm:3.5.1"
|
resolution: "axios-retry@npm:4.5.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime": "npm:^7.15.4"
|
|
||||||
is-retry-allowed: "npm:^2.2.0"
|
is-retry-allowed: "npm:^2.2.0"
|
||||||
checksum: 10/3ff8d4a5350abf356dd65749e31c6b34cbe3ecfab59e8631a188bc0fef524a67ddf112c60abd8fa92a766bfda23b3a82c9a74d384870e2f32a4bd212baadd9b2
|
peerDependencies:
|
||||||
|
axios: 0.x || 1.x
|
||||||
|
checksum: 10/39ed05248757387a44dde94255df8ad54088aece50574c6ce9a1cd02b9e40252f7390285cea54ded04e33a3a549e462d5bdacc8d3178221b7cd40e8aff09ba46
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"axios@npm:^1.4.0":
|
"axios@npm:^1.10.0":
|
||||||
version: 1.4.0
|
version: 1.10.0
|
||||||
resolution: "axios@npm:1.4.0"
|
resolution: "axios@npm:1.10.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: "npm:^1.15.0"
|
follow-redirects: "npm:^1.15.6"
|
||||||
form-data: "npm:^4.0.0"
|
form-data: "npm:^4.0.0"
|
||||||
proxy-from-env: "npm:^1.1.0"
|
proxy-from-env: "npm:^1.1.0"
|
||||||
checksum: 10/b987e4259e5cfc93e95ee306c267a44380bbc045789a91b716e8434a75e22987344605eb4e133482fe285dd3a2e0b7e791ba26999965f04a5ecdde25f56930cb
|
checksum: 10/d43c80316a45611fd395743e15d16ea69a95f2b7f7095f2bb12cb78f9ca0a905194a02e52a3bf4e0db9f85fd1186d6c690410644c10ecd8bb0a468e57c2040e4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -11452,13 +11453,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"follow-redirects@npm:^1.15.0":
|
"follow-redirects@npm:^1.15.6":
|
||||||
version: 1.15.2
|
version: 1.15.9
|
||||||
resolution: "follow-redirects@npm:1.15.2"
|
resolution: "follow-redirects@npm:1.15.9"
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
debug:
|
debug:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10/8be0d39919770054812537d376850ccde0b4762b0501c440bd08724971a078123b55f57704f2984e0664fecc0c86adea85add63295804d9dce401cd9604c91d3
|
checksum: 10/e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue