Compare commits

..

No commits in common. "main" and "fix/headless-task" have entirely different histories.

66 changed files with 2345 additions and 3377 deletions

View file

@ -2,142 +2,6 @@
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.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)

View file

@ -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 191 versionCode 181
versionName "1.11.1" versionName "1.10.1"
multiDexEnabled true multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -64,10 +64,6 @@
<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="/"/>

View file

@ -1,7 +0,0 @@
<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>

View file

@ -4,6 +4,4 @@
<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>

View file

@ -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') ?: '35') targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
ndkVersion = "26.1.10909125" ndkVersion = "26.1.10909125"

View file

@ -131,12 +131,12 @@ let config = {
"tel", "tel",
"telprompt", "telprompt",
], ],
BGTaskSchedulerPermittedIdentifiers: [
"com.transistorsoft.fetch",
"com.transistorsoft.customtask",
],
}, },
UIBackgroundModes: ["location", "fetch", "processing"], UIBackgroundModes: ["location", "fetch", "processing"],
BGTaskSchedulerPermittedIdentifiers: [
"com.transistorsoft.fetch",
"com.transistorsoft.customtask",
],
}, },
plugins: [ plugins: [
[ [

View file

@ -97,72 +97,3 @@ 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

191
index.js
View file

@ -19,8 +19,7 @@ import onMessageReceived from "~/notifications/onMessageReceived";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import * as Sentry from "@sentry/react-native"; import * as Sentry from "@sentry/react-native";
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
// setup notification, this have to stay in index.js // setup notification, this have to stay in index.js
notifee.onBackgroundEvent(notificationBackgroundEvent); notifee.onBackgroundEvent(notificationBackgroundEvent);
@ -32,15 +31,13 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
registerRootComponent(App); registerRootComponent(App);
// Constants for persistence // Constants for persistence
const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000; const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
// const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
// Helper functions for persisting sync time // Helper functions for persisting sync time
const getLastSyncTime = async () => { const getLastSyncTime = async () => {
try { try {
const value = await memoryAsyncStorage.getItem( const value = await AsyncStorage.getItem(LAST_SYNC_TIME_KEY);
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
);
return value ? parseInt(value, 10) : Date.now(); return value ? parseInt(value, 10) : Date.now();
} catch (error) { } catch (error) {
Sentry.captureException(error, { Sentry.captureException(error, {
@ -52,10 +49,7 @@ const getLastSyncTime = async () => {
const setLastSyncTime = async (time) => { const setLastSyncTime = async (time) => {
try { try {
await memoryAsyncStorage.setItem( await AsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString());
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
time.toString(),
);
} catch (error) { } catch (error) {
Sentry.captureException(error, { Sentry.captureException(error, {
tags: { module: "headless-task", operation: "set-last-sync-time" }, tags: { module: "headless-task", operation: "set-last-sync-time" },
@ -124,24 +118,111 @@ const HeadlessTask = async (event) => {
throw new Error("Invalid event name received"); 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 }); geolocBgLogger.info("HeadlessTask event received", { name, params });
switch (name) { switch (name) {
case "heartbeat": case "heartbeat":
// Add breadcrumb for heartbeat event
Sentry.addBreadcrumb({
message: "Heartbeat event received",
category: "headless-task",
level: "info",
timestamp: Date.now() / 1000,
});
// Get persisted last sync time // Get persisted last sync time
const lastSyncTime = await getLastSyncTime(); const lastSyncTime = await getLastSyncTime();
const now = Date.now(); const now = Date.now();
const timeSinceLastSync = now - lastSyncTime; 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 // Get current position with performance tracking
const locationStartTime = Date.now();
const location = await getCurrentPosition(); 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 }); geolocBgLogger.debug("getCurrentPosition result", { location });
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) { if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
geolocBgLogger.info("Forcing location sync"); 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 { 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 // Change pace to ensure location updates with timeout
await Promise.race([ await Promise.race([
BackgroundGeolocation.changePace(true), BackgroundGeolocation.changePace(true),
@ -153,6 +234,12 @@ const HeadlessTask = async (event) => {
), ),
]); ]);
Sentry.addBreadcrumb({
message: "changePace completed",
category: "headless-task",
level: "info",
});
// Perform sync with timeout // Perform sync with timeout
const syncResult = await Promise.race([ const syncResult = await Promise.race([
BackgroundGeolocation.sync(), BackgroundGeolocation.sync(),
@ -161,8 +248,26 @@ const HeadlessTask = async (event) => {
), ),
]); ]);
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 // Update last sync time after successful sync
await setLastSyncTime(now); await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "Last sync time updated",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncError) { } catch (syncError) {
Sentry.captureException(syncError, { Sentry.captureException(syncError, {
tags: { tags: {
@ -180,6 +285,22 @@ const HeadlessTask = async (event) => {
geolocBgLogger.error("Force sync failed", { error: syncError }); 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;
@ -190,6 +311,17 @@ const HeadlessTask = async (event) => {
break; 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", { geolocBgLogger.debug("Location update received", {
location: params.location, location: params.location,
}); });
@ -205,6 +337,17 @@ const HeadlessTask = async (event) => {
const httpStatus = params.response?.status; const httpStatus = params.response?.status;
const isHttpSuccess = httpStatus === 200; 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", { geolocBgLogger.debug("HTTP response received", {
response: params.response, response: params.response,
}); });
@ -214,6 +357,13 @@ const HeadlessTask = async (event) => {
try { try {
const now = Date.now(); const now = Date.now();
await setLastSyncTime(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) { } catch (syncTimeError) {
geolocBgLogger.error("Failed to update sync time", { geolocBgLogger.error("Failed to update sync time", {
error: syncTimeError, error: syncTimeError,
@ -230,11 +380,26 @@ const HeadlessTask = async (event) => {
break; break;
default: default:
break; Sentry.addBreadcrumb({
message: "Unknown event type",
category: "headless-task",
level: "warning",
data: { eventName: name },
});
} }
// Task completed successfully // Task completed successfully
const taskDuration = Date.now() - taskStartTime; 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; const taskDuration = Date.now() - taskStartTime;

View file

@ -1,249 +0,0 @@
#!/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

View file

@ -1,11 +1,16 @@
# This `.xcode.env` file is versioned and is used to source the environment # This is used by the React Native CLI to control various options
# used when running script phases inside Xcode. # Configuration name to load
# To customize your local environment, you can create an `.xcode.env.local` CONFIGURATION_NAME=Debug
# file that is not versioned. # Path to the Xcode project
PROJECT_PATH="AlerteSecours.xcodeproj"
# Whether to enable the New Architecture
RCT_NEW_ARCH_ENABLED=0
# Whether to enable Hermes
USE_HERMES=1
# NODE_BINARY variable contains the PATH to the node executable. # Sentry Configuration
# export SENTRY_PROPERTIES="ios/sentry.properties"
# Customize the NODE_BINARY variable here. export AUTO_RELEASE=true
# For example, to use nvm with brew, add the following line export SENTRY_CLI_EXTRA_ARGS="--log-level debug"
# . "$(brew --prefix nvm)/nvm.sh" --no-use export SENTRY_CLI_RN_XCODE_EXTRA_ARGS="--allow-fetch"
export NODE_BINARY=$(command -v node) export SENTRY_INCLUDE_NATIVE_SOURCES=true

File diff suppressed because one or more lines are too long

View file

@ -1,21 +1,24 @@
#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 = @{};

View file

@ -0,0 +1,30 @@
<?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>

View file

@ -0,0 +1,21 @@
{
"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.

After

Width:  |  Height:  |  Size: 160 KiB

View file

@ -0,0 +1,21 @@
{
"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.

After

Width:  |  Height:  |  Size: 70 B

View file

@ -2,11 +2,6 @@
<!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.fetch</string>
<string>com.transistorsoft.customtask</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@ -24,7 +19,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.11.1</string> <string>1.10.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -47,7 +42,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>191</string> <string>181</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View file

@ -1,10 +1,9 @@
<?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="32700.99.1234" 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="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina5_5" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<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>
@ -13,29 +12,34 @@
<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="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
<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="0.0" y="0.0"/> <point key="canvasLocation" x="140.625" y="129.4921875"/>
</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"/>

View file

@ -0,0 +1,25 @@
<?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&amp;channel=release</string>
</dict>
</plist>

View file

@ -0,0 +1,30 @@
<?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>

View file

@ -1,5 +1,19 @@
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 {}
@ -7,18 +21,11 @@ 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']
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' use_autolinking_method_symbol = ('use' + '_native' + '_modules!').to_sym
install! 'cocoapods', origin_autolinking_method = self.method(use_autolinking_method_symbol)
:deterministic_uuids => false self.define_singleton_method(use_autolinking_method_symbol) do |*args|
if ENV['EXPO_UNSTABLE_CORE_AUTOLINKING'] == '1'
prepare_react_native_project! Pod::UI.puts('Using expo-modules-autolinking as core autolinking source'.green)
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',
@ -29,13 +36,53 @@ target 'AlerteSecours' do
'--platform', '--platform',
'ios' 'ios'
] ]
origin_autolinking_method.call(config_command)
else
origin_autolinking_method.call()
end end
end
config = use_native_modules!(config_command) platform :ios, '13.4'
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',
@ -65,5 +112,30 @@ 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

File diff suppressed because it is too large Load diff

View file

@ -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"
} }

View file

@ -0,0 +1,8 @@
<?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>

View file

@ -1,6 +1,6 @@
{ {
"name": "alerte-secours", "name": "alerte-secours",
"version": "1.11.1", "version": "1.10.1",
"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 -rf ./node_modules dist/* ios/build ios/Pods ios/KScoreApp.xcarchive android/build android/app/build || true", "clean": "\\rm -fr ./node_modules && \\rm -fr dist/* && \\rm -fr ios/build ios/Pods ios/KScoreApp.xcarchive && \\rm -fr android/build android/app/build",
"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,7 +37,6 @@
"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'",
@ -50,8 +49,8 @@
"screenshot:android": "scripts/screenshot-android.sh" "screenshot:android": "scripts/screenshot-android.sh"
}, },
"customExpoVersioning": { "customExpoVersioning": {
"versionCode": 191, "versionCode": 181,
"buildNumber": 191 "buildNumber": 181
}, },
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {
@ -109,8 +108,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.10.0", "axios": "^1.4.0",
"axios-retry": "^4.5.0", "axios-retry": "^3.5.1",
"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",
@ -279,4 +278,4 @@
} }
}, },
"packageManager": "yarn@4.5.3" "packageManager": "yarn@4.5.3"
} }

View file

@ -6,9 +6,7 @@ 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, paramsActions } from "~/stores"; import { authActions, permissionWizardActions } from "~/stores";
import { secureStore } from "~/storage/memorySecureStore";
import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
import "~/lib/mapbox"; import "~/lib/mapbox";
import "~/i18n"; import "~/i18n";
@ -44,7 +42,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 initFn(); await Promise.resolve(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}`, {
@ -55,14 +53,9 @@ const initializeStores = () => {
} }
}; };
// Initialize memory stores first // Initialize stores sequentially to maintain order
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");

View file

@ -39,18 +39,13 @@ export async function loginUserToken({ authToken }) {
} }
export async function storeFcmToken({ deviceId, fcmToken }) { export async function storeFcmToken({ deviceId, fcmToken }) {
const { data, errors } = await network.apolloClient.mutate({ const { data } = 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 };
} }

View file

@ -1,50 +1,13 @@
import { secureStore } from "~/storage/memorySecureStore"; import { secureStore } from "~/lib/secureStore";
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() {
// If a UUID generation is already in progress, wait for it let deviceUuid = await secureStore.getItemAsync("deviceUuid");
if (uuidGenerationPromise) { if (!deviceUuid) {
deviceLogger.debug("UUID generation already in progress, waiting..."); deviceUuid = uuidGenerator.v4();
return await uuidGenerationPromise; await secureStore.setItemAsync("deviceUuid", deviceUuid);
} }
return deviceUuid;
// 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;
} finally {
// Clear the promise so future calls can proceed
uuidGenerationPromise = null;
}
})();
return await uuidGenerationPromise;
} }
export { getDeviceUuid }; export { getDeviceUuid };

View file

@ -1,7 +1,6 @@
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 "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import Text from "../Text"; import Text from "../Text";
@ -65,12 +64,14 @@ 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(STORAGE_KEYS.EULA_ACCEPTED, "true"); await AsyncStorage.setItem(EULA_STORAGE_KEY, "true");
onAccept(); onAccept();
} catch (error) { } catch (error) {
console.error("Error saving EULA acceptance:", error); console.error("Error saving EULA acceptance:", error);

View file

@ -11,8 +11,6 @@ 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";
@ -213,23 +211,6 @@ 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 {

View file

@ -256,7 +256,15 @@ 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.",
); );
} }
// Battery optimization warning is now handled in the Android settings box if (
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) => (
@ -272,36 +280,12 @@ const HeroMode = () => {
}; };
const renderAndroidPermissionWarning = () => { const renderAndroidPermissionWarning = () => {
const hasBatteryOptimizationIssue =
batteryOptimizationEnabled && batteryOptAttempted;
return ( return (
<View <View style={styles.androidWarning}>
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.
@ -322,14 +306,9 @@ const HeroMode = () => {
version d'Android) version d'Android)
</Text> </Text>
</View> </View>
{hasBatteryOptimizationIssue && ( {batteryOptimizationEnabled && batteryOptAttempted && (
<View style={styles.androidWarningSteps}> <View style={styles.androidWarningSteps}>
<Text <Text style={styles.androidWarningText}>
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}>
@ -648,10 +627,6 @@ 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",
@ -689,23 +664,6 @@ 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,

View file

@ -1,6 +1,8 @@
import { Platform } from "react-native"; import { Platform } from "react-native";
import { secureStore } from "~/storage/memorySecureStore"; import { secureStore } from "~/lib/secureStore";
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;
@ -95,7 +97,7 @@ export const setStaging = async (enabled) => {
} }
// Persist the staging setting // Persist the staging setting
await secureStore.setItemAsync(STORAGE_KEYS.ENV_IS_STAGING, String(enabled)); await secureStore.setItemAsync(STAGING_SETTING_KEY, String(enabled));
}; };
// Initialize with default values // Initialize with default values
@ -104,9 +106,7 @@ 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( const storedStaging = await secureStore.getItemAsync(STAGING_SETTING_KEY);
STORAGE_KEYS.ENV_IS_STAGING,
);
if (storedStaging !== null) { if (storedStaging !== null) {
const isStaging = storedStaging === "true"; const isStaging = storedStaging === "true";
if (isStaging) { if (isStaging) {

View file

@ -1,8 +1,9 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import AsyncStorage from "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
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);
@ -15,7 +16,7 @@ export const useEULA = () => {
const checkEULA = async () => { const checkEULA = async () => {
try { try {
const accepted = await AsyncStorage.getItem(STORAGE_KEYS.EULA_ACCEPTED); const accepted = await AsyncStorage.getItem(EULA_STORAGE_KEY);
setEulaAccepted(!!accepted); setEulaAccepted(!!accepted);
} catch (error) { } catch (error) {
console.error("Error checking EULA status:", error); console.error("Error checking EULA status:", error);

View file

@ -1,9 +1,10 @@
import BackgroundGeolocation from "react-native-background-geolocation"; import BackgroundGeolocation from "react-native-background-geolocation";
import AsyncStorage from "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
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;
@ -17,9 +18,7 @@ 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( const storedValue = await AsyncStorage.getItem(EMULATOR_MODE_KEY);
STORAGE_KEYS.EMULATOR_MODE_ENABLED,
);
emulatorLogger.debug("Initializing emulator mode", { storedValue }); emulatorLogger.debug("Initializing emulator mode", { storedValue });
if (storedValue === "true") { if (storedValue === "true") {
@ -59,7 +58,7 @@ export const enableEmulatorMode = async () => {
isEmulatorModeEnabled = true; isEmulatorModeEnabled = true;
// Persist the setting // Persist the setting
await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "true"); await AsyncStorage.setItem(EMULATOR_MODE_KEY, "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", {
@ -82,7 +81,7 @@ export const disableEmulatorMode = async () => {
// Persist the setting // Persist the setting
try { try {
await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "false"); await AsyncStorage.setItem(EMULATOR_MODE_KEY, "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", {

View file

@ -4,8 +4,17 @@ 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 { getAuthState, subscribeAuthState, permissionsActions } from "~/stores"; import throttle from "lodash.throttle";
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";
@ -67,6 +76,9 @@ 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", {
@ -96,6 +108,25 @@ 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);
@ -140,6 +171,19 @@ 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 &&
@ -151,12 +195,100 @@ export default async function trackLocation() {
} }
}); });
BackgroundGeolocation.onHttp(async (response) => { // The core auth reload function that will be throttled
// log status code and response function _reloadAuth() {
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, 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,
});
}
reloadAuth();
break;
}
}); });
try { try {

View file

@ -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, 9); const section2 = routes.slice(5, 8);
const section3 = routes.slice(9, routes.length); const section3 = routes.slice(8, routes.length);
return ( return (
<> <>

View file

@ -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;
} }

View file

@ -1,23 +0,0 @@
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",
},
},
]);
}

View file

@ -1,26 +0,0 @@
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",
});
}

View file

@ -1,67 +0,0 @@
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",
);
}

View file

@ -1,10 +0,0 @@
import { createChannel } from "../helpers";
const channelId = "system";
export async function createNotificationChannel() {
await createChannel({
id: channelId,
name: "Paramètres",
});
}

View file

@ -88,9 +88,9 @@ export const generateSuggestKeepOpenContent = (data) => {
export const generateBackgroundGeolocationLostContent = (data) => { export const generateBackgroundGeolocationLostContent = (data) => {
return { return {
title: `Alerte-Secours ne peut plus accéder à votre position`, title: `Localisation en arrière-plan désactivée`,
body: `Vous ne pouvez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`, body: `Votre localisation en arrière-plan a été désactivée. Veuillez vérifier les paramètres de l'application.`,
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.`, 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.`,
}; };
}; };

View file

@ -7,7 +7,6 @@ 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";
const displayLogger = createLogger({ const displayLogger = createLogger({
module: BACKGROUND_SCOPES.NOTIFICATIONS, module: BACKGROUND_SCOPES.NOTIFICATIONS,
@ -21,7 +20,6 @@ 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,
}; };
export default async function displayNotificationHandler(data) { export default async function displayNotificationHandler(data) {

View file

@ -1,7 +1,6 @@
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;
@ -84,16 +83,7 @@ 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) => { background_geolocation_lost: async (data) => openSettings(),
navActions.setNextNavigation([
{
name: "Params",
params: {
anchor: "permissions",
},
},
]);
},
}; };
return { return {

View file

@ -14,8 +14,6 @@ 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";
@ -98,12 +96,11 @@ export const onNotificationOpenedAppEvent = async (remoteMessage) => {
// return; // return;
// } // }
try { try {
eventLogger.debug("Processing background notification tap", { eventLogger.info("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) {
@ -276,28 +273,5 @@ 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;
}
} }
}; };

View file

@ -6,7 +6,6 @@ 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
@ -18,7 +17,6 @@ export default async function setActionCategories() {
createSuggestKeepOpenChannel(), createSuggestKeepOpenChannel(),
createRelativeAllowAskChannel(), createRelativeAllowAskChannel(),
createRelativeInvitationChannel(), createRelativeInvitationChannel(),
createSystemChannel(),
]); ]);
} catch (error) { } catch (error) {
const errorData = { const errorData = {
@ -115,20 +113,5 @@ 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,
},
],
},
]); ]);
} }

View file

@ -1,78 +0,0 @@
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;

View file

@ -1,40 +1,17 @@
import React, { useCallback, useRef } from "react"; import React from "react";
import { View, ScrollView, InteractionManager } from "react-native"; import { View, ScrollView } 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 ref={scrollRef} style={styles.scrollView}> <ScrollView style={styles.scrollView}>
<View style={styles.container}> <View style={styles.container}>
<View style={styles.section}> <View style={styles.section}>
<ThemeSwitcher /> <ThemeSwitcher />
@ -49,9 +26,6 @@ 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>

View file

@ -114,14 +114,11 @@ 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,

View file

@ -3,15 +3,6 @@ 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 = () => {
@ -32,139 +23,60 @@ const getReleaseVersion = () => {
return `com.alertesecours@${version}+${buildNumber}`; return `com.alertesecours@${version}+${buildNumber}`;
}; };
// Check if Sentry is enabled by user preference Sentry.init({
const checkSentryEnabled = async () => { dsn: env.SENTRY_DSN,
try { tracesSampleRate: 1.0,
// Wait for memory storage to be initialized debug: __DEV__,
let retries = 0; // Configure release to match ios-archive.sh format
const maxRetries = 10; release: getReleaseVersion(),
// Use BUILD_TIME from env to match the value used in sourcemap upload
dist: env.BUILD_TIME,
enableNative: true,
attachStacktrace: true,
environment: __DEV__ ? "development" : "production",
normalizeDepth: 10,
maxBreadcrumbs: 100,
// Enable debug ID tracking
_experiments: {
debugIds: true,
},
beforeSend(event) {
event.extra = {
...event.extra,
jsEngine: global.HermesInternal ? "hermes" : "jsc",
hermesEnabled: !!global.HermesInternal,
version: packageJson.version,
buildNumber: getBuildNumber(),
buildTime: env.BUILD_TIME,
};
while (retries < maxRetries) { if (event.exception) {
try { event.exception.values = event.exception.values?.map((value) => ({
const stored = await memoryAsyncStorage.getItem( ...value,
STORAGE_KEYS.SENTRY_ENABLED, mechanism: {
); ...value.mechanism,
if (stored !== null) { handled: true,
return JSON.parse(stored); synthetic: false,
} type: "hermes",
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 return event;
const initializeSentry = async () => { },
const isEnabled = await checkSentryEnabled(); beforeBreadcrumb(breadcrumb) {
if (breadcrumb.category === "console") {
Sentry.init({
dsn: env.SENTRY_DSN,
enabled: isEnabled,
tracesSampleRate: 0.1,
debug: __DEV__,
// Configure release to match ios-archive.sh format
release: getReleaseVersion(),
// Use BUILD_TIME from env to match the value used in sourcemap upload
dist: env.BUILD_TIME,
enableNative: true,
attachStacktrace: true,
environment: __DEV__ ? "development" : "production",
normalizeDepth: 10,
maxBreadcrumbs: 100,
// Enable debug ID tracking
_experiments: {
debugIds: true,
},
beforeSend(event) {
event.extra = {
...event.extra,
jsEngine: global.HermesInternal ? "hermes" : "jsc",
hermesEnabled: !!global.HermesInternal,
version: packageJson.version,
buildNumber: getBuildNumber(),
buildTime: env.BUILD_TIME,
};
if (event.exception) {
event.exception.values = event.exception.values?.map((value) => ({
...value,
mechanism: {
...value.mechanism,
handled: true,
synthetic: false,
type: "hermes",
},
}));
}
return event;
},
beforeBreadcrumb(breadcrumb) {
if (breadcrumb.category === "console") {
return breadcrumb;
}
return breadcrumb; return breadcrumb;
},
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
// Sentry.mobileReplayIntegration({
// maskAllText: false,
// maskAllImages: 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) { return breadcrumb;
sentryLogger.warn("Failed to toggle Sentry state", { },
error: error.message, replaysSessionSampleRate: 0.1,
}); replaysOnErrorSampleRate: 1.0,
} integrations: [
}; // Sentry.mobileReplayIntegration({
// maskAllText: false,
// maskAllImages: false,
// maskAllVectors: false,
// }),
],
});

View file

@ -1,283 +0,0 @@
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;

View file

@ -1,191 +0,0 @@
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;

View file

@ -1,84 +0,0 @@
/**
* 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"),
};

View file

@ -1,7 +1,8 @@
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 "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
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 = {};
@ -9,7 +10,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(
STORAGE_KEYS.OVERRIDE_MESSAGES, OVERRIDE_MESSAGES_STORAGE_KEY,
); );
const storedMessages = storedData ? JSON.parse(storedData) : {}; const storedMessages = storedData ? JSON.parse(storedData) : {};
Object.entries(storedMessages).forEach(([messageId, data]) => { Object.entries(storedMessages).forEach(([messageId, data]) => {
@ -23,7 +24,7 @@ export default createAtom(({ merge, set, get, reset }) => {
const saveOverrideMessagesToStorage = async () => { const saveOverrideMessagesToStorage = async () => {
try { try {
await AsyncStorage.setItem( await AsyncStorage.setItem(
STORAGE_KEYS.OVERRIDE_MESSAGES, OVERRIDE_MESSAGES_STORAGE_KEY,
JSON.stringify(overrideMessagesCache), JSON.stringify(overrideMessagesCache),
); );
} catch (error) { } catch (error) {

View file

@ -1,5 +1,4 @@
import { secureStore } from "~/storage/memorySecureStore"; import { secureStore } from "~/lib/secureStore";
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";
@ -11,13 +10,13 @@ import isExpired from "~/lib/time/isExpired";
import { registerUser, loginUserToken } from "~/auth/actions"; import { registerUser, loginUserToken } from "~/auth/actions";
// DEV // DEV
// SecureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN); // SecureStore.deleteItemAsync("userToken");
// SecureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN); // SecureStore.deleteItemAsync("authToken");
// SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN); // SecureStore.deleteItemAsync("dev.userToken");
// SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN); // SecureStore.deleteItemAsync("dev.authToken");
// SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN); // SecureStore.deleteItemAsync("anon.userToken");
// SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN); // SecureStore.deleteItemAsync("anon.authToken");
// SecureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN).then((t) => authLogger.debug("User token", { token: t })); // SecureStore.getItemAsync("userToken").then((t) => authLogger.debug("User token", { token: t }));
const authLogger = createLogger({ const authLogger = createLogger({
module: FEATURE_SCOPES.AUTH, module: FEATURE_SCOPES.AUTH,
@ -69,7 +68,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(STORAGE_KEYS.USER_TOKEN, userToken); await secureStore.setItemAsync("userToken", userToken);
endLoading({ endLoading({
userToken, userToken,
}); });
@ -82,8 +81,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(STORAGE_KEYS.AUTH_TOKEN), secureStore.deleteItemAsync("authToken"),
secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN), secureStore.deleteItemAsync("userToken"),
]); ]);
return init(); return init();
} }
@ -94,8 +93,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(STORAGE_KEYS.USER_TOKEN), userToken: secureStore.getItemAsync("userToken"),
authToken: secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN), authToken: secureStore.getItemAsync("authToken"),
}); });
// await delay(5); // await delay(5);
// authLogger.debug("Auth tokens", { userToken, authToken }); // authLogger.debug("Auth tokens", { userToken, authToken });
@ -122,7 +121,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(STORAGE_KEYS.AUTH_TOKEN, authToken); await secureStore.setItemAsync("authToken", authToken);
} }
if (!userToken && authToken) { if (!userToken && authToken) {
@ -154,7 +153,6 @@ 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;
} }
@ -164,15 +162,9 @@ 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 });
@ -184,7 +176,7 @@ export default createAtom(({ get, merge, getActions }) => {
const { onReloadAuthToken: authToken } = get(); const { onReloadAuthToken: authToken } = get();
if (authToken) { if (authToken) {
await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken); await secureStore.setItemAsync("authToken", authToken);
await loadUserJWT(authToken); await loadUserJWT(authToken);
} else { } else {
await init(); await init();
@ -205,12 +197,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(STORAGE_KEYS.AUTH_TOKEN), secureStore.getItemAsync("authToken"),
secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN), secureStore.getItemAsync("userToken"),
]); ]);
await Promise.all([ await Promise.all([
secureStore.setItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN, anonAuthToken), secureStore.setItemAsync("anon.authToken", anonAuthToken),
secureStore.setItemAsync(STORAGE_KEYS.ANON_USER_TOKEN, anonUserToken), secureStore.setItemAsync("anon.userToken", anonUserToken),
]); ]);
} }
merge({ onReloadAuthToken: authTokenJwt }); merge({ onReloadAuthToken: authTokenJwt });
@ -220,12 +212,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(STORAGE_KEYS.AUTH_TOKEN), secureStore.getItemAsync("authToken"),
secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN), secureStore.getItemAsync("userToken"),
]); ]);
await Promise.all([ await Promise.all([
secureStore.setItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN, anonAuthToken), secureStore.setItemAsync("dev.authToken", anonAuthToken),
secureStore.setItemAsync(STORAGE_KEYS.DEV_USER_TOKEN, anonUserToken), secureStore.setItemAsync("dev.userToken", anonUserToken),
]); ]);
merge({ onReloadAuthToken: authTokenJwt }); merge({ onReloadAuthToken: authTokenJwt });
triggerReload(); triggerReload();
@ -235,29 +227,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(STORAGE_KEYS.DEV_AUTH_TOKEN), secureStore.getItemAsync("dev.authToken"),
secureStore.getItemAsync(STORAGE_KEYS.DEV_USER_TOKEN), secureStore.getItemAsync("dev.userToken"),
secureStore.getItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN), secureStore.getItemAsync("anon.authToken"),
secureStore.getItemAsync(STORAGE_KEYS.ANON_USER_TOKEN), secureStore.getItemAsync("anon.userToken"),
]); ]);
if (devAuthToken && devUserToken) { if (devAuthToken && devUserToken) {
await Promise.all([ await Promise.all([
secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, devAuthToken), secureStore.setItemAsync("authToken", devAuthToken),
secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, devUserToken), secureStore.setItemAsync("userToken", devUserToken),
secureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN), secureStore.deleteItemAsync("dev.authToken"),
secureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN), secureStore.deleteItemAsync("dev.userToken"),
]); ]);
} else if (anonAuthToken && anonUserToken) { } else if (anonAuthToken && anonUserToken) {
await Promise.all([ await Promise.all([
secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, anonAuthToken), secureStore.setItemAsync("authToken", anonAuthToken),
secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, anonUserToken), secureStore.setItemAsync("userToken", anonUserToken),
secureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN), secureStore.deleteItemAsync("anon.authToken"),
secureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN), secureStore.deleteItemAsync("anon.userToken"),
]); ]);
} else { } else {
await Promise.all([ await Promise.all([
secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN), secureStore.deleteItemAsync("authToken"),
secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN), secureStore.deleteItemAsync("userToken"),
]); ]);
merge({ merge({
userOffMode: true, userOffMode: true,
@ -276,31 +268,6 @@ 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,
@ -320,7 +287,6 @@ export default createAtom(({ get, merge, getActions }) => {
logout, logout,
onReload, onReload,
userOnMode, userOnMode,
setUserToken,
}, },
}; };
}); });

View file

@ -1,6 +1,5 @@
import { createAtom } from "~/lib/atomic-zustand"; import { createAtom } from "~/lib/atomic-zustand";
import { secureStore } from "~/storage/memorySecureStore"; import { secureStore } from "~/lib/secureStore";
import { STORAGE_KEYS } from "~/storage/storageKeys";
export default createAtom(({ merge, reset }) => { export default createAtom(({ merge, reset }) => {
const setFcmToken = (token) => { const setFcmToken = (token) => {
@ -10,11 +9,8 @@ export default createAtom(({ merge, reset }) => {
}; };
const setFcmTokenStored = ({ fcmToken, deviceId }) => { const setFcmTokenStored = ({ fcmToken, deviceId }) => {
secureStore.setItemAsync(STORAGE_KEYS.FCM_TOKEN_STORED, fcmToken); secureStore.setItemAsync("fcmTokenStored", fcmToken);
secureStore.setItemAsync( secureStore.setItemAsync("fcmTokenStoredDeviceId", deviceId.toString());
STORAGE_KEYS.FCM_TOKEN_STORED_DEVICE_ID,
deviceId.toString(),
);
merge({ merge({
fcmTokenStored: fcmToken, fcmTokenStored: fcmToken,
deviceId, deviceId,
@ -22,11 +18,9 @@ export default createAtom(({ merge, reset }) => {
}; };
const init = async () => { const init = async () => {
const fcmTokenStored = await secureStore.getItemAsync( const fcmTokenStored = await secureStore.getItemAsync("fcmTokenStored");
STORAGE_KEYS.FCM_TOKEN_STORED,
);
const fcmTokenStoredDeviceId = await secureStore.getItemAsync( const fcmTokenStoredDeviceId = await secureStore.getItemAsync(
STORAGE_KEYS.FCM_TOKEN_STORED_DEVICE_ID, "fcmTokenStoredDeviceId",
); );
const deviceId = fcmTokenStoredDeviceId const deviceId = fcmTokenStoredDeviceId
? parseInt(fcmTokenStoredDeviceId, 10) ? parseInt(fcmTokenStoredDeviceId, 10)

View file

@ -61,7 +61,7 @@ export default createAtom(({ get, merge, reset }) => {
...m, ...m,
routeName, routeName,
}); });
navLogger.debug("Route updated", { routeName }); navLogger.info("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.debug("Resetting navigation state to initial values"); navLogger.info("Resetting navigation state to initial values");
reset(); reset();
}, },
updateRouteFromRootStack: (state) => { updateRouteFromRootStack: (state) => {
navLogger.debug("Updating route from root stack", { state }); navLogger.info("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.debug("Updating route from drawer", { state }); navLogger.info("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.debug("Updating route from main", { state }); navLogger.info("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.debug("Setting next navigation", { nextNavigation }); navLogger.info("Setting next navigation", { nextNavigation });
merge({ merge({
nextNavigation, nextNavigation,
}); });
}, },
setMessageViewFocus: (isFocused, alertId = null) => { setMessageViewFocus: (isFocused, alertId = null) => {
navLogger.debug("Setting message view focus", { isFocused, alertId }); navLogger.info("Setting message view focus", { isFocused, alertId });
merge({ merge({
isOnMessageView: isFocused, isOnMessageView: isFocused,
currentMessageAlertId: isFocused ? alertId : null, currentMessageAlertId: isFocused ? alertId : null,

View file

@ -1,13 +1,4 @@
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) => {
@ -46,45 +37,6 @@ 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,
@ -94,7 +46,6 @@ export default createAtom(({ merge, reset }) => {
mapColorScheme: "auto", mapColorScheme: "auto",
hasRegisteredRelatives: null, hasRegisteredRelatives: null,
alertListSortBy: "location", alertListSortBy: "location",
sentryEnabled: true,
}, },
actions: { actions: {
reset, reset,
@ -104,8 +55,6 @@ export default createAtom(({ merge, reset }) => {
setMapColorScheme, setMapColorScheme,
setHasRegisteredRelatives, setHasRegisteredRelatives,
setAlertListSortBy, setAlertListSortBy,
setSentryEnabled,
init,
}, },
}; };
}); });

View file

@ -1,13 +1,12 @@
import { createAtom } from "~/lib/atomic-zustand"; import { createAtom } from "~/lib/atomic-zustand";
import AsyncStorage from "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
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( const wizardCompleted = await AsyncStorage.getItem(WIZARD_COMPLETED_KEY);
STORAGE_KEYS.PERMISSION_WIZARD_COMPLETED,
);
if (wizardCompleted === "true") { if (wizardCompleted === "true") {
set("completed", true); set("completed", true);
} }
@ -28,10 +27,7 @@ export default createAtom(({ set, get }) => {
setCompleted: (completed) => { setCompleted: (completed) => {
set("completed", completed); set("completed", completed);
if (completed) { if (completed) {
AsyncStorage.setItem( AsyncStorage.setItem(WIZARD_COMPLETED_KEY, "true").catch((error) => {
STORAGE_KEYS.PERMISSION_WIZARD_COMPLETED,
"true",
).catch((error) => {
console.error("Error saving permission wizard status:", error); console.error("Error saving permission wizard status:", error);
}); });
} }

View file

@ -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",

View file

@ -1,13 +1,14 @@
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 "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
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 () => {
@ -27,17 +28,12 @@ const checkForUpdate = async () => {
return; return;
} }
try { try {
const lastCheckString = await AsyncStorage.getItem( const lastCheckString = await AsyncStorage.getItem(LAST_UPDATE_CHECK_KEY);
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( await AsyncStorage.setItem(LAST_UPDATE_CHECK_KEY, nowDate.toISOString());
STORAGE_KEYS.LAST_UPDATE_CHECK_TIME,
nowDate.toISOString(),
);
const update = await Updates.checkForUpdateAsync(); const update = await Updates.checkForUpdateAsync();
if (!update.isAvailable) { if (!update.isAvailable) {

View file

@ -1,5 +1,4 @@
import AsyncStorage from "~/storage/memoryAsyncStorage"; import AsyncStorage from "@react-native-async-storage/async-storage";
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";
@ -8,6 +7,8 @@ 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
@ -35,7 +36,7 @@ export async function storeLocation(
}); });
await AsyncStorage.setItem( await AsyncStorage.setItem(
STORAGE_KEYS.LAST_KNOWN_LOCATION, LOCATION_STORAGE_KEY,
JSON.stringify({ JSON.stringify({
coords, coords,
timestamp, timestamp,
@ -57,7 +58,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(STORAGE_KEYS.LAST_KNOWN_LOCATION); const stored = await AsyncStorage.getItem(LOCATION_STORAGE_KEY);
if (!stored) { if (!stored) {
storageLogger.debug("No stored location data found"); storageLogger.debug("No stored location data found");

View file

@ -3128,7 +3128,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@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": "@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":
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.10.0" axios: "npm:^1.4.0"
axios-retry: "npm:^4.5.0" axios-retry: "npm:^3.5.1"
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,25 +7460,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"axios-retry@npm:^4.5.0": "axios-retry@npm:^3.5.1":
version: 4.5.0 version: 3.5.1
resolution: "axios-retry@npm:4.5.0" resolution: "axios-retry@npm:3.5.1"
dependencies: dependencies:
"@babel/runtime": "npm:^7.15.4"
is-retry-allowed: "npm:^2.2.0" is-retry-allowed: "npm:^2.2.0"
peerDependencies: checksum: 10/3ff8d4a5350abf356dd65749e31c6b34cbe3ecfab59e8631a188bc0fef524a67ddf112c60abd8fa92a766bfda23b3a82c9a74d384870e2f32a4bd212baadd9b2
axios: 0.x || 1.x
checksum: 10/39ed05248757387a44dde94255df8ad54088aece50574c6ce9a1cd02b9e40252f7390285cea54ded04e33a3a549e462d5bdacc8d3178221b7cd40e8aff09ba46
languageName: node languageName: node
linkType: hard linkType: hard
"axios@npm:^1.10.0": "axios@npm:^1.4.0":
version: 1.10.0 version: 1.4.0
resolution: "axios@npm:1.10.0" resolution: "axios@npm:1.4.0"
dependencies: dependencies:
follow-redirects: "npm:^1.15.6" follow-redirects: "npm:^1.15.0"
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/d43c80316a45611fd395743e15d16ea69a95f2b7f7095f2bb12cb78f9ca0a905194a02e52a3bf4e0db9f85fd1186d6c690410644c10ecd8bb0a468e57c2040e4 checksum: 10/b987e4259e5cfc93e95ee306c267a44380bbc045789a91b716e8434a75e22987344605eb4e133482fe285dd3a2e0b7e791ba26999965f04a5ecdde25f56930cb
languageName: node languageName: node
linkType: hard linkType: hard
@ -11453,13 +11452,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"follow-redirects@npm:^1.15.6": "follow-redirects@npm:^1.15.0":
version: 1.15.9 version: 1.15.2
resolution: "follow-redirects@npm:1.15.9" resolution: "follow-redirects@npm:1.15.2"
peerDependenciesMeta: peerDependenciesMeta:
debug: debug:
optional: true optional: true
checksum: 10/e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c checksum: 10/8be0d39919770054812537d376850ccde0b4762b0501c440bd08724971a078123b55f57704f2984e0664fecc0c86adea85add63295804d9dce401cd9604c91d3
languageName: node languageName: node
linkType: hard linkType: hard