Compare commits
17 commits
ac84ae707b
...
8331029e91
Author | SHA1 | Date | |
---|---|---|---|
8331029e91 | |||
b4b7441bac | |||
774f7dc163 | |||
52e7314005 | |||
7a816c867e | |||
16332cbb76 | |||
8e8e120391 | |||
e6924ac9ff | |||
4a0f3ab7ef | |||
8ba4056187 | |||
d780fb4190 | |||
8d669c8b73 | |||
a1ed6cfca6 | |||
c947d4915a | |||
d9b5d10684 | |||
7082161b7f | |||
6c290f21b4 |
|
@ -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(),
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
BIN
android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
Normal file
After Width: | Height: | Size: 70 KiB |
|
@ -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>
|
|
@ -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>
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -64,3 +62,6 @@ 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
|
|
@ -23,9 +23,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 +56,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
|
@ -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/
|
358
index.js
|
@ -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,404 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
|
||||||
// the environment is set up appropriately
|
// the environment is set up appropriately
|
||||||
registerRootComponent(App);
|
registerRootComponent(App);
|
||||||
|
|
||||||
|
// Constants for persistence
|
||||||
|
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
|
||||||
|
const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Helper functions for persisting sync time
|
||||||
|
const getLastSyncTime = async () => {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(LAST_SYNC_TIME_KEY);
|
||||||
|
return value ? parseInt(value, 10) : Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { module: "headless-task", operation: "get-last-sync-time" },
|
||||||
|
});
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLastSyncTime = async (time) => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString());
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { module: "headless-task", operation: "set-last-sync-time" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
// 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
|
||||||
|
|
||||||
|
// Simple performance tracking without deprecated APIs
|
||||||
|
const taskStartTime = Date.now();
|
||||||
|
|
||||||
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 with performance tracking
|
||||||
|
const locationStartTime = Date.now();
|
||||||
const location = await getCurrentPosition();
|
const location = await getCurrentPosition();
|
||||||
|
const locationDuration = Date.now() - locationStartTime;
|
||||||
|
|
||||||
|
const isLocationError = location && location.code !== undefined;
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "getCurrentPosition completed",
|
||||||
|
category: "headless-task",
|
||||||
|
level: isLocationError ? "warning" : "info",
|
||||||
|
data: {
|
||||||
|
success: !isLocationError,
|
||||||
|
error: isLocationError ? location : undefined,
|
||||||
|
coords: !isLocationError ? location?.coords : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
geolocBgLogger.debug("getCurrentPosition result", { location });
|
geolocBgLogger.debug("getCurrentPosition result", { location });
|
||||||
|
|
||||||
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
||||||
geolocBgLogger.info("Forcing location sync after 24h");
|
geolocBgLogger.info("Forcing location sync after 24h");
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Force sync triggered",
|
||||||
|
category: "headless-task",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
timeSinceLastSyncHours: (
|
||||||
|
timeSinceLastSync /
|
||||||
|
(1000 * 60 * 60)
|
||||||
|
).toFixed(2),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get pending records count before sync with timeout
|
||||||
|
const pendingCount = await Promise.race([
|
||||||
|
BackgroundGeolocation.getCount(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("getCount timeout")), 10000),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Pending records count",
|
||||||
|
category: "headless-task",
|
||||||
|
level: "info",
|
||||||
|
data: { pendingCount },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change pace to ensure location updates with timeout
|
||||||
|
await Promise.race([
|
||||||
|
BackgroundGeolocation.changePace(true),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("changePace timeout")),
|
||||||
|
10000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "changePace completed",
|
||||||
|
category: "headless-task",
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform sync with timeout
|
||||||
|
const syncResult = await Promise.race([
|
||||||
|
BackgroundGeolocation.sync(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("sync timeout")), 20000),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Sync completed successfully",
|
||||||
|
category: "headless-task",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
syncResult: Array.isArray(syncResult)
|
||||||
|
? `${syncResult.length} records`
|
||||||
|
: "completed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Update last sync time after successful sync
|
// Update last sync time after successful sync
|
||||||
await BackgroundGeolocation.changePace(true);
|
await setLastSyncTime(now);
|
||||||
await BackgroundGeolocation.sync();
|
|
||||||
lastSyncTime = 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 },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Task completed successfully
|
||||||
|
const taskDuration = Date.now() - taskStartTime;
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "HeadlessTask completed successfully",
|
||||||
|
category: "headless-task",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
eventName: name,
|
||||||
|
duration: taskDuration,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
geolocBgLogger.error("HeadlessTask error", { error });
|
const taskDuration = Date.now() - taskStartTime;
|
||||||
|
|
||||||
|
// Capture any unexpected errors
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: {
|
||||||
|
module: "headless-task",
|
||||||
|
eventName: event?.name || "unknown",
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
duration: taskDuration,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
geolocBgLogger.error("HeadlessTask error", {
|
||||||
|
error,
|
||||||
|
event,
|
||||||
|
duration: taskDuration,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// Clear the timeout
|
||||||
|
clearTimeout(taskTimeout);
|
||||||
|
|
||||||
|
const finalDuration = Date.now() - taskStartTime;
|
||||||
|
geolocBgLogger.debug("HeadlessTask completed", {
|
||||||
|
event: event?.name,
|
||||||
|
duration: finalDuration,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,20 @@
|
||||||
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 */,
|
||||||
|
B1AB92A327A24FB294681EDD /* Fix Xcode 15 Bug */,
|
||||||
|
0E26E4D25E2E49C3AB2723FA /* Fix Xcode 15 Bug */,
|
||||||
|
8589214E888941E1817F4C9F /* Remove signature files (Xcode workaround) */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -644,6 +657,448 @@ 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
B1AB92A327A24FB294681EDD /* 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";
|
||||||
|
};
|
||||||
|
F65632CE7D4A409F817EEC81 /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
0E26E4D25E2E49C3AB2723FA /* 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";
|
||||||
|
};
|
||||||
|
8589214E888941E1817F4C9F /* 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 */
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
23
ios/AlerteSecours/Images.xcassets/SplashScreenLogo.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
BIN
ios/AlerteSecours/Images.xcassets/SplashScreenLogo.imageset/image.png
vendored
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
ios/AlerteSecours/Images.xcassets/SplashScreenLogo.imageset/image@2x.png
vendored
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
ios/AlerteSecours/Images.xcassets/SplashScreenLogo.imageset/image@3x.png
vendored
Normal file
After Width: | Height: | Size: 90 KiB |
|
@ -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>
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
76
package.json
|
@ -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",
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
"@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.4.1",
|
||||||
"@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.1",
|
||||||
|
@ -92,7 +92,8 @@
|
||||||
"@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.1.18",
|
||||||
"@react-navigation/stack": "^6.4.1",
|
"@react-navigation/stack": "^6.4.1",
|
||||||
"@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": {
|
||||||
|
|
BIN
src/assets/img/splash-icon.png
Normal file
After Width: | Height: | Size: 68 KiB |
|
@ -1,5 +1,12 @@
|
||||||
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 {
|
import {
|
||||||
|
@ -9,6 +16,10 @@ import {
|
||||||
} from "~/stores";
|
} from "~/stores";
|
||||||
import { createStyles, useTheme } from "~/theme";
|
import { createStyles, useTheme } from "~/theme";
|
||||||
import openSettings from "~/lib/native/openSettings";
|
import openSettings from "~/lib/native/openSettings";
|
||||||
|
import {
|
||||||
|
RequestDisableOptimization,
|
||||||
|
BatteryOptEnabled,
|
||||||
|
} from "react-native-battery-optimization-check";
|
||||||
|
|
||||||
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
||||||
import requestPermissionMotion from "~/permissions/requestPermissionMotion";
|
import requestPermissionMotion from "~/permissions/requestPermissionMotion";
|
||||||
|
@ -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,176 @@ const HeroMode = () => {
|
||||||
permissionWizardActions.setCurrentStep("skipInfo");
|
permissionWizardActions.setCurrentStep("skipInfo");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleBatteryOptimization = useCallback(async () => {
|
||||||
|
if (Platform.OS !== "android") {
|
||||||
|
permissionsActions.setBatteryOptimizationDisabled(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setBatteryOptInProgress(true);
|
||||||
|
|
||||||
|
// Check if battery optimization is enabled
|
||||||
|
const isEnabled = await BatteryOptEnabled();
|
||||||
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
console.log(
|
||||||
|
"Battery optimization is enabled, requesting to disable...",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Request to disable battery optimization (opens Android Settings)
|
||||||
|
RequestDisableOptimization();
|
||||||
|
setBatteryOptAttempted(true);
|
||||||
|
|
||||||
|
// Return false to indicate user needs to complete action in Settings
|
||||||
|
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 battery optimization FIRST (opens Android Settings)
|
||||||
|
// This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs
|
||||||
|
const batteryOptDisabled = await handleBatteryOptimization();
|
||||||
|
console.log("Battery optimization disabled:", batteryOptDisabled);
|
||||||
|
|
||||||
|
// Request motion permission second
|
||||||
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
|
// Request background location last (after user returns from Settings if needed)
|
||||||
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
|
// Only set step to tracking after all permission requests are complete
|
||||||
if (locationGranted && motionGranted) {
|
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 +256,15 @@ const HeroMode = () => {
|
||||||
"Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.",
|
"Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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 +306,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 +393,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 +419,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 +487,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>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import { initEmulatorMode } from "./emulatorService";
|
import { initEmulatorMode } from "./emulatorService";
|
||||||
|
import * as Sentry from "@sentry/react-native";
|
||||||
|
import { SPAN_STATUS_OK, SPAN_STATUS_ERROR } from "@sentry/react-native";
|
||||||
|
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
@ -32,6 +34,7 @@ const config = {
|
||||||
locationAuthorizationRequest: "Always",
|
locationAuthorizationRequest: "Always",
|
||||||
stopOnTerminate: false,
|
stopOnTerminate: false,
|
||||||
startOnBoot: true,
|
startOnBoot: true,
|
||||||
|
heartbeatInterval: 900,
|
||||||
// Force the plugin to start aggressively
|
// Force the plugin to start aggressively
|
||||||
foregroundService: true,
|
foregroundService: true,
|
||||||
notification: {
|
notification: {
|
||||||
|
@ -167,6 +170,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 +220,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 +247,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 +269,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,
|
||||||
|
@ -284,47 +330,11 @@ export default async function trackLocation() {
|
||||||
code: error.code,
|
code: error.code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a function to check for pending records
|
|
||||||
async function checkPendingRecords() {
|
|
||||||
try {
|
|
||||||
const count = await BackgroundGeolocation.getCount();
|
|
||||||
locationLogger.debug("Pending location records", { count });
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
locationLogger.info(`Found ${count} pending records, forcing sync`);
|
|
||||||
try {
|
|
||||||
const { userToken } = getAuthState();
|
|
||||||
const state = await BackgroundGeolocation.getState();
|
|
||||||
if (userToken && state.enabled) {
|
|
||||||
const records = await BackgroundGeolocation.sync();
|
|
||||||
locationLogger.debug("Forced sync result", {
|
|
||||||
recordsCount: records?.length || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
locationLogger.error("Forced sync failed", {
|
|
||||||
error: error,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
locationLogger.error("Failed to get pending records count", {
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { userToken } = getAuthState();
|
const { userToken } = getAuthState();
|
||||||
locationLogger.debug("Setting up auth state subscription");
|
locationLogger.debug("Setting up auth state subscription");
|
||||||
subscribeAuthState(({ userToken }) => userToken, handleAuth);
|
subscribeAuthState(({ userToken }) => userToken, handleAuth);
|
||||||
locationLogger.debug("Performing initial auth handling");
|
locationLogger.debug("Performing initial auth handling");
|
||||||
handleAuth(userToken);
|
handleAuth(userToken);
|
||||||
|
|
||||||
// Check for pending records after a short delay to ensure everything is initialized
|
|
||||||
setTimeout(checkPendingRecords, 5000);
|
|
||||||
|
|
||||||
// Initialize emulator mode if previously enabled
|
// Initialize emulator mode if previously enabled
|
||||||
initEmulatorMode();
|
initEmulatorMode();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,187 @@
|
||||||
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();
|
await Sentry.startSpan(
|
||||||
|
{
|
||||||
|
name: "auto-cancel-expired-notifications",
|
||||||
|
op: "background-task",
|
||||||
|
},
|
||||||
|
async (span) => {
|
||||||
|
try {
|
||||||
|
logger.info("Starting auto-cancel expired notifications task");
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Auto-cancel task started",
|
||||||
|
category: "notifications",
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get displayed notifications with timeout protection
|
||||||
|
let notifications;
|
||||||
|
await Sentry.startSpan(
|
||||||
|
{
|
||||||
|
op: "get-displayed-notifications",
|
||||||
|
description: "Getting displayed notifications",
|
||||||
|
},
|
||||||
|
async (getNotificationsSpan) => {
|
||||||
|
try {
|
||||||
|
// Add timeout protection for the API call
|
||||||
|
notifications = await Promise.race([
|
||||||
|
notifee.getDisplayedNotifications(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Timeout getting notifications")),
|
||||||
|
10000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
getNotificationsSpan.setStatus("ok");
|
||||||
|
} catch (error) {
|
||||||
|
getNotificationsSpan.setStatus("internal_error");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Array.isArray(notifications)) {
|
||||||
|
logger.warn("No notifications array received", { notifications });
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "No notifications array received",
|
||||||
|
category: "notifications",
|
||||||
|
level: "warning",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentTime = Math.round(new Date() / 1000);
|
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) {
|
for (const notification of notifications) {
|
||||||
|
try {
|
||||||
|
if (!notification || !notification.id) {
|
||||||
|
logger.warn("Invalid notification object", { notification });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const expires = notification.data?.expires;
|
const expires = notification.data?.expires;
|
||||||
if (expires && expires < currentTime) {
|
if (!expires) {
|
||||||
await notifee.cancelNotification(notification.id);
|
continue; // Skip notifications without expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expires !== "number" || expires < currentTime) {
|
||||||
|
logger.debug("Cancelling expired notification", {
|
||||||
|
notificationId: notification.id,
|
||||||
|
expires,
|
||||||
|
currentTime,
|
||||||
|
expired: expires < currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel notification with timeout protection
|
||||||
|
await Promise.race([
|
||||||
|
notifee.cancelNotification(notification.id),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Timeout cancelling notification")),
|
||||||
|
5000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cancelledCount++;
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Notification cancelled",
|
||||||
|
category: "notifications",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
notificationId: notification.id,
|
||||||
|
expires,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (notificationError) {
|
||||||
|
errorCount++;
|
||||||
|
logger.error("Failed to process notification", {
|
||||||
|
error: notificationError,
|
||||||
|
notificationId: notification?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Sentry.captureException(notificationError, {
|
||||||
|
tags: {
|
||||||
|
module: "auto-cancel-expired",
|
||||||
|
operation: "cancel-notification",
|
||||||
|
},
|
||||||
|
contexts: {
|
||||||
|
notification: {
|
||||||
|
id: notification?.id,
|
||||||
|
expires: notification?.data?.expires,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Auto-cancel task completed", {
|
||||||
|
totalNotifications: notifications.length,
|
||||||
|
cancelledCount,
|
||||||
|
errorCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
message: "Auto-cancel task completed",
|
||||||
|
category: "notifications",
|
||||||
|
level: "info",
|
||||||
|
data: {
|
||||||
|
totalNotifications: notifications.length,
|
||||||
|
cancelledCount,
|
||||||
|
errorCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
span.setStatus("ok");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Auto-cancel task failed", { error });
|
||||||
|
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: {
|
||||||
|
module: "auto-cancel-expired",
|
||||||
|
operation: "background-task",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
span.setStatus("internal_error");
|
||||||
|
throw error; // Re-throw to be handled by caller
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAutoCancelExpired = () => {
|
export const useAutoCancelExpired = () => {
|
||||||
|
@ -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 });
|
||||||
|
|
||||||
|
try {
|
||||||
await backgroundTask();
|
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);
|
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();
|
|
||||||
|
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);
|
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) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
// For battery optimization, we need to handle the async nature differently
|
||||||
|
if (
|
||||||
|
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
|
// Double-check the status to ensure UI is in sync
|
||||||
const actualStatus = await checkPermissionStatus(permission);
|
const actualStatus = await checkPermissionStatus(permission);
|
||||||
setPermissions[permission](actualStatus);
|
setPermissions[permission](actualStatus);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error requesting ${permission} permission:`, error);
|
console.error(`Error requesting ${permission} permission:`, error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,10 +73,10 @@ Sentry.init({
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.1,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
integrations: [
|
integrations: [
|
||||||
Sentry.mobileReplayIntegration({
|
// Sentry.mobileReplayIntegration({
|
||||||
maskAllText: false,
|
// maskAllText: false,
|
||||||
maskAllImages: false,
|
// maskAllImages: false,
|
||||||
maskAllVectors: false,
|
// maskAllVectors: false,
|
||||||
}),
|
// }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|