Compare commits

...

8 commits

37 changed files with 4632 additions and 1975 deletions

View file

@ -1,17 +0,0 @@
diff --git a/android/src/main/cpp/JavaScriptModuleObject.cpp b/android/src/main/cpp/JavaScriptModuleObject.cpp
index 08c21538ddb638a2b98601bedf5bd00de2ae7c20..5b1bb31151962d8dd377525c6d765c9327d0d374 100644
--- a/android/src/main/cpp/JavaScriptModuleObject.cpp
+++ b/android/src/main/cpp/JavaScriptModuleObject.cpp
@@ -145,7 +145,11 @@ void JavaScriptModuleObject::decorate(jsi::Runtime &runtime, jsi::Object *module
for (auto &[name, classInfo]: classes) {
auto &[classRef, constructor, ownerClass] = classInfo;
auto classObject = classRef->cthis();
- auto weakConstructor = std::weak_ptr(constructor);
+
+ // https://github.com/expo/expo/discussions/29610#discussioncomment-9762642
+ // https://github.com/expo/expo/pull/29075/files
+ auto weakConstructor = std::weak_ptr<decltype(constructor)::element_type>(constructor);
+
auto klass = SharedObject::createClass(
runtime,
name.c_str(),

View file

@ -1,3 +1,11 @@
source "https://rubygems.org" source "https://rubygems.org"
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '<= 1.3.4'
gem "fastlane" gem "fastlane"

View file

@ -67,11 +67,11 @@ def jscFlavor = 'org.webkit:android-jsc:+'
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle") apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")
android { android {
// @generated begin react-native-background-geolocation-project - expo prebuild (DO NOT MODIFY) sync-451bbca0f2f08c9fc8b21a201ef5b476b165ba21 // @generated begin react-native-background-geolocation-project - expo prebuild (DO NOT MODIFY) sync-451bbca0f2f08c9fc8b21a201ef5b476b165ba21
Project background_geolocation = project(':react-native-background-geolocation') // Project background_geolocation = project(':react-native-background-geolocation')
apply from: "${background_geolocation.projectDir}/app.gradle" // apply from: "${background_geolocation.projectDir}/app.gradle"
// @generated end react-native-background-geolocation-project // @generated end react-native-background-geolocation-project
// @generated begin react-native-background-fetch-project - expo prebuild (DO NOT MODIFY) sync-56d2d70cbc3f26369dd5e711d0ab87bf3c0aebb3 // @generated begin react-native-background-fetch-project - expo prebuild (DO NOT MODIFY) sync-56d2d70cbc3f26369dd5e711d0ab87bf3c0aebb3
Project background_fetch = project(':react-native-background-fetch') // Project background_fetch = project(':react-native-background-fetch')
// @generated end react-native-background-fetch-project // @generated end react-native-background-fetch-project
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
@ -115,10 +115,10 @@ Project background_fetch = project(':react-native-background-fetch')
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
// @generated begin react-native-background-geolocation-proguard - expo prebuild (DO NOT MODIFY) sync-606db7f838db1722ea0ff547adceb798272bd706 // @generated begin react-native-background-geolocation-proguard - expo prebuild (DO NOT MODIFY) sync-606db7f838db1722ea0ff547adceb798272bd706
proguardFiles "${background_geolocation.projectDir}/proguard-rules.pro" // proguardFiles "${background_geolocation.projectDir}/proguard-rules.pro"
// @generated end react-native-background-geolocation-proguard // @generated end react-native-background-geolocation-proguard
// @generated begin react-native-background-fetch-proguard - expo prebuild (DO NOT MODIFY) sync-7cb5d9a88ae03463dcde5b7e8571a725ecd5854f // @generated begin react-native-background-fetch-proguard - expo prebuild (DO NOT MODIFY) sync-7cb5d9a88ae03463dcde5b7e8571a725ecd5854f
proguardFiles "${background_fetch.projectDir}/proguard-rules.pro" // proguardFiles "${background_fetch.projectDir}/proguard-rules.pro"
// @generated end react-native-background-fetch-proguard // @generated end react-native-background-fetch-proguard
crunchPngs false crunchPngs false
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro" proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"

View file

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<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"/>
@ -29,7 +30,7 @@
<data android:scheme="waze"/> <data android:scheme="waze"/>
</intent> </intent>
</queries> </queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:theme="@style/AppTheme" android:networkSecurityConfig="@xml/network_security_config"> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/> <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="com.transistorsoft.locationmanager.hms.license" android:value="ba479a3c61fbe471c826a39d1ee8b1a088df6d2249fad51f6ab5f24346f6bf87"/> <meta-data android:name="com.transistorsoft.locationmanager.hms.license" android:value="ba479a3c61fbe471c826a39d1ee8b1a088df6d2249fad51f6ab5f24346f6bf87"/>
@ -38,7 +39,7 @@
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/> <meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="-----BEGIN CERTIFICATE-----&#xD;&#xA;MIICzTCCAbWgAwIBAgIJR0KfFDoMJrYZMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV&#xD;&#xA;BAMTBHRlc3QwIBcNMjQwODE4MTU0OTExWhgPMjEyNDA4MTgxNTQ5MTFaMA8xDTAL&#xD;&#xA;BgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk46qR&#xD;&#xA;a0Do2fpBDtif18a/WQNWHm/xseHsh97bZdt8ooV4PQooK6VZUbADUhhJXqqomapa&#xD;&#xA;yFMJX7sZzfBUF7/xMrWDrgS0R4FLbXijAolhpXoqMkBCx3toKUCbU4ljA+Lz/BX1&#xD;&#xA;AEqVWqAweNzNDi4bvd1PG1/sQuuEtoZuSVfTPRAjF8vVkWyn8nfkorTtMYaw2QFu&#xD;&#xA;ugs1wp7YieD4C8CIK5gMX7f8bxx3l7BR50bf+9MHJFI+eTjmoFoJFEVWbCcrOrky&#xD;&#xA;FLM0+NrMI2fZYunrN6jcKc/NKEaDKb1VDO9yrLcFQOtXJJIXz94/lS6kHDjzEgUV&#xD;&#xA;zx3uaDbAdSQsyZxxAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E&#xD;&#xA;DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEASIjMvS3N9NEPeakUmYXg&#xD;&#xA;MEyjaX+N/62Pbcp4taU4G9vDB/fyDqMMef8+CWBpo/noXqzt4K6k1id7UwdZhRks&#xD;&#xA;xdBTSf1x5yzDB24mbqNAvPa2q8G6KIoNZuvLUDz35366FxR+vTHQmp2d4Yz92kIL&#xD;&#xA;EEmFr8eMHf60tfHG+em97p+evmXDyBjF3CwOvtuzog1wCF/AsJ1d0gbPPMKdAHKC&#xD;&#xA;LZHsiXJ5i/oFuYzWkDDJkO9bb6HaQplt/46iC0CyM6SsT6H8kkDVQbfQCH1JAXsL&#xD;&#xA;Knk10FbAMKJ7GWbAdsdcbNZlDMrzPprw8N/fpGc7RHdHBwKcFm44mNtrMrzEd4eX&#xD;&#xA;pQ==&#xD;&#xA;-----END CERTIFICATE-----&#xD;&#xA;"/> <meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="-----BEGIN CERTIFICATE-----&#xD;&#xA;MIICzTCCAbWgAwIBAgIJR0KfFDoMJrYZMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV&#xD;&#xA;BAMTBHRlc3QwIBcNMjQwODE4MTU0OTExWhgPMjEyNDA4MTgxNTQ5MTFaMA8xDTAL&#xD;&#xA;BgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk46qR&#xD;&#xA;a0Do2fpBDtif18a/WQNWHm/xseHsh97bZdt8ooV4PQooK6VZUbADUhhJXqqomapa&#xD;&#xA;yFMJX7sZzfBUF7/xMrWDrgS0R4FLbXijAolhpXoqMkBCx3toKUCbU4ljA+Lz/BX1&#xD;&#xA;AEqVWqAweNzNDi4bvd1PG1/sQuuEtoZuSVfTPRAjF8vVkWyn8nfkorTtMYaw2QFu&#xD;&#xA;ugs1wp7YieD4C8CIK5gMX7f8bxx3l7BR50bf+9MHJFI+eTjmoFoJFEVWbCcrOrky&#xD;&#xA;FLM0+NrMI2fZYunrN6jcKc/NKEaDKb1VDO9yrLcFQOtXJJIXz94/lS6kHDjzEgUV&#xD;&#xA;zx3uaDbAdSQsyZxxAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E&#xD;&#xA;DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEASIjMvS3N9NEPeakUmYXg&#xD;&#xA;MEyjaX+N/62Pbcp4taU4G9vDB/fyDqMMef8+CWBpo/noXqzt4K6k1id7UwdZhRks&#xD;&#xA;xdBTSf1x5yzDB24mbqNAvPa2q8G6KIoNZuvLUDz35366FxR+vTHQmp2d4Yz92kIL&#xD;&#xA;EEmFr8eMHf60tfHG+em97p+evmXDyBjF3CwOvtuzog1wCF/AsJ1d0gbPPMKdAHKC&#xD;&#xA;LZHsiXJ5i/oFuYzWkDDJkO9bb6HaQplt/46iC0CyM6SsT6H8kkDVQbfQCH1JAXsL&#xD;&#xA;Knk10FbAMKJ7GWbAdsdcbNZlDMrzPprw8N/fpGc7RHdHBwKcFm44mNtrMrzEd4eX&#xD;&#xA;pQ==&#xD;&#xA;-----END CERTIFICATE-----&#xD;&#xA;"/>
<meta-data android:name="expo.modules.updates.CODE_SIGNING_METADATA" android:value="{&quot;keyid&quot;:&quot;main&quot;,&quot;alg&quot;:&quot;rsa-v1_5-sha256&quot;}"/> <meta-data android:name="expo.modules.updates.CODE_SIGNING_METADATA" android:value="{&quot;keyid&quot;:&quot;main&quot;,&quot;alg&quot;:&quot;rsa-v1_5-sha256&quot;}"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/> <meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/> <meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
<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"/>

View file

@ -1,4 +1,5 @@
package com.alertesecours package com.alertesecours
import expo.modules.splashscreen.SplashScreenManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -15,7 +16,10 @@ class MainActivity : ReactActivity() {
// Set the theme to AppTheme BEFORE onCreate to support // Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar. // coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen. // This is required for expo-splash-screen.
setTheme(R.style.AppTheme); // setTheme(R.style.AppTheme);
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
SplashScreenManager.registerOnActivity(this)
// @generated end expo-splashscreen
super.onCreate(null) super.onCreate(null)
} }

View file

@ -10,6 +10,7 @@ import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ApplicationLifecycleDispatcher
@ -40,7 +41,7 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
SoLoader.init(this, false) SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() load()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -0,0 +1,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
<item>
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
</item>
</layer-list>

View file

@ -5,13 +5,16 @@
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#364fc7</item> <item name="android:statusBarColor">#364fc7</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
</style> </style>
<style name="ResetEditText" parent="@android:style/Widget.EditText"> <style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item> <item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item> <item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item> <item name="android:textColor">@android:color/black</item>
</style> </style>
<style name="Theme.App.SplashScreen" parent="AppTheme"> <style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="android:windowBackground">@drawable/splashscreen</item> <item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style> </style>
</resources> </resources>

View file

@ -7,10 +7,10 @@ buildscript {
appCompatVersion = "1.4.2" appCompatVersion = "1.4.2"
// @generated end expo-gradle-ext-vars // @generated end expo-gradle-ext-vars
// buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0' // buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34') compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23' kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
ndkVersion = "26.1.10909125" ndkVersion = "26.1.10909125"
} }
@ -20,7 +20,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.google.gms:google-services:4.4.1' classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle') classpath('com.android.tools.build:gradle:8.6.0')
classpath('com.facebook.react:react-native-gradle-plugin') classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
} }

View file

@ -22,8 +22,6 @@ org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Enable AAPT2 PNG crunching # Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true android.enablePngCrunchInReleaseBuilds=true
@ -63,4 +61,7 @@ android.extraMavenRepos=[]
android.enableProguardInReleaseBuilds=true android.enableProguardInReleaseBuilds=true
# fix notifee + expo-update crash, see https://github.com/expo/expo/issues/15298 # fix notifee + expo-update crash, see https://github.com/expo/expo/issues/15298
EX_UPDATES_ANDROID_DELAY_LOAD_APP=false EX_UPDATES_ANDROID_DELAY_LOAD_APP=false
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
expo.edgeToEdgeEnabled=false

View file

@ -10,7 +10,8 @@ let config = {
version, version,
updates: { updates: {
url: "https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&channel=release", url: "https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&channel=release",
enabled: true, // enabled: true,
enabled: false, // DEBUGGING
checkAutomatically: "ON_ERROR_RECOVERY", checkAutomatically: "ON_ERROR_RECOVERY",
fallbackToCacheTimeout: 0, fallbackToCacheTimeout: 0,
codeSigningCertificate: "./keys/certificate.pem", codeSigningCertificate: "./keys/certificate.pem",
@ -23,9 +24,10 @@ let config = {
orientation: "portrait", orientation: "portrait",
userInterfaceStyle: "automatic", userInterfaceStyle: "automatic",
splash: { splash: {
image: "./src/assets/img/splashscreen.png", image: "./src/assets/img/splash-icon.png",
backgroundColor: "#364fc7", backgroundColor: "#364fc7",
resizeMode: "contain", resizeMode: "contain",
imageWidth: 200,
}, },
// Add notification configuration at root level // Add notification configuration at root level
notification: { notification: {
@ -55,6 +57,7 @@ let config = {
}, },
], ],
permissions: [ permissions: [
"android.permission.ACCESS_BACKGROUND_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_FINE_LOCATION",
"android.permission.CALL_PHONE", "android.permission.CALL_PHONE",

18
docs/upgrade.md Normal file
View file

@ -0,0 +1,18 @@
# upgrading guide
## step
- upgrade expo sdk first
```sh
npx expo install expo@52
```
- align package with expo version
```sh
npx expo install --fix
```
## helpers
https://react-native-community.github.io/upgrade-helper/

374
index.js
View file

@ -18,6 +18,8 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
import onMessageReceived from "~/notifications/onMessageReceived"; import onMessageReceived from "~/notifications/onMessageReceived";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import * as Sentry from "@sentry/react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
// setup notification, this have to stay in index.js // setup notification, this have to stay in index.js
notifee.onBackgroundEvent(notificationBackgroundEvent); notifee.onBackgroundEvent(notificationBackgroundEvent);
@ -28,72 +30,418 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
// the environment is set up appropriately // the environment is set up appropriately
registerRootComponent(App); registerRootComponent(App);
// Constants for persistence
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
// const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const FORCE_SYNC_INTERVAL = 60 * 1000; // DEBUGGING
// Helper functions for persisting sync time
const getLastSyncTime = async () => {
try {
const value = await AsyncStorage.getItem(LAST_SYNC_TIME_KEY);
return value ? parseInt(value, 10) : Date.now();
} catch (error) {
Sentry.captureException(error, {
tags: { module: "headless-task", operation: "get-last-sync-time" },
});
return Date.now();
}
};
const setLastSyncTime = async (time) => {
try {
await AsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString());
} catch (error) {
Sentry.captureException(error, {
tags: { module: "headless-task", operation: "set-last-sync-time" },
});
}
};
// this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode // this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
const getCurrentPosition = () => { const getCurrentPosition = () => {
return new Promise((resolve) => { return new Promise((resolve) => {
// Add timeout protection
const timeout = setTimeout(() => {
resolve({ code: -1, message: "getCurrentPosition timeout" });
}, 15000); // 15 second timeout
BackgroundGeolocation.getCurrentPosition( BackgroundGeolocation.getCurrentPosition(
{ {
samples: 1, samples: 1,
persist: true, persist: true,
extras: { background: true }, extras: { background: true },
timeout: 10, // 10 second timeout in the plugin itself
}, },
(location) => { (location) => {
clearTimeout(timeout);
resolve(location); resolve(location);
}, },
(error) => { (error) => {
clearTimeout(timeout);
resolve(error); resolve(error);
}, },
); );
}); });
}; };
const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
let lastSyncTime = Date.now();
const geolocBgLogger = createLogger({ const geolocBgLogger = createLogger({
service: "background-geolocation", service: "background-geolocation",
task: "headless", task: "headless",
}); });
const HeadlessTask = async (event) => { const HeadlessTask = async (event) => {
const { name, params } = event; // Add timeout protection for the entire headless task
geolocBgLogger.info("HeadlessTask event received", { name, params }); 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
let transaction;
try { try {
// 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");
}
// Start Sentry transaction for the entire HeadlessTask
transaction = Sentry.startTransaction({
name: "headless-task",
op: "background-task",
data: { eventName: name },
});
Sentry.getCurrentScope().setSpan(transaction);
// Add initial breadcrumb
Sentry.addBreadcrumb({
message: "HeadlessTask started",
category: "headless-task",
level: "info",
data: {
eventName: name,
params: params ? JSON.stringify(params) : null,
timestamp: Date.now(),
},
});
geolocBgLogger.info("HeadlessTask event received", { name, params });
switch (name) { switch (name) {
case "heartbeat": case "heartbeat":
// Check if we need to force a sync // Add breadcrumb for heartbeat event
Sentry.addBreadcrumb({
message: "Heartbeat event received",
category: "headless-task",
level: "info",
timestamp: Date.now() / 1000,
});
// Get persisted last sync time
const lastSyncTime = await getLastSyncTime();
const now = Date.now(); const now = Date.now();
const timeSinceLastSync = now - lastSyncTime; const timeSinceLastSync = now - lastSyncTime;
// Add context about sync timing
Sentry.setContext("sync-timing", {
lastSyncTime: new Date(lastSyncTime).toISOString(),
currentTime: new Date(now).toISOString(),
timeSinceLastSync: timeSinceLastSync,
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
});
Sentry.addBreadcrumb({
message: "Sync timing calculated",
category: "headless-task",
level: "info",
data: {
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
},
});
// Get current position
const locationSpan = transaction.startChild({
op: "get-current-position",
description: "Getting current position",
});
const location = await getCurrentPosition(); const location = await getCurrentPosition();
locationSpan.finish();
const isLocationError = location && location.code !== undefined;
Sentry.addBreadcrumb({
message: "getCurrentPosition completed",
category: "headless-task",
level: isLocationError ? "warning" : "info",
data: {
success: !isLocationError,
error: isLocationError ? location : undefined,
coords: !isLocationError ? location?.coords : undefined,
},
});
geolocBgLogger.debug("getCurrentPosition result", { location }); geolocBgLogger.debug("getCurrentPosition result", { location });
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) { if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
geolocBgLogger.info("Forcing location sync after 24h"); geolocBgLogger.info("Forcing location sync after 24h");
// Update last sync time after successful sync
await BackgroundGeolocation.changePace(true); Sentry.addBreadcrumb({
await BackgroundGeolocation.sync(); message: "Force sync triggered",
lastSyncTime = now; category: "headless-task",
level: "info",
data: {
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
},
});
try {
// Get pending records count before sync with timeout
const pendingCount = await Promise.race([
BackgroundGeolocation.getCount(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("getCount timeout")), 10000),
),
]);
Sentry.addBreadcrumb({
message: "Pending records count",
category: "headless-task",
level: "info",
data: { pendingCount },
});
// Change pace to ensure location updates with timeout
const paceSpan = transaction.startChild({
op: "change-pace",
description: "Changing pace to true",
});
await Promise.race([
BackgroundGeolocation.changePace(true),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("changePace timeout")),
10000,
),
),
]);
paceSpan.finish();
Sentry.addBreadcrumb({
message: "changePace completed",
category: "headless-task",
level: "info",
});
// Perform sync with timeout
const syncSpan = transaction.startChild({
op: "sync-locations",
description: "Syncing locations",
});
const syncResult = await Promise.race([
BackgroundGeolocation.sync(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("sync timeout")), 20000),
),
]);
syncSpan.finish();
Sentry.addBreadcrumb({
message: "Sync completed successfully",
category: "headless-task",
level: "info",
data: {
syncResult: Array.isArray(syncResult)
? `${syncResult.length} records`
: "completed",
},
});
// Update last sync time after successful sync
await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "Last sync time updated",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncError) {
Sentry.captureException(syncError, {
tags: {
module: "headless-task",
operation: "force-sync",
eventName: name,
},
contexts: {
syncAttempt: {
timeSinceLastSync: timeSinceLastSync,
lastSyncTime: new Date(lastSyncTime).toISOString(),
},
},
});
geolocBgLogger.error("Force sync failed", { error: syncError });
}
} else {
Sentry.addBreadcrumb({
message: "Force sync not needed",
category: "headless-task",
level: "info",
data: {
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
nextSyncInHours: (
(FORCE_SYNC_INTERVAL - timeSinceLastSync) /
(1000 * 60 * 60)
).toFixed(2),
},
});
} }
break; break;
case "location": case "location":
// Validate location parameters
if (!params || typeof params !== "object") {
geolocBgLogger.warn("Invalid location params", { params });
break;
}
Sentry.addBreadcrumb({
message: "Location update received",
category: "headless-task",
level: "info",
data: {
coords: params.location?.coords,
activity: params.location?.activity,
hasLocation: !!params.location,
},
});
geolocBgLogger.debug("Location update received", { geolocBgLogger.debug("Location update received", {
location: params.location, location: params.location,
}); });
break; break;
case "http": case "http":
// Validate HTTP parameters
if (!params || typeof params !== "object" || !params.response) {
geolocBgLogger.warn("Invalid HTTP params", { params });
break;
}
const httpStatus = params.response?.status;
const isHttpSuccess = httpStatus === 200;
Sentry.addBreadcrumb({
message: "HTTP response received",
category: "headless-task",
level: isHttpSuccess ? "info" : "warning",
data: {
status: httpStatus,
success: params.response?.success,
hasResponse: !!params.response,
},
});
geolocBgLogger.debug("HTTP response received", { geolocBgLogger.debug("HTTP response received", {
response: params.response, response: params.response,
}); });
// Update last sync time on successful HTTP response // Update last sync time on successful HTTP response
if (params.response?.status === 200) { if (isHttpSuccess) {
lastSyncTime = Date.now(); try {
const now = Date.now();
await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "Last sync time updated (HTTP success)",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncTimeError) {
geolocBgLogger.error("Failed to update sync time", {
error: syncTimeError,
});
Sentry.captureException(syncTimeError, {
tags: {
module: "headless-task",
operation: "update-sync-time-http",
},
});
}
} }
break; break;
default:
Sentry.addBreadcrumb({
message: "Unknown event type",
category: "headless-task",
level: "warning",
data: { eventName: name },
});
}
// Finish transaction successfully
if (transaction) {
transaction.setStatus("ok");
} }
} catch (error) { } catch (error) {
geolocBgLogger.error("HeadlessTask error", { error }); // Capture any unexpected errors
Sentry.captureException(error, {
tags: {
module: "headless-task",
eventName: event?.name || "unknown",
},
});
geolocBgLogger.error("HeadlessTask error", { error, event });
// Mark transaction as failed
if (transaction) {
transaction.setStatus("internal_error");
}
} finally {
// Clear the timeout
clearTimeout(taskTimeout);
// Always finish the transaction
if (transaction) {
transaction.finish();
}
geolocBgLogger.debug("HeadlessTask completed", { event: event?.name });
} }
}; };

View file

@ -173,7 +173,18 @@
9884A4E42AD446859C6E2786 /* Fix Xcode 15 Bug */, 9884A4E42AD446859C6E2786 /* Fix Xcode 15 Bug */,
DEB86A4507014F29A1CDA958 /* Fix Xcode 15 Bug */, DEB86A4507014F29A1CDA958 /* Fix Xcode 15 Bug */,
84E188840E8D42DEA48127B6 /* Fix Xcode 15 Bug */, 84E188840E8D42DEA48127B6 /* Fix Xcode 15 Bug */,
7C1DF1A9A5D745278E4131FE /* Remove signature files (Xcode workaround) */, 4AB47738006C40F3BB7F76AC /* Fix Xcode 15 Bug */,
0CA6CB18A44941C2AD4797FA /* Fix Xcode 15 Bug */,
775A99C1D9B64D0FA36BD1D3 /* Fix Xcode 15 Bug */,
D2C79B25CD5B47AFBA354A0F /* Fix Xcode 15 Bug */,
BAE2F4DBEA194AB69D2811BB /* Fix Xcode 15 Bug */,
F2DF200966C64A768C3211A6 /* Fix Xcode 15 Bug */,
584A0F6095304B0394BBF04C /* Fix Xcode 15 Bug */,
0D8601B7DAD24244A759CFF8 /* Fix Xcode 15 Bug */,
F71413BB66E2439C85473BEA /* Fix Xcode 15 Bug */,
496EE3C8D7E445ABA85A39A6 /* Fix Xcode 15 Bug */,
F7ADCC68A8E44BA69FCA849E /* Fix Xcode 15 Bug */,
3F0C28FA929447E59D14DFED /* Remove signature files (Xcode workaround) */,
); );
buildRules = ( buildRules = (
); );
@ -644,6 +655,380 @@ fi";
shellScript = " shellScript = "
echo \"Remove signature files (Xcode workaround)\"; echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
4AB47738006C40F3BB7F76AC /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
9A209F09EF18478DB278A834 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
0CA6CB18A44941C2AD4797FA /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
E76EE8E5B27542998DF133F5 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
775A99C1D9B64D0FA36BD1D3 /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
C9A8A24DB2284DF495B6E491 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
D2C79B25CD5B47AFBA354A0F /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
1610003B8A284D1FA22A1FB8 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
BAE2F4DBEA194AB69D2811BB /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
791E84BAC7784EDEB2127581 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
F2DF200966C64A768C3211A6 /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
8FE4D53EA8D24006AE0FDFCE /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
584A0F6095304B0394BBF04C /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
F9D4E2A972FB43C98F58B6A5 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
0D8601B7DAD24244A759CFF8 /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
32852CF51E7749C69634F779 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
F71413BB66E2439C85473BEA /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
01869A2B52804DD1AB289BB3 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
496EE3C8D7E445ABA85A39A6 /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
A286AB676C1446E8AC907F77 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
F7ADCC68A8E44BA69FCA849E /* Fix Xcode 15 Bug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Fix Xcode 15 Bug";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
echo \"Remove signature files (Xcode 15 workaround)\"
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
fi";
};
3F0C28FA929447E59D14DFED /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
"; ";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */

View file

@ -3,7 +3,7 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>production</string>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:app.alertesecours.fr</string> <string>applinks:app.alertesecours.fr</string>

View file

@ -0,0 +1,20 @@
{
"colors": [
{
"color": {
"components": {
"alpha": "1.000",
"blue": "0.780392156862745",
"green": "0.309803921568627",
"red": "0.211764705882353"
},
"color-space": "srgb"
},
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

View file

@ -0,0 +1,23 @@
{
"images": [
{
"idiom": "universal",
"filename": "image.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "image@2x.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "image@3x.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "expo"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View file

@ -18,25 +18,18 @@
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" image="SplashScreenBackground" translatesAutoresizingMaskIntoConstraints="NO" id="EXPO-SplashScreenBackground" userLabel="SplashScreenBackground"> <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"/> <rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
</imageView> </imageView>
<imageView id="EXPO-SplashScreen" userLabel="SplashScreen" image="SplashScreen" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" 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>
<constraints> <constraints>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="1gX-mQ-vu6"/>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="6tX-OG-Sck"/>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="ABX-8g-7v4"/>
<constraint firstItem="EXPO-SplashScreenBackground" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="jkI-2V-eW5"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="2VS-Uz-0LU"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="LhH-Ei-DKo"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="I6l-TP-6fn"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="nbp-HC-eaG"/>
<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"/> <viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
<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"/>
@ -47,5 +40,9 @@
<resources> <resources>
<image name="SplashScreenBackground" width="1" height="1"/> <image name="SplashScreenBackground" width="1" height="1"/>
<image name="SplashScreen" width="414" height="736"/> <image name="SplashScreen" width="414" height="736"/>
<image name="SplashScreenLogo" width="414" height="736"/>
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="0.780392156862745" green="0.309803921568627" red="0.211764705882353" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
</resources> </resources>
</document> </document>

View file

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

@ -73,7 +73,7 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.13.1", "@apollo/client": "^3.13.1",
"@bam.tech/react-native-image-resizer": "^3.0.7", "@bam.tech/react-native-image-resizer": "^3.0.7",
"@expo/config-plugins": "~8.0.0", "@expo/config-plugins": "~9.0.0",
"@hookform/resolvers": "^3.2.0", "@hookform/resolvers": "^3.2.0",
"@mapbox/geo-viewport": "^0.5.0", "@mapbox/geo-viewport": "^0.5.0",
"@mapbox/locale-utils": "^0.0.6", "@mapbox/locale-utils": "^0.0.6",
@ -81,18 +81,19 @@
"@maplibre/maplibre-react-native": "10.0.0-alpha.23", "@maplibre/maplibre-react-native": "10.0.0-alpha.23",
"@notifee/react-native": "^9.1.8", "@notifee/react-native": "^9.1.8",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/netinfo": "^11.4.1", "@react-native-community/netinfo": "^11.3.3",
"@react-native-community/slider": "^4.5.2", "@react-native-community/slider": "4.5.5",
"@react-native-firebase/app": "^20.5.0", "@react-native-firebase/app": "^20.5.0",
"@react-native-firebase/messaging": "^20.5.0", "@react-native-firebase/messaging": "^20.5.0",
"@react-native-masked-view/masked-view": "0.3.1", "@react-native-masked-view/masked-view": "^0.3.0",
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/drawer": "^6.7.2", "@react-navigation/drawer": "^6.7.2",
"@react-navigation/elements": "^1.3.31", "@react-navigation/elements": "^1.3.31",
"@react-navigation/material-top-tabs": "^6.6.14", "@react-navigation/material-top-tabs": "^6.6.14",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.0.8",
"@react-navigation/stack": "^6.4.1", "@react-navigation/stack": "^6.3.21",
"@sentry/react-native": "^5.35.0", "@sentry/react-native": "~6.10.0",
"@sentry/tracing": "^7.120.3",
"@turf/along": "^7.1.0", "@turf/along": "^7.1.0",
"@turf/boolean-equal": "^7.1.0", "@turf/boolean-equal": "^7.1.0",
"@turf/distance": "^7.1.0", "@turf/distance": "^7.1.0",
@ -101,7 +102,7 @@
"@turf/meta": "^7.1.0", "@turf/meta": "^7.1.0",
"@turf/nearest-point": "^7.1.0", "@turf/nearest-point": "^7.1.0",
"@turf/point-to-line-distance": "^7.1.0", "@turf/point-to-line-distance": "^7.1.0",
"@types/react": "~18.2.79", "@types/react": "~18.3.12",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"ajv-errors": "^3.0.0", "ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
@ -113,26 +114,26 @@
"country-codes-list": "^1.6.11", "country-codes-list": "^1.6.11",
"delay": "^6.0.0", "delay": "^6.0.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"expo": "~51.0.39", "expo": "52",
"expo-av": "~14.0.7", "expo-av": "~15.0.2",
"expo-build-properties": "~0.12.5", "expo-build-properties": "~0.13.3",
"expo-constants": "~16.0.2", "expo-constants": "~17.0.8",
"expo-contacts": "~13.0.5", "expo-contacts": "~14.0.5",
"expo-dev-client": "~4.0.29", "expo-dev-client": "~5.0.20",
"expo-device": "~6.0.2", "expo-device": "~7.0.3",
"expo-gradle-ext-vars": "^0.1.1", "expo-gradle-ext-vars": "^0.1.1",
"expo-linear-gradient": "~13.0.2", "expo-linear-gradient": "~14.0.2",
"expo-linking": "~6.3.1", "expo-linking": "~7.0.5",
"expo-localization": "~15.0.3", "expo-localization": "~16.0.1",
"expo-location": "~17.0.1", "expo-location": "~18.0.10",
"expo-notifications": "~0.28.19", "expo-notifications": "~0.29.14",
"expo-secure-store": "~13.0.2", "expo-secure-store": "~14.0.1",
"expo-sensors": "~13.0.9", "expo-sensors": "~14.0.2",
"expo-splash-screen": "~0.27.7", "expo-splash-screen": "~0.29.24",
"expo-status-bar": "~1.12.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~3.0.7", "expo-system-ui": "~4.0.9",
"expo-task-manager": "~11.8.2", "expo-task-manager": "~12.0.6",
"expo-updates": "~0.25.27", "expo-updates": "~0.27.4",
"fast-equals": "^5.0.1", "fast-equals": "^5.0.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"geolib": "^3.3.4", "geolib": "^3.3.4",
@ -158,22 +159,23 @@
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1", "lodash.upperfirst": "^4.3.1",
"lottie-react-native": "^7.0.0", "lottie-react-native": "7.1.0",
"minisearch": "^6.1.0", "minisearch": "^6.1.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"open-color": "^1.9.1", "open-color": "^1.9.1",
"open-location-code": "^1.0.3", "open-location-code": "^1.0.3",
"osrm-text-instructions": "^0.15.0", "osrm-text-instructions": "^0.15.0",
"react": "18.2.0", "react": "18.3.1",
"react-countdown": "^2.3.5", "react-countdown": "^2.3.5",
"react-dom": "^18.2.0", "react-dom": "18.3.1",
"react-hook-form": "^7.47.0", "react-hook-form": "^7.47.0",
"react-i18next": "^13.0.2", "react-i18next": "^13.0.2",
"react-native": "0.74.5", "react-native": "0.76.9",
"react-native-animatable": "^1.3.3", "react-native-animatable": "^1.3.3",
"react-native-app-link": "^1.0.1", "react-native-app-link": "^1.0.1",
"react-native-background-fetch": "^4.2.7", "react-native-background-fetch": "^4.2.7",
"react-native-background-geolocation": "^4.18.6", "react-native-background-geolocation": "^4.18.6",
"react-native-battery-optimization-check": "^1.0.8",
"react-native-contact-pick": "^0.1.2", "react-native-contact-pick": "^0.1.2",
"react-native-country-picker-modal": "^2.0.0", "react-native-country-picker-modal": "^2.0.0",
"react-native-device-country": "^1.0.5", "react-native-device-country": "^1.0.5",
@ -181,20 +183,20 @@
"react-native-dropdownalert": "^5.1.0", "react-native-dropdownalert": "^5.1.0",
"react-native-error-boundary": "^1.2.8", "react-native-error-boundary": "^1.2.8",
"react-native-geolocation-service": "^5.3.1", "react-native-geolocation-service": "^5.3.1",
"react-native-gesture-handler": "~2.16.1", "react-native-gesture-handler": "~2.20.2",
"react-native-image-crop-picker": "^0.40.3", "react-native-image-crop-picker": "^0.40.3",
"react-native-immediate-phone-call": "^2.0.0", "react-native-immediate-phone-call": "^2.0.0",
"react-native-map-link": "^3.6.1", "react-native-map-link": "^3.6.1",
"react-native-material-ripple": "^0.9.1", "react-native-material-ripple": "^0.9.1",
"react-native-modal-selector": "^2.1.2", "react-native-modal-selector": "^2.1.2",
"react-native-optiongroup": "^0.0.7", "react-native-optiongroup": "^0.0.7",
"react-native-pager-view": "6.3.0", "react-native-pager-view": "6.5.1",
"react-native-paper": "^5.9.1", "react-native-paper": "^5.9.1",
"react-native-permissions": "^4.1.5", "react-native-permissions": "^4.1.5",
"react-native-phone-number-input": "^2.1.0", "react-native-phone-number-input": "^2.1.0",
"react-native-reanimated": "~3.10.1", "react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.10.5", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "3.31.1", "react-native-screens": "~4.4.0",
"react-native-send-intent": "^1.3.0", "react-native-send-intent": "^1.3.0",
"react-native-storage": "^1.0.1", "react-native-storage": "^1.0.1",
"react-native-styled-text": "^2.0.0", "react-native-styled-text": "^2.0.0",
@ -203,9 +205,9 @@
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.2", "react-native-uuid": "^2.0.2",
"react-native-vector-icons": "^9.2.0", "react-native-vector-icons": "^9.2.0",
"react-native-web": "~0.19.10", "react-native-web": "~0.19.13",
"supercluster": "^8.0.1", "supercluster": "^8.0.1",
"typescript": "~5.3.3", "typescript": "~5.8.3",
"use-sync-external-store": "^1.4.0", "use-sync-external-store": "^1.4.0",
"zustand": "^5.0.3", "zustand": "^5.0.3",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
@ -220,7 +222,8 @@
"@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-flow-strip-types": "^7.25.2",
"@babel/preset-env": "^7.22.7", "@babel/preset-env": "^7.22.7",
"@babel/preset-react": "^7.24.1", "@babel/preset-react": "^7.24.1",
"@react-native/metro-config": "^0.75.3", "@react-native-community/cli": "^18.0.0",
"@react-native/metro-config": "^0.75.0",
"@types/lodash.debounce": "^4", "@types/lodash.debounce": "^4",
"@types/lodash.kebabcase": "^4", "@types/lodash.kebabcase": "^4",
"@types/lodash.snakecase": "^4", "@types/lodash.snakecase": "^4",
@ -251,14 +254,13 @@
"eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-sort-keys-fix": "^1.1.2",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.0.0",
"husky": "^9.0.11", "husky": "^9.0.11",
"jest": "^29.7.0", "jest": "^29.2.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"react-native-clean-project": "^4.0.3", "react-native-clean-project": "^4.0.3",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"private": true, "private": true,
"resolutions": { "resolutions": {
"expo-modules-core@1.12.23": "patch:expo-modules-core@npm%3A1.12.23#./.yarn/patches/expo-modules-core-npm-1.12.23-4ea588b9bf.patch",
"react-native-drawer@^2.5.1": "patch:react-native-drawer@npm%3A2.5.1#./.yarn/patches/react-native-drawer-npm-2.5.1-d9da0c325e.patch" "react-native-drawer@^2.5.1": "patch:react-native-drawer@npm%3A2.5.1#./.yarn/patches/react-native-drawer-npm-2.5.1-d9da0c325e.patch"
}, },
"detox": { "detox": {
@ -276,4 +278,4 @@
} }
}, },
"packageManager": "yarn@4.5.3" "packageManager": "yarn@4.5.3"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1,7 +1,18 @@
import React, { useState, useCallback, useEffect } from "react"; import React, { useState, useCallback, useEffect } from "react";
import { View, StyleSheet, Image, ScrollView, Platform } from "react-native"; import {
View,
StyleSheet,
Image,
ScrollView,
Platform,
AppState,
} from "react-native";
import { Title } from "react-native-paper"; import { Title } from "react-native-paper";
import { Ionicons, Entypo } from "@expo/vector-icons"; import { Ionicons, Entypo } from "@expo/vector-icons";
import {
RequestDisableOptimization,
BatteryOptEnabled,
} from "react-native-battery-optimization-check";
import { import {
permissionsActions, permissionsActions,
usePermissionsState, usePermissionsState,
@ -30,7 +41,15 @@ const HeroMode = () => {
const [requesting, setRequesting] = useState(false); const [requesting, setRequesting] = useState(false);
const [hasAttempted, setHasAttempted] = useState(false); const [hasAttempted, setHasAttempted] = useState(false);
const [hasRetried, setHasRetried] = useState(false); const [hasRetried, setHasRetried] = useState(false);
const permissions = usePermissionsState(["locationBackground", "motion"]); const [batteryOptimizationEnabled, setBatteryOptimizationEnabled] =
useState(null);
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
const permissions = usePermissionsState([
"locationBackground",
"motion",
"batteryOptimizationDisabled",
]);
const theme = useTheme(); const theme = useTheme();
const [skipMessage] = useState(() => { const [skipMessage] = useState(() => {
@ -46,38 +65,172 @@ const HeroMode = () => {
permissionWizardActions.setCurrentStep("skipInfo"); permissionWizardActions.setCurrentStep("skipInfo");
}, []); }, []);
const handleBatteryOptimization = useCallback(async () => {
if (Platform.OS !== "android") {
permissionsActions.setBatteryOptimizationDisabled(true);
return true;
}
try {
setBatteryOptInProgress(true);
const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled);
if (isEnabled) {
console.log(
"Battery optimization is enabled, requesting to disable...",
);
RequestDisableOptimization();
setBatteryOptAttempted(true);
// Give some time for the user to interact with the system dialog
// We'll check the status again in the retry flow
return false;
} else {
console.log("Battery optimization already disabled");
permissionsActions.setBatteryOptimizationDisabled(true);
return true;
}
} catch (error) {
console.error("Error handling battery optimization:", error);
setBatteryOptAttempted(true);
return false;
} finally {
setBatteryOptInProgress(false);
}
}, []);
const handleRequestPermissions = useCallback(async () => { const handleRequestPermissions = useCallback(async () => {
setRequesting(true); setRequesting(true);
try { try {
// Set step to tracking before requesting permissions // Don't change step immediately to avoid race conditions
permissionWizardActions.setCurrentStep("tracking"); console.log("Starting permission requests...");
// Request motion permission first // Request motion permission first
const motionGranted = await requestPermissionMotion.requestPermission(); const motionGranted = await requestPermissionMotion.requestPermission();
permissionsActions.setMotion(motionGranted); permissionsActions.setMotion(motionGranted);
console.log("Motion permission:", motionGranted);
// Then request background location // Then request background location
const locationGranted = await requestPermissionLocationBackground(); const locationGranted = await requestPermissionLocationBackground();
permissionsActions.setLocationBackground(locationGranted); permissionsActions.setLocationBackground(locationGranted);
console.log("Location background permission:", locationGranted);
// If both granted, move to success // Handle battery optimization separately to avoid dialog conflicts
if (locationGranted && motionGranted) { const batteryOptDisabled = await handleBatteryOptimization();
console.log("Battery optimization disabled:", batteryOptDisabled);
// Only set step to tracking after all permission requests are complete
permissionWizardActions.setCurrentStep("tracking");
// Check if we should proceed to success immediately
if (locationGranted && motionGranted && batteryOptDisabled) {
permissionWizardActions.setHeroPermissionsGranted(true); permissionWizardActions.setHeroPermissionsGranted(true);
permissionWizardActions.setCurrentStep("success"); // Don't navigate immediately, let the useEffect handle it
} }
} catch (error) { } catch (error) {
console.error("Error requesting permissions:", error); console.error("Error requesting permissions:", error);
} }
setRequesting(false); setRequesting(false);
setHasAttempted(true); setHasAttempted(true);
}, []); }, [handleBatteryOptimization]);
const handleRetry = useCallback(async () => { const handleRetry = useCallback(async () => {
await handleRequestPermissions(); // Re-check battery optimization status before retrying
setHasRetried(true); if (Platform.OS === "android") {
}, [handleRequestPermissions]); try {
const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled);
const allGranted = permissions.locationBackground && permissions.motion; // If battery optimization is now disabled, update the store
if (!isEnabled) {
console.log("Battery optimization now disabled after retry");
permissionsActions.setBatteryOptimizationDisabled(true);
}
} catch (error) {
console.error("Error re-checking battery optimization:", error);
}
}
// Only request permissions again if some are still missing
const needsRetry =
!permissions.locationBackground ||
!permissions.motion ||
(Platform.OS === "android" && batteryOptimizationEnabled);
if (needsRetry) {
await handleRequestPermissions();
}
setHasRetried(true);
}, [handleRequestPermissions, permissions, batteryOptimizationEnabled]);
const allGranted =
permissions.locationBackground &&
permissions.motion &&
(Platform.OS === "ios" || !batteryOptimizationEnabled);
// Check battery optimization status on mount
useEffect(() => {
const checkInitialBatteryOptimization = async () => {
if (Platform.OS === "android") {
try {
const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled);
// If already disabled, update the store
if (!isEnabled) {
permissionsActions.setBatteryOptimizationDisabled(true);
}
} catch (error) {
console.error("Error checking initial battery optimization:", error);
}
} else {
// iOS doesn't have battery optimization, so mark as disabled
permissionsActions.setBatteryOptimizationDisabled(true);
}
};
checkInitialBatteryOptimization();
}, []);
// Listen for app state changes to re-check battery optimization when user returns from settings
useEffect(() => {
const handleAppStateChange = async (nextAppState) => {
if (
nextAppState === "active" &&
Platform.OS === "android" &&
batteryOptAttempted
) {
console.log("App became active, re-checking battery optimization...");
try {
const isEnabled = await BatteryOptEnabled();
setBatteryOptimizationEnabled(isEnabled);
if (!isEnabled) {
console.log(
"Battery optimization disabled after returning from settings",
);
permissionsActions.setBatteryOptimizationDisabled(true);
}
} catch (error) {
console.error(
"Error re-checking battery optimization on app focus:",
error,
);
}
}
};
const subscription = AppState.addEventListener(
"change",
handleAppStateChange,
);
return () => {
subscription?.remove();
};
}, [batteryOptAttempted]);
useEffect(() => { useEffect(() => {
if (hasAttempted && allGranted) { if (hasAttempted && allGranted) {
@ -99,6 +252,15 @@ const HeroMode = () => {
"Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.", "Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.",
); );
} }
if (
Platform.OS === "android" &&
batteryOptimizationEnabled &&
batteryOptAttempted
) {
warnings.push(
"L'optimisation de la batterie est encore activée. L'application pourrait ne pas fonctionner correctement en arrière-plan.",
);
}
return warnings.length > 0 ? ( return warnings.length > 0 ? (
<View style={styles.warningsContainer}> <View style={styles.warningsContainer}>
{warnings.map((warning, index) => ( {warnings.map((warning, index) => (
@ -140,6 +302,22 @@ const HeroMode = () => {
version d'Android) version d'Android)
</Text> </Text>
</View> </View>
{batteryOptimizationEnabled && batteryOptAttempted && (
<View style={styles.androidWarningSteps}>
<Text style={styles.androidWarningText}>
Pour désactiver l'optimisation de la batterie :
</Text>
<Text style={styles.androidWarningStep}>
4. Recherchez "Batterie" ou "Optimisation de la batterie"
</Text>
<Text style={styles.androidWarningStep}>
5. Trouvez cette application dans la liste
</Text>
<Text style={styles.androidWarningStep}>
6. Sélectionnez "Ne pas optimiser" ou "Désactiver l'optimisation"
</Text>
</View>
)}
<CustomButton <CustomButton
mode="outlined" mode="outlined"
onPress={openSettings} onPress={openSettings}
@ -211,8 +389,11 @@ const HeroMode = () => {
mode="contained" mode="contained"
onPress={handleRequestPermissions} onPress={handleRequestPermissions}
loading={requesting} loading={requesting}
disabled={batteryOptInProgress}
> >
J'accorde les permissions {batteryOptInProgress
? "Traitement de l'optimisation de la batterie..."
: "J'accorde les permissions"}
</CustomButton> </CustomButton>
); );
} }
@ -234,8 +415,15 @@ const HeroMode = () => {
> >
{skipMessage} {skipMessage}
</CustomButton> </CustomButton>
<CustomButton mode="contained" onPress={handleRetry}> <CustomButton
Réessayer d'accorder les permissions mode="contained"
onPress={handleRetry}
loading={requesting}
disabled={batteryOptInProgress}
>
{batteryOptInProgress
? "Vérification en cours..."
: "Réessayer d'accorder les permissions"}
</CustomButton> </CustomButton>
{hasRetried && ( {hasRetried && (
<> <>
@ -295,6 +483,20 @@ const HeroMode = () => {
donnée de mouvement n'est stockée ni transmise. donnée de mouvement n'est stockée ni transmise.
</Text> </Text>
</View> </View>
{Platform.OS === "android" && (
<View style={styles.permissionItem}>
<Ionicons
name="battery-charging"
size={24}
style={styles.icon}
/>
<Text style={styles.permissionText}>
Optimisation de la batterie : désactiver l'optimisation de
la batterie pour cette application afin qu'elle puisse
fonctionner correctement en arrière-plan.
</Text>
</View>
)}
</View> </View>
</View> </View>

View file

@ -4,6 +4,7 @@ import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes"; import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode";
import { initEmulatorMode } from "./emulatorService"; import { initEmulatorMode } from "./emulatorService";
import * as Sentry from "@sentry/react-native";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
@ -32,6 +33,7 @@ const config = {
locationAuthorizationRequest: "Always", locationAuthorizationRequest: "Always",
stopOnTerminate: false, stopOnTerminate: false,
startOnBoot: true, startOnBoot: true,
heartbeatInterval: 60, // DEBUGGING
// Force the plugin to start aggressively // Force the plugin to start aggressively
foregroundService: true, foregroundService: true,
notification: { notification: {
@ -167,6 +169,20 @@ export default async function trackLocation() {
activity: location.activity, activity: location.activity,
battery: location.battery, battery: location.battery,
}); });
// Add Sentry breadcrumb for location updates
Sentry.addBreadcrumb({
message: "Location update in trackLocation",
category: "geolocation",
level: "info",
data: {
coords: location.coords,
activity: location.activity?.type,
battery: location.battery?.level,
isMoving: location.isMoving,
},
});
if ( if (
location.coords && location.coords &&
location.coords.latitude && location.coords.latitude &&
@ -203,6 +219,20 @@ export default async function trackLocation() {
response?.request?.headers || "Headers not available in response", response?.request?.headers || "Headers not available in response",
}); });
// Add Sentry breadcrumb for HTTP responses
Sentry.addBreadcrumb({
message: "Background geolocation HTTP response",
category: "geolocation-http",
level: response?.status === 200 ? "info" : "warning",
data: {
status: response?.status,
success: response?.success,
url: response?.url,
isSync: response?.isSync,
recordCount: response?.count,
},
});
// Log the current auth token for comparison // Log the current auth token for comparison
const { userToken } = getAuthState(); const { userToken } = getAuthState();
locationLogger.debug("Current auth state token", { locationLogger.debug("Current auth state token", {
@ -216,6 +246,11 @@ export default async function trackLocation() {
case 410: case 410:
// Token expired, logout // Token expired, logout
locationLogger.info("Auth token expired (410), logging out"); locationLogger.info("Auth token expired (410), logging out");
Sentry.addBreadcrumb({
message: "Auth token expired - logging out",
category: "geolocation-auth",
level: "warning",
});
authActions.logout(); authActions.logout();
break; break;
case 401: case 401:
@ -233,6 +268,16 @@ export default async function trackLocation() {
errorMessage: errorBody?.error?.message, errorMessage: errorBody?.error?.message,
errorPath: errorBody?.error?.errors?.[0]?.path, errorPath: errorBody?.error?.errors?.[0]?.path,
}); });
Sentry.addBreadcrumb({
message: "Unauthorized - refreshing token",
category: "geolocation-auth",
level: "warning",
data: {
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
},
});
} catch (e) { } catch (e) {
locationLogger.debug("Failed to parse error response", { locationLogger.debug("Failed to parse error response", {
error: e.message, error: e.message,
@ -291,8 +336,21 @@ export default async function trackLocation() {
const count = await BackgroundGeolocation.getCount(); const count = await BackgroundGeolocation.getCount();
locationLogger.debug("Pending location records", { count }); locationLogger.debug("Pending location records", { count });
Sentry.addBreadcrumb({
message: "Checking pending location records",
category: "geolocation",
level: "info",
data: { pendingCount: count },
});
if (count > 0) { if (count > 0) {
locationLogger.info(`Found ${count} pending records, forcing sync`); locationLogger.info(`Found ${count} pending records, forcing sync`);
const transaction = Sentry.startTransaction({
name: "force-sync-pending-records",
op: "geolocation-sync",
});
try { try {
const { userToken } = getAuthState(); const { userToken } = getAuthState();
const state = await BackgroundGeolocation.getState(); const state = await BackgroundGeolocation.getState();
@ -301,18 +359,64 @@ export default async function trackLocation() {
locationLogger.debug("Forced sync result", { locationLogger.debug("Forced sync result", {
recordsCount: records?.length || 0, recordsCount: records?.length || 0,
}); });
Sentry.addBreadcrumb({
message: "Forced sync completed",
category: "geolocation",
level: "info",
data: {
recordsCount: records?.length || 0,
hadToken: true,
wasEnabled: true,
},
});
transaction.setStatus("ok");
} else {
Sentry.addBreadcrumb({
message: "Forced sync skipped",
category: "geolocation",
level: "warning",
data: {
hasToken: !!userToken,
isEnabled: state.enabled,
},
});
transaction.setStatus("cancelled");
} }
} catch (error) { } catch (error) {
locationLogger.error("Forced sync failed", { locationLogger.error("Forced sync failed", {
error: error, error: error,
stack: error.stack, stack: error.stack,
}); });
Sentry.captureException(error, {
tags: {
module: "track-location",
operation: "force-sync-pending",
},
contexts: {
pendingRecords: { count },
},
});
transaction.setStatus("internal_error");
} finally {
transaction.finish();
} }
} }
} catch (error) { } catch (error) {
locationLogger.error("Failed to get pending records count", { locationLogger.error("Failed to get pending records count", {
error: error.message, error: error.message,
}); });
Sentry.captureException(error, {
tags: {
module: "track-location",
operation: "check-pending-records",
},
});
} }
} }

View file

@ -1,17 +1,186 @@
import notifee from "@notifee/react-native"; import notifee from "@notifee/react-native";
import BackgroundFetch from "react-native-background-fetch"; import BackgroundFetch from "react-native-background-fetch";
import * as Sentry from "@sentry/react-native";
import useMount from "~/hooks/useMount"; 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 // Background task to cancel expired notifications
const backgroundTask = async () => { const backgroundTask = async () => {
const notifications = await notifee.getDisplayedNotifications(); const transaction = Sentry.startTransaction({
const currentTime = Math.round(new Date() / 1000); name: "auto-cancel-expired-notifications",
for (const notification of notifications) { op: "background-task",
const expires = notification.data?.expires; });
if (expires && expires < currentTime) {
await notifee.cancelNotification(notification.id); Sentry.getCurrentScope().setSpan(transaction);
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
const getNotificationsSpan = transaction.startChild({
op: "get-displayed-notifications",
description: "Getting displayed notifications",
});
let notifications;
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;
} finally {
getNotificationsSpan.finish();
} }
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,
},
});
transaction.setStatus("ok");
} catch (error) {
logger.error("Auto-cancel task failed", { error });
Sentry.captureException(error, {
tags: {
module: "auto-cancel-expired",
operation: "background-task",
},
});
transaction.setStatus("internal_error");
throw error; // Re-throw to be handled by caller
} finally {
transaction.finish();
} }
}; };
@ -27,12 +196,61 @@ export const useAutoCancelExpired = () => {
enableHeadless: true, enableHeadless: true,
}, },
async (taskId) => { async (taskId) => {
console.log("[BackgroundFetch] taskId:", taskId); logger.info("BackgroundFetch task started", { taskId });
await backgroundTask();
BackgroundFetch.finish(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) => { (error) => {
console.log("[BackgroundFetch] failed to start", error); logger.error("BackgroundFetch failed to start", { error });
Sentry.captureException(error, {
tags: {
module: "auto-cancel-expired",
operation: "background-fetch-configure",
},
});
}, },
); );
return () => { return () => {
@ -43,7 +261,104 @@ export const useAutoCancelExpired = () => {
// Register headless task // Register headless task
BackgroundFetch.registerHeadlessTask(async (event) => { BackgroundFetch.registerHeadlessTask(async (event) => {
const taskId = event.taskId; const taskId = event?.taskId;
await backgroundTask();
BackgroundFetch.finish(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

@ -16,7 +16,7 @@ import { largeIcons, smallIcon } from "../icons";
import { ALERT_INFOS_QUERY } from "../gql"; import { ALERT_INFOS_QUERY } from "../gql";
import { displayNotification, createChannel } from "../helpers"; import { displayNotification, createChannel } from "../helpers";
import { generateAlertInfosContent } from "../content"; import { generateAlertEmergencyInfoContent } from "../content";
const { custom } = Light; const { custom } = Light;
@ -60,7 +60,7 @@ export default async function notifAlertInfos(data) {
const largeIcon = largeIcons[level]; const largeIcon = largeIcons[level];
// Generate notification content // Generate notification content
const { title, body, bigText } = generateAlertInfosContent({ const { title, body, bigText } = generateAlertEmergencyInfoContent({
code, code,
what3Words, what3Words,
address, address,

View file

@ -1,8 +1,18 @@
import React, { useEffect, useCallback } from "react"; import React, { useEffect, useCallback } from "react";
import { View, TouchableOpacity, StyleSheet } from "react-native"; import {
View,
TouchableOpacity,
StyleSheet,
Platform,
AppState,
} from "react-native";
import { Button, Title } from "react-native-paper"; import { Button, Title } from "react-native-paper";
import { usePermissionsState, permissionsActions } from "~/stores"; import { usePermissionsState, permissionsActions } from "~/stores";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {
RequestDisableOptimization,
BatteryOptEnabled,
} from "react-native-battery-optimization-check";
import openSettings from "~/lib/native/openSettings"; import openSettings from "~/lib/native/openSettings";
import requestPermissionFcm from "~/permissions/requestPermissionFcm"; import requestPermissionFcm from "~/permissions/requestPermissionFcm";
@ -16,6 +26,29 @@ import * as Location from "expo-location";
import * as Notifications from "expo-notifications"; import * as Notifications from "expo-notifications";
import * as Contacts from "expo-contacts"; import * as Contacts from "expo-contacts";
// Battery optimization request handler
const requestBatteryOptimizationDisable = async () => {
if (Platform.OS !== "android") {
return true; // iOS doesn't have battery optimization
}
try {
const isEnabled = await BatteryOptEnabled();
if (isEnabled) {
console.log("Battery optimization enabled, requesting to disable...");
RequestDisableOptimization();
// Return false as the user needs to interact with the system dialog
return false;
} else {
console.log("Battery optimization already disabled");
return true;
}
} catch (error) {
console.error("Error handling battery optimization:", error);
return false;
}
};
const requestPermissions = { const requestPermissions = {
fcm: requestPermissionFcm, fcm: requestPermissionFcm,
locationBackground: requestPermissionLocationBackground, locationBackground: requestPermissionLocationBackground,
@ -23,6 +56,7 @@ const requestPermissions = {
readContacts: requestPermissionReadContacts, readContacts: requestPermissionReadContacts,
phoneCall: requestPermissionPhoneCall, phoneCall: requestPermissionPhoneCall,
motion: requestPermissionMotion.requestPermission, motion: requestPermissionMotion.requestPermission,
batteryOptimizationDisabled: requestBatteryOptimizationDisable,
}; };
const setPermissions = { const setPermissions = {
@ -32,6 +66,8 @@ const setPermissions = {
readContacts: (b) => permissionsActions.setReadContacts(b), readContacts: (b) => permissionsActions.setReadContacts(b),
phoneCall: (b) => permissionsActions.setPhoneCall(b), phoneCall: (b) => permissionsActions.setPhoneCall(b),
motion: (b) => permissionsActions.setMotion(b), motion: (b) => permissionsActions.setMotion(b),
batteryOptimizationDisabled: (b) =>
permissionsActions.setBatteryOptimizationDisabled(b),
}; };
const titlePermissions = { const titlePermissions = {
@ -41,6 +77,7 @@ const titlePermissions = {
readContacts: "Contacts", readContacts: "Contacts",
phoneCall: "Appels", phoneCall: "Appels",
motion: "Détection de mouvement", motion: "Détection de mouvement",
batteryOptimizationDisabled: "Optimisation de la batterie",
}; };
// Function to check current permission status // Function to check current permission status
@ -68,6 +105,17 @@ const checkPermissionStatus = async (permission) => {
// Note: Phone call permissions on iOS are determined at build time // Note: Phone call permissions on iOS are determined at build time
// and on Android they're requested at runtime // and on Android they're requested at runtime
return true; // This might need adjustment based on your specific implementation return true; // This might need adjustment based on your specific implementation
case "batteryOptimizationDisabled":
if (Platform.OS !== "android") {
return true; // iOS doesn't have battery optimization
}
try {
const isEnabled = await BatteryOptEnabled();
return !isEnabled; // Return true if optimization is disabled
} catch (error) {
console.error("Error checking battery optimization:", error);
return false;
}
default: default:
return false; return false;
} }
@ -95,18 +143,9 @@ const PermissionItem = ({ permission, status, onRequestPermission }) => (
); );
export default function Permissions() { export default function Permissions() {
const permissionsState = usePermissionsState([ // Create permissions list based on platform
"fcm", const getPermissionsList = () => {
"phoneCall", const basePermissions = [
"locationForeground",
"locationBackground",
"motion",
"readContacts",
]);
// Memoize the check permissions function
const checkAllPermissions = useCallback(async () => {
const permissionKeys = [
"fcm", "fcm",
"phoneCall", "phoneCall",
"locationForeground", "locationForeground",
@ -115,25 +154,70 @@ export default function Permissions() {
"readContacts", "readContacts",
]; ];
for (const permission of permissionKeys) { // Add battery optimization only on Android
if (Platform.OS === "android") {
return [...basePermissions, "batteryOptimizationDisabled"];
}
return basePermissions;
};
const permissionsList = getPermissionsList();
const permissionsState = usePermissionsState(permissionsList);
// Memoize the check permissions function
const checkAllPermissions = useCallback(async () => {
for (const permission of permissionsList) {
const status = await checkPermissionStatus(permission); const status = await checkPermissionStatus(permission);
setPermissions[permission](status); setPermissions[permission](status);
} }
}, []); }, [permissionsList]);
// Check all permissions when component mounts // Check all permissions when component mounts
useEffect(() => { useEffect(() => {
checkAllPermissions(); checkAllPermissions();
}, [checkAllPermissions]); }, [checkAllPermissions]);
// Listen for app state changes to re-check permissions when user returns from settings
useEffect(() => {
const handleAppStateChange = async (nextAppState) => {
if (nextAppState === "active") {
console.log("App became active, re-checking all permissions...");
// Re-check all permissions when app becomes active
await checkAllPermissions();
}
};
const subscription = AppState.addEventListener(
"change",
handleAppStateChange,
);
return () => {
subscription?.remove();
};
}, [checkAllPermissions]);
const handleRequestPermission = async (permission) => { const handleRequestPermission = async (permission) => {
try { try {
const granted = await requestPermissions[permission](); const granted = await requestPermissions[permission]();
setPermissions[permission](granted); setPermissions[permission](granted);
// Double-check the status to ensure UI is in sync // For battery optimization, we need to handle the async nature differently
const actualStatus = await checkPermissionStatus(permission); if (
setPermissions[permission](actualStatus); permission === "batteryOptimizationDisabled" &&
Platform.OS === "android"
) {
// Give a short delay for the system dialog to potentially complete
setTimeout(async () => {
const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus);
}, 1000);
} else {
// Double-check the status to ensure UI is in sync
const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus);
}
} catch (error) { } catch (error) {
console.error(`Error requesting ${permission} permission:`, error); console.error(`Error requesting ${permission} permission:`, error);
} }

View file

@ -1,4 +1,5 @@
import * as Sentry from "@sentry/react-native"; import * as Sentry from "@sentry/react-native";
import "@sentry/tracing";
import { Platform } from "react-native"; import { Platform } from "react-native";
import env from "~/env"; import env from "~/env";

View file

@ -25,6 +25,10 @@ export default createAtom(({ merge }) => {
merge({ motion }); merge({ motion });
}; };
const setBatteryOptimizationDisabled = (batteryOptimizationDisabled) => {
merge({ batteryOptimizationDisabled });
};
return { return {
default: { default: {
fcm: false, fcm: false,
@ -33,6 +37,7 @@ export default createAtom(({ merge }) => {
readContacts: false, readContacts: false,
phoneCall: false, phoneCall: false,
motion: false, motion: false,
batteryOptimizationDisabled: false,
}, },
actions: { actions: {
setFcm, setFcm,
@ -41,6 +46,7 @@ export default createAtom(({ merge }) => {
setReadContacts, setReadContacts,
setPhoneCall, setPhoneCall,
setMotion, setMotion,
setBatteryOptimizationDisabled,
}, },
}; };
}); });

4787
yarn.lock

File diff suppressed because it is too large Load diff