Compare commits

..

No commits in common. "main" and "v1.10.3" have entirely different histories.

51 changed files with 2642 additions and 2728 deletions

View file

@ -2,125 +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.11](https://github.com/alerte-secours/as-app/compare/v1.11.10...v1.11.11) (2025-07-23)
## [1.11.10](https://github.com/alerte-secours/as-app/compare/v1.11.9...v1.11.10) (2025-07-22)
## [1.11.9](https://github.com/alerte-secours/as-app/compare/v1.11.8...v1.11.9) (2025-07-22)
## [1.11.8](https://github.com/alerte-secours/as-app/compare/v1.11.7...v1.11.8) (2025-07-22)
## [1.11.7](https://github.com/alerte-secours/as-app/compare/v1.11.6...v1.11.7) (2025-07-22)
## [1.11.6](https://github.com/alerte-secours/as-app/compare/v1.11.5...v1.11.6) (2025-07-21)
## [1.11.5](https://github.com/alerte-secours/as-app/compare/v1.11.4...v1.11.5) (2025-07-21)
## [1.11.4](https://github.com/alerte-secours/as-app/compare/v1.11.3...v1.11.4) (2025-07-20)
## [1.11.3](https://github.com/alerte-secours/as-app/compare/v1.11.2...v1.11.3) (2025-07-20)
## [1.11.2](https://github.com/alerte-secours/as-app/compare/v1.11.1...v1.11.2) (2025-07-19)
### Bug Fixes
* **ios:** BGTaskSchedulerPermittedIdentifiers + prebuild wip ([e6f23b8](https://github.com/alerte-secours/as-app/commit/e6f23b83be7f30adc6385dc8f3e074521c7caf22))
## [1.11.1](https://github.com/alerte-secours/as-app/compare/v1.11.0...v1.11.1) (2025-07-18)
## [1.11.0](https://github.com/alerte-secours/as-app/compare/v1.10.9...v1.11.0) (2025-07-12)
### Features
* **anchor:** bglost notification scroll to permissions + wip ([fd081d4](https://github.com/alerte-secours/as-app/commit/fd081d46a657059353bfb1e6022b68eedaee4e1a))
* optout sentry reporting ([c1b220f](https://github.com/alerte-secours/as-app/commit/c1b220f0078db8d07b3d58a7ac146d8af159a17d))
### Bug Fixes
* force location sync storage effect on interval ([7f30ef9](https://github.com/alerte-secours/as-app/commit/7f30ef9abf11bbdade5c3db3bfd7269dc212b39c))
* menu paramètres ([754e149](https://github.com/alerte-secours/as-app/commit/754e14946c0fa397558e5983a39c082c9072fe5c))
* message_permission_required ([894d26d](https://github.com/alerte-secours/as-app/commit/894d26dad12c4736cde827232764ed5c8d77df4e))
* **notification:** android background-geolocation-lost ([aea3a26](https://github.com/alerte-secours/as-app/commit/aea3a2609639f01b1d7aa414c67e3bdb99a1a47c))
* title_permission_required ([48b6637](https://github.com/alerte-secours/as-app/commit/48b663799d4bb4ef69671f0291a77a65418e2544))
* wip ([b635f29](https://github.com/alerte-secours/as-app/commit/b635f29f45928210bc9a9a8c5d201d91a6b48a07))
## [1.10.9](https://github.com/alerte-secours/as-app/compare/v1.10.7...v1.10.9) (2025-07-06)
### Bug Fixes
* bg location force sync all 12 hours ([732dc3d](https://github.com/alerte-secours/as-app/commit/732dc3df7b6ab635c70d02d657d066e6c72a49c4))
* **ios:** upgrade bundling ([23ecee5](https://github.com/alerte-secours/as-app/commit/23ecee5061e5e7324407223e7c1ce9a815f96521))
## [1.10.8](https://github.com/alerte-secours/as-app/compare/v1.10.7...v1.10.8) (2025-07-04)
### Bug Fixes
* bg location force sync all 12 hours ([732dc3d](https://github.com/alerte-secours/as-app/commit/732dc3df7b6ab635c70d02d657d066e6c72a49c4))
## [1.10.7](https://github.com/alerte-secours/as-app/compare/v1.10.1...v1.10.7) (2025-07-04)
### Bug Fixes
* **android:** foreground service ([0ac2851](https://github.com/alerte-secours/as-app/commit/0ac28515dff752af30e45ff2f88d52da46c591aa))
* back to stateless refresh (sync endpoint) ([6af5875](https://github.com/alerte-secours/as-app/commit/6af58755c1b3e8c7be0d117d6fc07278406a0459))
* **battery-opti-disable:** integrate permissions view ([d9b5d10](https://github.com/alerte-secours/as-app/commit/d9b5d10684446a01873810382e501e45cd19da1b))
* **battery-opti-disable:** wip ([7082161](https://github.com/alerte-secours/as-app/commit/7082161b7f004b1052adbce2cf3ac9f74b2eee03))
* bettery optimization glitch ([b4b7441](https://github.com/alerte-secours/as-app/commit/b4b7441bacc3728d684a6e2219c84b91c75461af))
* bg location lost notif ([2fa7b48](https://github.com/alerte-secours/as-app/commit/2fa7b4839aedec61a5fdf2e5292440b3b0bb9c6b))
* don't handle refresh in headless mode anymore ([b10ff5a](https://github.com/alerte-secours/as-app/commit/b10ff5a6e735df6323d70a3c5b0d651d1cbd01cc))
* **headless-task:** wip ([c947d49](https://github.com/alerte-secours/as-app/commit/c947d4915ab19377cd2263a86bc9f902c41defdc))
* **headless-task:** wip ([6c290f2](https://github.com/alerte-secours/as-app/commit/6c290f21b4d57513aa90dabcc68e9a7b074a6430))
* **headless:** async-storage in memory first ([9f6452d](https://github.com/alerte-secours/as-app/commit/9f6452d5e368c82e0f93afd06c354756dd7913db))
* **headless:** secure store in memory first ([4280820](https://github.com/alerte-secours/as-app/commit/4280820e0169c4f9e36793d3f0ad460e9bde558f))
* **headless:** use axios instead of apollo for auth ([6444801](https://github.com/alerte-secours/as-app/commit/644480182d8da2ca1197c919262d5a5df0817d25))
* **headless:** use fetch instead of axios for auth ([09ea8cd](https://github.com/alerte-secours/as-app/commit/09ea8cd5634f46c1daeeb24ad4230ee508bd8293))
* import typo generateAlertEmergencyInfoContent ([d780fb4](https://github.com/alerte-secours/as-app/commit/d780fb4190acc24ceb9811682f8684ffb232a716))
* improve error handling ([010aa2c](https://github.com/alerte-secours/as-app/commit/010aa2c2fc0cf80c6afd3eb4fe954f94c94b8bea))
* **ios:** up ios version for rn compat ([8b1c529](https://github.com/alerte-secours/as-app/commit/8b1c5291d4ce0b042d745f76fc6dc62cf8bd6ca4))
* known keys ([be0cd62](https://github.com/alerte-secours/as-app/commit/be0cd62cb943fb4d74243f78df2fb1fc29e97e76))
* memoryAsyncStorage ([6e290bd](https://github.com/alerte-secours/as-app/commit/6e290bdb6997dd06ee8d674087368ad68b97a881))
* prevent race condition ([6a77336](https://github.com/alerte-secours/as-app/commit/6a773367d49ef66a8a896ba18e63c660f30eb143))
* **profile:** button enregistrer should be greysed when stored or no change ([9cfb40e](https://github.com/alerte-secours/as-app/commit/9cfb40e510584567400a6ea6f4871b345d723ac6))
* re-enable expo-updates ([16332cb](https://github.com/alerte-secours/as-app/commit/16332cbb764be5f067aa7cb3c2713399d1959f7c))
* re-up ([8331029](https://github.com/alerte-secours/as-app/commit/8331029e91220cc9c679dce1058ffc5f2cbb0577))
* reduce tracesSampleRate ([b5ae235](https://github.com/alerte-secours/as-app/commit/b5ae235ba4f093451bb7bd41b19aa77a8a0bda96))
* remove debuggin ([b70d6ed](https://github.com/alerte-secours/as-app/commit/b70d6ed9a070e9c01fa0d5206db5cc1a115e074e))
* sentry + re-enable expo-updates + disable debug ([8e8e120](https://github.com/alerte-secours/as-app/commit/8e8e120391f197e7826edaef47817ff8fb14a6b8))
* sentry tracing ([e6924ac](https://github.com/alerte-secours/as-app/commit/e6924ac9ff22b114eff4293f007becebe44e286d))
* sentry tracing ([4a0f3ab](https://github.com/alerte-secours/as-app/commit/4a0f3ab7effd79f75efd976f5fd9e1d1531e5b19))
* sentry tracing ([8ba4056](https://github.com/alerte-secours/as-app/commit/8ba4056187e6bd9ebf8ceec881197d6dc0aaaebc))
* theming ([4e2ea42](https://github.com/alerte-secours/as-app/commit/4e2ea4219501c16f0e74fcd2a5d48dca9845b3ee))
* undefined error ([47f11d1](https://github.com/alerte-secours/as-app/commit/47f11d1b888f4372fcb03db9bd1e839495455df3))
* undefined error ([d4de0b4](https://github.com/alerte-secours/as-app/commit/d4de0b4541fedb29b3e5161f91fc65b3253308e0))
* up android target sdk version ([7918e74](https://github.com/alerte-secours/as-app/commit/7918e74184165509b7a76c2420c24fcf6629a5fa))
* **upgrade:** expo 52 + rn 0.76.9 ([a1ed6cf](https://github.com/alerte-secours/as-app/commit/a1ed6cfca6217ba2068908f12e78af990f91b9c5))
* **wizard hero:** battery opti red inside parameters bubble ([dccf361](https://github.com/alerte-secours/as-app/commit/dccf361dbcb23239d28ea7f860ddeae3377349c0))
* bg location force sync all 12 hours ([71b73e7](https://github.com/alerte-secours/as-app/commit/71b73e7ba4733bd46345d8f73a2a33bb8786726a))
* **ios:** up ios version for rn compat ([8b1c529](https://github.com/alerte-secours/as-app/commit/8b1c5291d4ce0b042d745f76fc6dc62cf8bd6ca4))
## [1.10.6](https://github.com/alerte-secours/as-app/compare/v1.10.5...v1.10.6) (2025-07-04)
## [1.10.5](https://github.com/alerte-secours/as-app/compare/v1.10.4...v1.10.5) (2025-07-04)
### Bug Fixes
* bg location lost notif ([2fa7b48](https://github.com/alerte-secours/as-app/commit/2fa7b4839aedec61a5fdf2e5292440b3b0bb9c6b))
* theming ([4e2ea42](https://github.com/alerte-secours/as-app/commit/4e2ea4219501c16f0e74fcd2a5d48dca9845b3ee))
## [1.10.4](https://github.com/alerte-secours/as-app/compare/v1.10.3...v1.10.4) (2025-07-03)
### Bug Fixes
* remove debuggin ([b70d6ed](https://github.com/alerte-secours/as-app/commit/b70d6ed9a070e9c01fa0d5206db5cc1a115e074e))
## [1.10.3](https://github.com/alerte-secours/as-app/compare/v1.10.2...v1.10.3) (2025-07-03) ## [1.10.3](https://github.com/alerte-secours/as-app/compare/v1.10.2...v1.10.3) (2025-07-03)

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 201 versionCode 183
versionName "1.11.11" versionName "1.10.3"
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

@ -3,6 +3,9 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CALL_PHONE"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
@ -44,6 +47,8 @@
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&amp;channel=release"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&amp;channel=release"/>
<service android:name="com.transistorsoft.locationmanager.service.LocationRequestService" android:foregroundServiceType="location|dataSync" android:enabled="true" android:exported="false" tools:replace="android:foregroundServiceType"/>
<service android:name="com.transistorsoft.backgroundfetch.BackgroundFetchService" android:foregroundServiceType="dataSync" android:enabled="true" android:exported="false"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
@ -64,10 +69,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

@ -131,13 +131,12 @@ let config = {
"tel", "tel",
"telprompt", "telprompt",
], ],
BGTaskSchedulerPermittedIdentifiers: [
"com.transistorsoft",
"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

283
index.js
View file

@ -5,9 +5,6 @@ import "./warnFilter";
import "expo-splash-screen"; import "expo-splash-screen";
import BackgroundGeolocation from "react-native-background-geolocation"; import BackgroundGeolocation from "react-native-background-geolocation";
import { Platform } from "react-native";
import BackgroundFetch from "react-native-background-fetch";
import notifee from "@notifee/react-native"; import notifee from "@notifee/react-native";
import messaging from "@react-native-firebase/messaging"; import messaging from "@react-native-firebase/messaging";
@ -21,7 +18,9 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
import onMessageReceived from "~/notifications/onMessageReceived"; import onMessageReceived from "~/notifications/onMessageReceived";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { executeHeartbeatSync } from "~/location/backgroundTask"; import * as Sentry from "@sentry/react-native";
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,82 +31,240 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
// the environment is set up appropriately // the environment is set up appropriately
registerRootComponent(App); registerRootComponent(App);
// Constants for persistence
// const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
// const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING
const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
// Helper functions for persisting sync time
const getLastSyncTime = async () => {
try {
const value = await AsyncStorage.getItem(
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
);
return value ? parseInt(value, 10) : Date.now();
} catch (error) {
Sentry.captureException(error, {
tags: { module: "headless-task", operation: "get-last-sync-time" },
});
return Date.now();
}
};
const setLastSyncTime = async (time) => {
try {
await AsyncStorage.setItem(
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
time.toString(),
);
} catch (error) {
Sentry.captureException(error, {
tags: { module: "headless-task", operation: "set-last-sync-time" },
});
}
};
// this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
const getCurrentPosition = () => {
return new Promise((resolve) => {
// Add timeout protection
const timeout = setTimeout(() => {
resolve({ code: -1, message: "getCurrentPosition timeout" });
}, 15000); // 15 second timeout
BackgroundGeolocation.getCurrentPosition(
{
samples: 1,
persist: true,
extras: { background: true },
timeout: 10, // 10 second timeout in the plugin itself
},
(location) => {
clearTimeout(timeout);
resolve(location);
},
(error) => {
clearTimeout(timeout);
resolve(error);
},
);
});
};
const geolocBgLogger = createLogger({ const geolocBgLogger = createLogger({
service: "background-geolocation", service: "background-geolocation",
task: "headless", task: "headless",
}); });
const HeadlessTask = async (event) => { const HeadlessTask = async (event) => {
// Add timeout protection for the entire headless task
const taskTimeout = setTimeout(() => {
geolocBgLogger.error("HeadlessTask timeout", { event });
Sentry.captureException(new Error("HeadlessTask timeout"), {
tags: {
module: "background-geolocation",
operation: "headless-task-timeout",
eventName: event?.name,
},
});
}, 60000); // 60 second timeout
// Simple performance tracking without deprecated APIs
const taskStartTime = Date.now();
try { try {
switch (event?.name) { // Validate event structure
if (!event || typeof event !== "object") {
throw new Error("Invalid event object received");
}
const { name, params } = event;
if (!name || typeof name !== "string") {
throw new Error("Invalid event name received");
}
geolocBgLogger.info("HeadlessTask event received", { name, params });
switch (name) {
case "heartbeat": case "heartbeat":
await executeHeartbeatSync(); // Get persisted last sync time
const lastSyncTime = await getLastSyncTime();
const now = Date.now();
const timeSinceLastSync = now - lastSyncTime;
// Get current position with performance tracking
const location = await getCurrentPosition();
geolocBgLogger.debug("getCurrentPosition result", { location });
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
geolocBgLogger.info("Forcing location sync after 24h");
try {
// Change pace to ensure location updates with timeout
await Promise.race([
BackgroundGeolocation.changePace(true),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("changePace timeout")),
10000,
),
),
]);
// Perform sync with timeout
const syncResult = await Promise.race([
BackgroundGeolocation.sync(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("sync timeout")), 20000),
),
]);
// Update last sync time after successful sync
await setLastSyncTime(now);
} catch (syncError) {
Sentry.captureException(syncError, {
tags: {
module: "headless-task",
operation: "force-sync",
eventName: name,
},
contexts: {
syncAttempt: {
timeSinceLastSync: timeSinceLastSync,
lastSyncTime: new Date(lastSyncTime).toISOString(),
},
},
});
geolocBgLogger.error("Force sync failed", { error: syncError });
}
}
break; break;
case "location":
// Validate location parameters
if (!params || typeof params !== "object") {
geolocBgLogger.warn("Invalid location params", { params });
break;
}
geolocBgLogger.debug("Location update received", {
location: params.location,
});
break;
case "http":
// Validate HTTP parameters
if (!params || typeof params !== "object" || !params.response) {
geolocBgLogger.warn("Invalid HTTP params", { params });
break;
}
const httpStatus = params.response?.status;
const isHttpSuccess = httpStatus === 200;
geolocBgLogger.debug("HTTP response received", {
response: params.response,
});
// Update last sync time on successful HTTP response
if (isHttpSuccess) {
try {
const now = Date.now();
await setLastSyncTime(now);
} catch (syncTimeError) {
geolocBgLogger.error("Failed to update sync time", {
error: syncTimeError,
});
Sentry.captureException(syncTimeError, {
tags: {
module: "headless-task",
operation: "update-sync-time-http",
},
});
}
}
break;
default: default:
break; break;
} }
// Task completed successfully
const taskDuration = Date.now() - taskStartTime;
} catch (error) { } catch (error) {
const taskDuration = Date.now() - taskStartTime;
// Capture any unexpected errors
Sentry.captureException(error, {
tags: {
module: "headless-task",
eventName: event?.name || "unknown",
},
extra: {
duration: taskDuration,
},
});
geolocBgLogger.error("HeadlessTask error", { geolocBgLogger.error("HeadlessTask error", {
error, error,
event, event,
duration: taskDuration,
});
} finally {
// Clear the timeout
clearTimeout(taskTimeout);
const finalDuration = Date.now() - taskStartTime;
geolocBgLogger.debug("HeadlessTask completed", {
event: event?.name,
duration: finalDuration,
}); });
} }
}; };
if (Platform.OS === "android") { BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
} else if (Platform.OS === "ios") {
BackgroundGeolocation.onLocation(async (_location) => {
await executeHeartbeatSync();
});
// Configure BackgroundFetch for iOS (iOS-specific configuration)
BackgroundFetch.configure(
{
minimumFetchInterval: 15, // Only valid option for iOS - gives best chance of execution
},
// Event callback
async (taskId) => {
let syncResult = null;
try {
// Execute the shared heartbeat logic and get result
syncResult = await executeHeartbeatSync();
} catch (error) {
// silent error
} finally {
// CRITICAL: Always call finish with appropriate result
try {
if (taskId) {
let fetchResult;
if (syncResult?.error || !syncResult?.syncSuccessful) {
// Task failed
fetchResult = BackgroundFetch.FETCH_RESULT_FAILED;
} else if (
syncResult?.syncPerformed &&
syncResult?.syncSuccessful
) {
// Force sync was performed successfully - new data
fetchResult = BackgroundFetch.FETCH_RESULT_NEW_DATA;
} else {
// No sync was needed - no new data
fetchResult = BackgroundFetch.FETCH_RESULT_NO_DATA;
}
BackgroundFetch.finish(taskId, fetchResult);
}
} catch (finishError) {
// silent error
}
}
},
// Timeout callback (REQUIRED by BackgroundFetch API)
async (taskId) => {
// CRITICAL: Must call finish on timeout with FAILED result
BackgroundFetch.finish(taskId, BackgroundFetch.FETCH_RESULT_FAILED);
},
).catch(() => {
// silent error
});
}

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

@ -44,7 +44,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "1" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"

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,12 +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</string>
<string>com.transistorsoft.fetch</string>
<string>com.transistorsoft.customtask</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@ -25,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.11</string> <string>1.10.3</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -48,7 +42,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>201</string> <string>183</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
@ -95,14 +89,23 @@
<dict> <dict>
<key>alertesecours.fr</key> <key>alertesecours.fr</key>
<dict> <dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/> <false/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSExceptionMinimumTLSVersion</key> <key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string> <string>TLSv1.0</string>
<key>NSExceptionRequiresForwardSecrecy</key> </dict>
<false/> <key>sentry.io</key>
<dict>
<key>NSIncludesSubdomains</key> <key>NSIncludesSubdomains</key>
<true/> <true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict> </dict>
<key>localhost</key> <key>localhost</key>
<dict> <dict>
@ -111,15 +114,6 @@
<key>NSIncludesSubdomains</key> <key>NSIncludesSubdomains</key>
<true/> <true/>
</dict> </dict>
<key>sentry.io</key>
<dict>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
@ -151,7 +145,6 @@
<string>fetch</string> <string>fetch</string>
<string>location</string> <string>location</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>processing</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>SplashScreen</string> <string>SplashScreen</string>

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

@ -1,22 +0,0 @@
//
// RNBackgroundGeolocation+AppDelegate.m
// RNBackgroundGeolocationSample
//
// Created by Christopher Scott on 2016-08-01.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "AppDelegate.h"
#import <TSBackgroundFetch/TSBackgroundFetch.h>
@implementation AppDelegate(AppDelegate)
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"RNBackgroundFetch AppDelegate received fetch event");
TSBackgroundFetch *fetchManager = [TSBackgroundFetch sharedInstance];
[fetchManager performFetchWithCompletionHandler:completionHandler applicationState:application.applicationState];
}
@end

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.11", "version": "1.10.3",
"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",
@ -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": 201, "versionCode": 183,
"buildNumber": 201 "buildNumber": 183
}, },
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {

View file

@ -6,7 +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 { secureStore } from "~/storage/memorySecureStore";
import memoryAsyncStorage from "~/storage/memoryAsyncStorage"; import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
@ -62,7 +62,6 @@ const initializeStores = () => {
// Then initialize other stores sequentially // 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

@ -690,7 +690,7 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
color: colors.primary, color: colors.primary,
}, },
batteryOptimizationAlert: { batteryOptimizationAlert: {
backgroundColor: colors.surfaceVariant, backgroundColor: colors.errorContainer || colors.surfaceVariant,
padding: 15, padding: 15,
borderRadius: 6, borderRadius: 6,
marginBottom: 15, marginBottom: 15,

View file

@ -1,93 +0,0 @@
import BackgroundGeolocation from "react-native-background-geolocation";
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import { createLogger } from "~/lib/logger";
// Constants for persistence
const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
// const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
const geolocBgLogger = createLogger({
service: "background-task",
task: "headless",
});
// Helper functions for persisting sync time
const getLastSyncTime = async () => {
try {
const value = await memoryAsyncStorage.getItem(
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
);
return value ? parseInt(value, 10) : Date.now();
} catch (error) {
return 0;
}
};
const setLastSyncTime = async (time) => {
try {
await memoryAsyncStorage.setItem(
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
time.toString(),
);
} catch (error) {
// silent error
}
};
// Shared heartbeat logic - mutualized between Android and iOS
const executeSync = async () => {
let syncPerformed = false;
let syncSuccessful = false;
try {
syncPerformed = true;
try {
// Change pace to ensure location updates
await BackgroundGeolocation.changePace(true);
// Perform sync
await BackgroundGeolocation.sync();
syncSuccessful = true;
} catch (syncError) {
syncSuccessful = false;
}
// Return result information for BackgroundFetch
return {
syncPerformed,
syncSuccessful,
};
} catch (error) {
// Return error result for BackgroundFetch
return {
syncPerformed,
syncSuccessful: false,
error: error.message,
};
}
};
export const executeHeartbeatSync = async () => {
const lastSyncTime = await getLastSyncTime();
const now = Date.now();
const timeSinceLastSync = now - lastSyncTime;
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
geolocBgLogger.info("Forcing location sync");
try {
await Promise.race([
async () => {
await executeSync();
},
new Promise((_, reject) =>
setTimeout(() => reject(new Error("changePace timeout")), 10000),
),
]);
await setLastSyncTime(now);
} catch (syncError) {
geolocBgLogger.error("Force sync failed", { error: syncError });
}
}
};

View file

@ -132,7 +132,7 @@ export default async function trackLocation() {
} }
} }
BackgroundGeolocation.onLocation(async (location) => { BackgroundGeolocation.onLocation((location) => {
locationLogger.debug("Location update received", { locationLogger.debug("Location update received", {
coords: location.coords, coords: location.coords,
timestamp: location.timestamp, timestamp: location.timestamp,

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

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

@ -0,0 +1,364 @@
import notifee from "@notifee/react-native";
import BackgroundFetch from "react-native-background-fetch";
import * as Sentry from "@sentry/react-native";
import useMount from "~/hooks/useMount";
import { createLogger } from "~/lib/logger";
const logger = createLogger({
service: "notifications",
task: "auto-cancel-expired",
});
// Background task to cancel expired notifications
const backgroundTask = async () => {
await Sentry.startSpan(
{
name: "auto-cancel-expired-notifications",
op: "background-task",
},
async (span) => {
try {
logger.info("Starting auto-cancel expired notifications task");
Sentry.addBreadcrumb({
message: "Auto-cancel task started",
category: "notifications",
level: "info",
});
// Get displayed notifications with timeout protection
let notifications;
await Sentry.startSpan(
{
op: "get-displayed-notifications",
description: "Getting displayed notifications",
},
async (getNotificationsSpan) => {
try {
// Add timeout protection for the API call
notifications = await Promise.race([
notifee.getDisplayedNotifications(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Timeout getting notifications")),
10000,
),
),
]);
getNotificationsSpan.setStatus("ok");
} catch (error) {
getNotificationsSpan.setStatus("internal_error");
throw error;
}
},
);
if (!Array.isArray(notifications)) {
logger.warn("No notifications array received", { notifications });
Sentry.addBreadcrumb({
message: "No notifications array received",
category: "notifications",
level: "warning",
});
return;
}
const currentTime = Math.round(new Date() / 1000);
let cancelledCount = 0;
let errorCount = 0;
logger.info("Processing notifications", {
totalNotifications: notifications.length,
currentTime,
});
Sentry.addBreadcrumb({
message: "Processing notifications",
category: "notifications",
level: "info",
data: {
totalNotifications: notifications.length,
currentTime,
},
});
// Process notifications with individual error handling
for (const notification of notifications) {
try {
if (!notification || !notification.id) {
logger.warn("Invalid notification object", { notification });
continue;
}
const expires = notification.data?.expires;
if (!expires) {
continue; // Skip notifications without expiry
}
if (typeof expires !== "number" || expires < currentTime) {
logger.debug("Cancelling expired notification", {
notificationId: notification.id,
expires,
currentTime,
expired: expires < currentTime,
});
// Cancel notification with timeout protection
await Promise.race([
notifee.cancelNotification(notification.id),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Timeout cancelling notification")),
5000,
),
),
]);
cancelledCount++;
Sentry.addBreadcrumb({
message: "Notification cancelled",
category: "notifications",
level: "info",
data: {
notificationId: notification.id,
expires,
},
});
}
} catch (notificationError) {
errorCount++;
logger.error("Failed to process notification", {
error: notificationError,
notificationId: notification?.id,
});
Sentry.captureException(notificationError, {
tags: {
module: "auto-cancel-expired",
operation: "cancel-notification",
},
contexts: {
notification: {
id: notification?.id,
expires: notification?.data?.expires,
},
},
});
}
}
logger.info("Auto-cancel task completed", {
totalNotifications: notifications.length,
cancelledCount,
errorCount,
});
Sentry.addBreadcrumb({
message: "Auto-cancel task completed",
category: "notifications",
level: "info",
data: {
totalNotifications: notifications.length,
cancelledCount,
errorCount,
},
});
span.setStatus("ok");
} catch (error) {
logger.error("Auto-cancel task failed", { error });
Sentry.captureException(error, {
tags: {
module: "auto-cancel-expired",
operation: "background-task",
},
});
span.setStatus("internal_error");
throw error; // Re-throw to be handled by caller
}
},
);
};
export const useAutoCancelExpired = () => {
useMount(() => {
// Initialize background fetch
BackgroundFetch.configure(
{
minimumFetchInterval: 180, // Fetch interval in minutes
stopOnTerminate: false,
startOnBoot: true,
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_NONE,
enableHeadless: true,
},
async (taskId) => {
logger.info("BackgroundFetch task started", { taskId });
try {
await backgroundTask();
logger.info("BackgroundFetch task completed successfully", {
taskId,
});
} catch (error) {
logger.error("BackgroundFetch task failed", { taskId, error });
Sentry.captureException(error, {
tags: {
module: "auto-cancel-expired",
operation: "background-fetch-task",
taskId,
},
});
} finally {
// CRITICAL: Always call finish, even on error
try {
if (taskId) {
BackgroundFetch.finish(taskId);
logger.debug("BackgroundFetch task finished", { taskId });
} else {
logger.error("Cannot finish BackgroundFetch task - no taskId");
}
} catch (finishError) {
// This is a critical error - the native side might be in a bad state
logger.error("CRITICAL: BackgroundFetch.finish() failed", {
taskId,
error: finishError,
});
Sentry.captureException(finishError, {
tags: {
module: "auto-cancel-expired",
operation: "background-fetch-finish",
critical: true,
},
contexts: {
task: { taskId },
},
});
}
}
},
(error) => {
logger.error("BackgroundFetch failed to start", { error });
Sentry.captureException(error, {
tags: {
module: "auto-cancel-expired",
operation: "background-fetch-configure",
},
});
},
);
return () => {
BackgroundFetch.stop();
};
});
};
// Register headless task
BackgroundFetch.registerHeadlessTask(async (event) => {
const taskId = event?.taskId;
logger.info("Headless task started", { taskId, event });
// Add timeout protection for the entire headless task
const taskTimeout = setTimeout(() => {
logger.error("Headless task timeout", { taskId });
Sentry.captureException(new Error("Headless task timeout"), {
tags: {
module: "auto-cancel-expired",
operation: "headless-task-timeout",
taskId,
},
});
// Force finish the task to prevent native side hanging
try {
if (taskId) {
BackgroundFetch.finish(taskId);
logger.debug("Headless task force-finished due to timeout", { taskId });
}
} catch (finishError) {
logger.error("CRITICAL: Failed to force-finish timed out headless task", {
taskId,
error: finishError,
});
Sentry.captureException(finishError, {
tags: {
module: "auto-cancel-expired",
operation: "headless-task-timeout-finish",
critical: true,
},
contexts: {
task: { taskId },
},
});
}
}, 30000); // 30 second timeout
try {
if (!taskId) {
throw new Error("No taskId provided in headless task event");
}
await backgroundTask();
logger.info("Headless task completed successfully", { taskId });
} catch (error) {
logger.error("Headless task failed", { taskId, error });
Sentry.captureException(error, {
tags: {
module: "auto-cancel-expired",
operation: "headless-task",
taskId,
},
contexts: {
event: {
taskId,
eventData: JSON.stringify(event),
},
},
});
} finally {
// Clear the timeout
clearTimeout(taskTimeout);
// CRITICAL: Always call finish, even on error
try {
if (taskId) {
BackgroundFetch.finish(taskId);
logger.debug("Headless task finished", { taskId });
} else {
logger.error("Cannot finish headless task - no taskId", { event });
}
} catch (finishError) {
// This is a critical error - the native side might be in a bad state
logger.error(
"CRITICAL: BackgroundFetch.finish() failed in headless task",
{
taskId,
error: finishError,
event,
},
);
Sentry.captureException(finishError, {
tags: {
module: "auto-cancel-expired",
operation: "headless-task-finish",
critical: true,
},
contexts: {
task: { taskId },
event: { eventData: JSON.stringify(event) },
},
});
}
}
});

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,38 +0,0 @@
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import { executeHeartbeatSync } from "~/location/backgroundTask";
const heartbeatLogger = createLogger({
module: BACKGROUND_SCOPES.NOTIFICATIONS,
feature: "geolocation-heartbeat-sync",
});
export default async function notifGeolocationHeartbeatSync(data) {
try {
heartbeatLogger.info(
"Received iOS geolocation heartbeat sync notification",
{
data,
},
);
// This is a silent notification - no visible notification is displayed
// Instead, we trigger the geolocation heartbeat sync directly
heartbeatLogger.info("Triggering geolocation heartbeat sync");
// Execute the heartbeat sync to force location update
await executeHeartbeatSync();
heartbeatLogger.info("Geolocation heartbeat sync completed successfully");
} catch (error) {
heartbeatLogger.error("Failed to execute geolocation heartbeat sync", {
error: error.message,
stack: error.stack,
data,
});
// Don't throw the error - this is a background operation
// and we don't want to crash the notification handler
}
}

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,8 +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";
import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync";
const displayLogger = createLogger({ const displayLogger = createLogger({
module: BACKGROUND_SCOPES.NOTIFICATIONS, module: BACKGROUND_SCOPES.NOTIFICATIONS,
@ -22,8 +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,
"geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
}; };
export default async function displayNotificationHandler(data) { export default async function displayNotificationHandler(data) {

View file

@ -15,6 +15,7 @@ import {
import useMount from "~/hooks/useMount"; import useMount from "~/hooks/useMount";
import setActionCategories from "./setActionCategories"; import setActionCategories from "./setActionCategories";
import onMessageReceived from "./onMessageReceived"; import onMessageReceived from "./onMessageReceived";
import { useAutoCancelExpired } from "./autoCancelExpired";
import { requestFcmPermission, setupFcm } from "./firebase"; import { requestFcmPermission, setupFcm } from "./firebase";
import { import {
requestNotifeePermission, requestNotifeePermission,
@ -203,4 +204,6 @@ export function useFcm() {
notifLogger.debug("Badge count reset"); notifLogger.debug("Badge count reset");
}); });
}); });
useAutoCancelExpired();
} }

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

@ -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: 0.1,
// 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

@ -80,5 +80,4 @@ export const STORAGE_KEYS = {
LAST_KNOWN_LOCATION: registerAsyncStorageKey("@last_known_location"), LAST_KNOWN_LOCATION: registerAsyncStorageKey("@last_known_location"),
EULA_ACCEPTED_SIMPLE: registerAsyncStorageKey("eula_accepted"), EULA_ACCEPTED_SIMPLE: registerAsyncStorageKey("eula_accepted"),
EMULATOR_MODE_ENABLED: registerAsyncStorageKey("emulator_mode_enabled"), EMULATOR_MODE_ENABLED: registerAsyncStorageKey("emulator_mode_enabled"),
SENTRY_ENABLED: registerAsyncStorageKey("@sentry_enabled"),
}; };

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

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