Compare commits
29 commits
Author | SHA1 | Date | |
---|---|---|---|
236121a73c | |||
cbd1803dc0 | |||
de560bd1e5 | |||
8487573c0f | |||
958eee1f72 | |||
a461f445c4 | |||
28b8b3d826 | |||
8d8da91696 | |||
d6a3e94ea7 | |||
d5ad23d1da | |||
ef9b5037fb | |||
0b5e936714 | |||
bce32dbd55 | |||
69d9fc9a6a | |||
cd17372335 | |||
ba61baf27f | |||
144ed88229 | |||
6ea01c0c6d | |||
8183c7e4af | |||
364e535a02 | |||
d8583b9ad7 | |||
a795e82bbe | |||
7220ee5667 | |||
6f628979c6 | |||
71c31eefc7 | |||
38e2f821dd | |||
5461852ada | |||
a88f3bf6c7 | |||
e6f23b83be |
39 changed files with 1262 additions and 731 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -84,6 +84,9 @@ DerivedData
|
||||||
# aidigest
|
# aidigest
|
||||||
codebase.md
|
codebase.md
|
||||||
|
|
||||||
|
# Build logs
|
||||||
|
logs/
|
||||||
|
|
||||||
# Sensitive configuration files
|
# Sensitive configuration files
|
||||||
ios/GoogleService-Info.plist
|
ios/GoogleService-Info.plist
|
||||||
ios/AlerteSecours/GoogleService-Info.plist
|
ios/AlerteSecours/GoogleService-Info.plist
|
||||||
|
|
86
CHANGELOG.md
86
CHANGELOG.md
|
@ -2,6 +2,92 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.12.3](https://github.com/alerte-secours/as-app/compare/v1.12.2...v1.12.3) (2025-09-05)
|
||||||
|
|
||||||
|
## [1.12.2](https://github.com/alerte-secours/as-app/compare/v1.12.1...v1.12.2) (2025-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 112 ([8487573](https://github.com/alerte-secours/as-app/commit/8487573c0f8eb6656cd5825ff217efe046d65407))
|
||||||
|
|
||||||
|
## [1.12.1](https://github.com/alerte-secours/as-app/compare/v1.12.0...v1.12.1) (2025-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* menu index typo ([8d8da91](https://github.com/alerte-secours/as-app/commit/8d8da916965c1dd7feaa8b011ea854591c859e03))
|
||||||
|
* placeholder in dark theme for chat input ([d5ad23d](https://github.com/alerte-secours/as-app/commit/d5ad23d1dae521e4a99f757505adec3f23a7914c))
|
||||||
|
* **push-notif:** label "undefined à " ([ef9b503](https://github.com/alerte-secours/as-app/commit/ef9b5037fbb97fa597194760a9d3a22a04eeeeda))
|
||||||
|
* **theming:** alertes archivées buttons ([d6a3e94](https://github.com/alerte-secours/as-app/commit/d6a3e94ea710a494623a8636ccad71205094d9c6))
|
||||||
|
* typo ([0b5e936](https://github.com/alerte-secours/as-app/commit/0b5e936714054fe8647b8520d6432ab29fe2ecb7))
|
||||||
|
* **voice-message:** wip ([28b8b3d](https://github.com/alerte-secours/as-app/commit/28b8b3d826685de053ae5ff7f7931b24a64920b4))
|
||||||
|
|
||||||
|
## [1.12.0](https://github.com/alerte-secours/as-app/compare/v1.11.17...v1.12.0) (2025-08-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **heartbeat:** remove ([69d9fc9](https://github.com/alerte-secours/as-app/commit/69d9fc9a6a1383bbbf5f1b5f5641d01b2378168b))
|
||||||
|
|
||||||
|
## [1.11.17](https://github.com/alerte-secours/as-app/compare/v1.11.16...v1.11.17) (2025-07-30)
|
||||||
|
|
||||||
|
## [1.11.16](https://github.com/alerte-secours/as-app/compare/v1.11.15...v1.11.16) (2025-07-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **io:** headless ([6ea01c0](https://github.com/alerte-secours/as-app/commit/6ea01c0c6d7f3cb8dbff51138b20f3fbba5b9766))
|
||||||
|
|
||||||
|
## [1.11.15](https://github.com/alerte-secours/as-app/compare/v1.11.14...v1.11.15) (2025-07-26)
|
||||||
|
|
||||||
|
## [1.11.14](https://github.com/alerte-secours/as-app/compare/v1.11.13...v1.11.14) (2025-07-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **io:** headless + debug wip ([a795e82](https://github.com/alerte-secours/as-app/commit/a795e82bbe30a425698173156862311d0c964207))
|
||||||
|
|
||||||
|
## [1.11.13](https://github.com/alerte-secours/as-app/compare/v1.11.12...v1.11.13) (2025-07-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios-headless:** wip ([6f62897](https://github.com/alerte-secours/as-app/commit/6f628979c632be7be12bddd52a8b56b105e6cbee))
|
||||||
|
|
||||||
|
## [1.11.12](https://github.com/alerte-secours/as-app/compare/v1.11.2...v1.11.12) (2025-07-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios-headless:** big wip leap ([5461852](https://github.com/alerte-secours/as-app/commit/5461852adaac33511889d7a2466f435d953b4a7b))
|
||||||
|
* **ios:** executeHeartbeatSync with silent push notification ([38e2f82](https://github.com/alerte-secours/as-app/commit/38e2f821ddeb5b2b4ad310bf2f32b23f5ca94ea7))
|
||||||
|
|
||||||
|
## [1.11.11](https://github.com/alerte-secours/as-app/compare/v1.11.10...v1.11.11) (2025-07-23)
|
||||||
|
|
||||||
|
## [1.11.10](https://github.com/alerte-secours/as-app/compare/v1.11.9...v1.11.10) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.9](https://github.com/alerte-secours/as-app/compare/v1.11.8...v1.11.9) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.8](https://github.com/alerte-secours/as-app/compare/v1.11.7...v1.11.8) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.7](https://github.com/alerte-secours/as-app/compare/v1.11.6...v1.11.7) (2025-07-22)
|
||||||
|
|
||||||
|
## [1.11.6](https://github.com/alerte-secours/as-app/compare/v1.11.5...v1.11.6) (2025-07-21)
|
||||||
|
|
||||||
|
## [1.11.5](https://github.com/alerte-secours/as-app/compare/v1.11.4...v1.11.5) (2025-07-21)
|
||||||
|
|
||||||
|
## [1.11.4](https://github.com/alerte-secours/as-app/compare/v1.11.3...v1.11.4) (2025-07-20)
|
||||||
|
|
||||||
|
## [1.11.3](https://github.com/alerte-secours/as-app/compare/v1.11.2...v1.11.3) (2025-07-20)
|
||||||
|
|
||||||
|
## [1.11.2](https://github.com/alerte-secours/as-app/compare/v1.11.1...v1.11.2) (2025-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ios:** BGTaskSchedulerPermittedIdentifiers + prebuild wip ([e6f23b8](https://github.com/alerte-secours/as-app/commit/e6f23b83be7f30adc6385dc8f3e074521c7caf22))
|
||||||
|
|
||||||
## [1.11.1](https://github.com/alerte-secours/as-app/compare/v1.11.0...v1.11.1) (2025-07-18)
|
## [1.11.1](https://github.com/alerte-secours/as-app/compare/v1.11.0...v1.11.1) (2025-07-18)
|
||||||
|
|
||||||
## [1.11.0](https://github.com/alerte-secours/as-app/compare/v1.10.9...v1.11.0) (2025-07-12)
|
## [1.11.0](https://github.com/alerte-secours/as-app/compare/v1.10.9...v1.11.0) (2025-07-12)
|
||||||
|
|
|
@ -83,8 +83,8 @@ android {
|
||||||
applicationId 'com.alertesecours'
|
applicationId 'com.alertesecours'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 191
|
versionCode 211
|
||||||
versionName "1.11.1"
|
versionName "1.12.3"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
<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.ACTIVITY_RECOGNITION"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
||||||
<string name="expo_runtime_version">1.0.0</string>
|
<string name="expo_runtime_version">1.0.0</string>
|
||||||
|
|
||||||
<!-- Override permission message with French text -->
|
|
||||||
<string name="message_permission_required">Alerte Secours nécessite la localisation en arrière-plan pour les alertes de proximité.</string>
|
<string name="message_permission_required">Alerte Secours nécessite la localisation en arrière-plan pour les alertes de proximité.</string>
|
||||||
<string name="title_permission_required">Autorisation requise</string>
|
<string name="title_permission_required">Autorisation requise</string>
|
||||||
</resources>
|
</resources>
|
|
@ -131,12 +131,13 @@ let config = {
|
||||||
"tel",
|
"tel",
|
||||||
"telprompt",
|
"telprompt",
|
||||||
],
|
],
|
||||||
|
BGTaskSchedulerPermittedIdentifiers: [
|
||||||
|
"com.transistorsoft",
|
||||||
|
"com.transistorsoft.fetch",
|
||||||
|
"com.transistorsoft.customtask",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
UIBackgroundModes: ["location", "fetch", "processing"],
|
UIBackgroundModes: ["location", "fetch", "processing"],
|
||||||
BGTaskSchedulerPermittedIdentifiers: [
|
|
||||||
"com.transistorsoft.fetch",
|
|
||||||
"com.transistorsoft.customtask",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
|
|
255
index.js
255
index.js
|
@ -5,6 +5,8 @@ import "./warnFilter";
|
||||||
import "expo-splash-screen";
|
import "expo-splash-screen";
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
import notifee from "@notifee/react-native";
|
import notifee from "@notifee/react-native";
|
||||||
import messaging from "@react-native-firebase/messaging";
|
import messaging from "@react-native-firebase/messaging";
|
||||||
|
|
||||||
|
@ -18,9 +20,7 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
|
||||||
import onMessageReceived from "~/notifications/onMessageReceived";
|
import onMessageReceived from "~/notifications/onMessageReceived";
|
||||||
|
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import * as Sentry from "@sentry/react-native";
|
// import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||||
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
|
|
||||||
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
|
||||||
|
|
||||||
// setup notification, this have to stay in index.js
|
// setup notification, this have to stay in index.js
|
||||||
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
||||||
|
@ -31,239 +31,28 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
|
||||||
// the environment is set up appropriately
|
// the environment is set up appropriately
|
||||||
registerRootComponent(App);
|
registerRootComponent(App);
|
||||||
|
|
||||||
// Constants for persistence
|
|
||||||
const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
|
|
||||||
// const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
|
|
||||||
|
|
||||||
// Helper functions for persisting sync time
|
|
||||||
const getLastSyncTime = async () => {
|
|
||||||
try {
|
|
||||||
const value = await memoryAsyncStorage.getItem(
|
|
||||||
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
|
||||||
);
|
|
||||||
return value ? parseInt(value, 10) : Date.now();
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: { module: "headless-task", operation: "get-last-sync-time" },
|
|
||||||
});
|
|
||||||
return Date.now();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLastSyncTime = async (time) => {
|
|
||||||
try {
|
|
||||||
await memoryAsyncStorage.setItem(
|
|
||||||
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
|
||||||
time.toString(),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: { module: "headless-task", operation: "set-last-sync-time" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// this have to stay in index.js, see also https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
|
||||||
const getCurrentPosition = () => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Add timeout protection
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
resolve({ code: -1, message: "getCurrentPosition timeout" });
|
|
||||||
}, 15000); // 15 second timeout
|
|
||||||
|
|
||||||
BackgroundGeolocation.getCurrentPosition(
|
|
||||||
{
|
|
||||||
samples: 1,
|
|
||||||
persist: true,
|
|
||||||
extras: { background: true },
|
|
||||||
timeout: 10, // 10 second timeout in the plugin itself
|
|
||||||
},
|
|
||||||
(location) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(location);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const geolocBgLogger = createLogger({
|
const geolocBgLogger = createLogger({
|
||||||
service: "background-geolocation",
|
service: "background-geolocation",
|
||||||
task: "headless",
|
task: "headless",
|
||||||
});
|
});
|
||||||
|
|
||||||
const HeadlessTask = async (event) => {
|
// const HeadlessTask = async (event) => {
|
||||||
// Add timeout protection for the entire headless task
|
// try {
|
||||||
const taskTimeout = setTimeout(() => {
|
// switch (event?.name) {
|
||||||
geolocBgLogger.error("HeadlessTask timeout", { event });
|
// case "heartbeat":
|
||||||
|
// await executeHeartbeatSync();
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// geolocBgLogger.error("HeadlessTask error", {
|
||||||
|
// error,
|
||||||
|
// event,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
Sentry.captureException(new Error("HeadlessTask timeout"), {
|
// if (Platform.OS === "android") {
|
||||||
tags: {
|
// BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
||||||
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 {
|
|
||||||
// Validate event structure
|
|
||||||
if (!event || typeof event !== "object") {
|
|
||||||
throw new Error("Invalid event object received");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, params } = event;
|
|
||||||
|
|
||||||
if (!name || typeof name !== "string") {
|
|
||||||
throw new Error("Invalid event name received");
|
|
||||||
}
|
|
||||||
|
|
||||||
geolocBgLogger.info("HeadlessTask event received", { name, params });
|
|
||||||
|
|
||||||
switch (name) {
|
|
||||||
case "heartbeat":
|
|
||||||
// Get persisted last sync time
|
|
||||||
const lastSyncTime = await getLastSyncTime();
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastSync = now - lastSyncTime;
|
|
||||||
|
|
||||||
// Get current position with performance tracking
|
|
||||||
const location = await getCurrentPosition();
|
|
||||||
|
|
||||||
geolocBgLogger.debug("getCurrentPosition result", { location });
|
|
||||||
|
|
||||||
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
|
||||||
geolocBgLogger.info("Forcing location sync");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Change pace to ensure location updates with timeout
|
|
||||||
await Promise.race([
|
|
||||||
BackgroundGeolocation.changePace(true),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("changePace timeout")),
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Perform sync with timeout
|
|
||||||
const syncResult = await Promise.race([
|
|
||||||
BackgroundGeolocation.sync(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("sync timeout")), 20000),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update last sync time after successful sync
|
|
||||||
await setLastSyncTime(now);
|
|
||||||
} catch (syncError) {
|
|
||||||
Sentry.captureException(syncError, {
|
|
||||||
tags: {
|
|
||||||
module: "headless-task",
|
|
||||||
operation: "force-sync",
|
|
||||||
eventName: name,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
syncAttempt: {
|
|
||||||
timeSinceLastSync: timeSinceLastSync,
|
|
||||||
lastSyncTime: new Date(lastSyncTime).toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.error("Force sync failed", { error: syncError });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "location":
|
|
||||||
// Validate location parameters
|
|
||||||
if (!params || typeof params !== "object") {
|
|
||||||
geolocBgLogger.warn("Invalid location params", { params });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
geolocBgLogger.debug("Location update received", {
|
|
||||||
location: params.location,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "http":
|
|
||||||
// Validate HTTP parameters
|
|
||||||
if (!params || typeof params !== "object" || !params.response) {
|
|
||||||
geolocBgLogger.warn("Invalid HTTP params", { params });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpStatus = params.response?.status;
|
|
||||||
const isHttpSuccess = httpStatus === 200;
|
|
||||||
|
|
||||||
geolocBgLogger.debug("HTTP response received", {
|
|
||||||
response: params.response,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last sync time on successful HTTP response
|
|
||||||
if (isHttpSuccess) {
|
|
||||||
try {
|
|
||||||
const now = Date.now();
|
|
||||||
await setLastSyncTime(now);
|
|
||||||
} catch (syncTimeError) {
|
|
||||||
geolocBgLogger.error("Failed to update sync time", {
|
|
||||||
error: syncTimeError,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(syncTimeError, {
|
|
||||||
tags: {
|
|
||||||
module: "headless-task",
|
|
||||||
operation: "update-sync-time-http",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task completed successfully
|
|
||||||
const taskDuration = Date.now() - taskStartTime;
|
|
||||||
} catch (error) {
|
|
||||||
const taskDuration = Date.now() - taskStartTime;
|
|
||||||
|
|
||||||
// Capture any unexpected errors
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "headless-task",
|
|
||||||
eventName: event?.name || "unknown",
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
duration: taskDuration,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.error("HeadlessTask error", {
|
|
||||||
error,
|
|
||||||
event,
|
|
||||||
duration: taskDuration,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Clear the timeout
|
|
||||||
clearTimeout(taskTimeout);
|
|
||||||
|
|
||||||
const finalDuration = Date.now() - taskStartTime;
|
|
||||||
geolocBgLogger.debug("HeadlessTask completed", {
|
|
||||||
event: event?.name,
|
|
||||||
duration: finalDuration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
|
||||||
|
|
|
@ -158,9 +158,17 @@
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||||
6C173A6450034E8CAB58FB0C /* Fix Xcode 15 Bug */,
|
6C173A6450034E8CAB58FB0C /* Fix Xcode 15 Bug */,
|
||||||
CBB0E03A64A84F6FB794EDB2 /* Upload Debug Symbols to Sentry */,
|
CBB0E03A64A84F6FB794EDB2 /* Upload Debug Symbols to Sentry */,
|
||||||
6EA5AE3725914306AC3A5BE5 /* Remove signature files (Xcode workaround) */,
|
|
||||||
8A20F54D80BCC2E27CF783AE /* [CP] Embed Pods Frameworks */,
|
8A20F54D80BCC2E27CF783AE /* [CP] Embed Pods Frameworks */,
|
||||||
36EBB336DD5343908AA35FFC /* [CP-User] [RNFB] Core Configuration */,
|
36EBB336DD5343908AA35FFC /* [CP-User] [RNFB] Core Configuration */,
|
||||||
|
8EC12A68941D40E98E0D60BE /* Fix Xcode 15 Bug */,
|
||||||
|
49AEAB1D332B45ED9A37B009 /* Fix Xcode 15 Bug */,
|
||||||
|
D75A41050AB3445786799848 /* Fix Xcode 15 Bug */,
|
||||||
|
FB7FA195D27D412AA897F419 /* Fix Xcode 15 Bug */,
|
||||||
|
0C44FF6DBD8F4BDD8D2B9784 /* Fix Xcode 15 Bug */,
|
||||||
|
AC008438EEF4422BA1C35CDF /* Fix Xcode 15 Bug */,
|
||||||
|
1A6C945D28C14747A29A3560 /* Fix Xcode 15 Bug */,
|
||||||
|
1C287A64431A4C0A859F067B /* Fix Xcode 15 Bug */,
|
||||||
|
EBD8BAB94522461484E3792D /* Remove signature files (Xcode workaround) */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -473,6 +481,278 @@
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
|
shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";
|
||||||
};
|
};
|
||||||
|
8EC12A68941D40E98E0D60BE /* 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";
|
||||||
|
};
|
||||||
|
700E335CAFA54AABB46BAB62 /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
49AEAB1D332B45ED9A37B009 /* 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";
|
||||||
|
};
|
||||||
|
E611D841DFAE4F71B8077AD3 /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
D75A41050AB3445786799848 /* 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";
|
||||||
|
};
|
||||||
|
ABC6C5A0D48A4B7980D60E1B /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
FB7FA195D27D412AA897F419 /* 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";
|
||||||
|
};
|
||||||
|
B1FDDB484A8E497F9FF7F32C /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
0C44FF6DBD8F4BDD8D2B9784 /* 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";
|
||||||
|
};
|
||||||
|
658BC0C976C44270ACBDF3C6 /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
AC008438EEF4422BA1C35CDF /* 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";
|
||||||
|
};
|
||||||
|
6743177E81F94D198E926A21 /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
1A6C945D28C14747A29A3560 /* 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";
|
||||||
|
};
|
||||||
|
96170835D29D4D569C60B051 /* 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\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
1C287A64431A4C0A859F067B /* 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";
|
||||||
|
};
|
||||||
|
EBD8BAB94522461484E3792D /* 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 */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
@ -516,7 +796,7 @@
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.alertesecours.alertesecours;
|
PRODUCT_BUNDLE_IDENTIFIER = com.alertesecours.alertesecours;
|
||||||
PRODUCT_NAME = AlerteSecours;
|
PRODUCT_NAME = "AlerteSecours";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "AlerteSecours/AlerteSecours-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "AlerteSecours/AlerteSecours-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
@ -535,7 +815,7 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 2PZ49Y23LX;
|
DEVELOPMENT_TEAM = 2PZ49Y23LX;
|
||||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64";
|
||||||
INFOPLIST_FILE = AlerteSecours/Info.plist;
|
INFOPLIST_FILE = AlerteSecours/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
@ -547,7 +827,7 @@
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.alertesecours.alertesecours;
|
PRODUCT_BUNDLE_IDENTIFIER = com.alertesecours.alertesecours;
|
||||||
PRODUCT_NAME = AlerteSecours;
|
PRODUCT_NAME = "AlerteSecours";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "AlerteSecours/AlerteSecours-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "AlerteSecours/AlerteSecours-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "1"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 62 KiB |
|
@ -2,6 +2,12 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>com.transistorsoft</string>
|
||||||
|
<string>com.transistorsoft.fetch</string>
|
||||||
|
<string>com.transistorsoft.customtask</string>
|
||||||
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
@ -19,7 +25,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.11.1</string>
|
<string>1.12.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
@ -42,7 +48,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>191</string>
|
<string>211</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
@ -89,23 +95,14 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>alertesecours.fr</key>
|
<key>alertesecours.fr</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSIncludesSubdomains</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSExceptionRequiresForwardSecrecy</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSExceptionMinimumTLSVersion</key>
|
<key>NSExceptionMinimumTLSVersion</key>
|
||||||
<string>TLSv1.0</string>
|
<string>TLSv1.0</string>
|
||||||
</dict>
|
<key>NSExceptionRequiresForwardSecrecy</key>
|
||||||
<key>sentry.io</key>
|
<false/>
|
||||||
<dict>
|
|
||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExceptionRequiresForwardSecrecy</key>
|
|
||||||
<false/>
|
|
||||||
<key>NSExceptionMinimumTLSVersion</key>
|
|
||||||
<string>TLSv1.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>localhost</key>
|
<key>localhost</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -114,6 +111,15 @@
|
||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>sentry.io</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionMinimumTLSVersion</key>
|
||||||
|
<string>TLSv1.0</string>
|
||||||
|
<key>NSExceptionRequiresForwardSecrecy</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
|
@ -145,6 +151,7 @@
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>location</string>
|
<string>location</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
|
22
ios/RNBackgroundFetch+AppDelegate.m
Normal file
22
ios/RNBackgroundFetch+AppDelegate.m
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// RNBackgroundGeolocation+AppDelegate.m
|
||||||
|
// RNBackgroundGeolocationSample
|
||||||
|
//
|
||||||
|
// Created by Christopher Scott on 2016-08-01.
|
||||||
|
// Copyright © 2016 Facebook. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
#import <TSBackgroundFetch/TSBackgroundFetch.h>
|
||||||
|
|
||||||
|
@implementation AppDelegate(AppDelegate)
|
||||||
|
|
||||||
|
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
|
||||||
|
{
|
||||||
|
NSLog(@"RNBackgroundFetch AppDelegate received fetch event");
|
||||||
|
TSBackgroundFetch *fetchManager = [TSBackgroundFetch sharedInstance];
|
||||||
|
[fetchManager performFetchWithCompletionHandler:completionHandler applicationState:application.applicationState];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "alerte-secours",
|
"name": "alerte-secours",
|
||||||
"version": "1.11.1",
|
"version": "1.12.3",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
"screenshot:android": "scripts/screenshot-android.sh"
|
"screenshot:android": "scripts/screenshot-android.sh"
|
||||||
},
|
},
|
||||||
"customExpoVersioning": {
|
"customExpoVersioning": {
|
||||||
"versionCode": 191,
|
"versionCode": 211,
|
||||||
"buildNumber": 191
|
"buildNumber": 211
|
||||||
},
|
},
|
||||||
"commit-and-tag-version": {
|
"commit-and-tag-version": {
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -86,6 +86,9 @@ mv ios/main.jsbundle.hbc ios/main.jsbundle
|
||||||
|
|
||||||
cd ios
|
cd ios
|
||||||
|
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
mkdir -p ../logs
|
||||||
|
|
||||||
# Create archive using xcodebuild
|
# Create archive using xcodebuild
|
||||||
echo "Creating archive..."
|
echo "Creating archive..."
|
||||||
xcodebuild \
|
xcodebuild \
|
||||||
|
@ -93,6 +96,6 @@ xcodebuild \
|
||||||
-scheme AlerteSecours \
|
-scheme AlerteSecours \
|
||||||
-configuration Release \
|
-configuration Release \
|
||||||
-archivePath AlerteSecours.xcarchive \
|
-archivePath AlerteSecours.xcarchive \
|
||||||
archive
|
archive 2>&1 | tee "../logs/ios-archive-$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
|
||||||
echo "Archive completed successfully at AlerteSecours.xcarchive"
|
echo "Archive completed successfully at AlerteSecours.xcarchive"
|
||||||
|
|
|
@ -25,6 +25,8 @@ import { useUpdates } from "~/updates";
|
||||||
import Error from "~/components/Error";
|
import Error from "~/components/Error";
|
||||||
|
|
||||||
import useTrackLocation from "~/hooks/useTrackLocation";
|
import useTrackLocation from "~/hooks/useTrackLocation";
|
||||||
|
// import { initializeBackgroundFetch } from "~/services/backgroundFetch";
|
||||||
|
import useMount from "~/hooks/useMount";
|
||||||
|
|
||||||
const appLogger = createLogger({
|
const appLogger = createLogger({
|
||||||
module: SYSTEM_SCOPES.APP,
|
module: SYSTEM_SCOPES.APP,
|
||||||
|
@ -219,6 +221,23 @@ function AppContent() {
|
||||||
useNetworkListener();
|
useNetworkListener();
|
||||||
useTrackLocation();
|
useTrackLocation();
|
||||||
|
|
||||||
|
// useMount(() => {
|
||||||
|
// const setupBackgroundFetch = async () => {
|
||||||
|
// try {
|
||||||
|
// appLogger.info("Setting up BackgroundFetch");
|
||||||
|
// await initializeBackgroundFetch();
|
||||||
|
// appLogger.debug("BackgroundFetch setup completed");
|
||||||
|
// } catch (error) {
|
||||||
|
// lifecycleLogger.error("BackgroundFetch setup failed", {
|
||||||
|
// error: error?.message,
|
||||||
|
// });
|
||||||
|
// errorHandler(error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// setupBackgroundFetch();
|
||||||
|
// });
|
||||||
|
|
||||||
// Handle deep links after app is initialized with error handling
|
// Handle deep links after app is initialized with error handling
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let subscription;
|
let subscription;
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default React.memo(function TextArea({
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
showSoftInputOnFocus={keyboardEnabled} // controlled by state
|
showSoftInputOnFocus={keyboardEnabled} // controlled by state
|
||||||
placeholder="Message"
|
placeholder="Message"
|
||||||
placeholderTextColor={colors.onBackgroundDisabled}
|
placeholderTextColor={colors.placeholder}
|
||||||
onTouchStart={() => {
|
onTouchStart={() => {
|
||||||
if (!keyboardEnabled) {
|
if (!keyboardEnabled) {
|
||||||
setKeyboardEnabled(true);
|
setKeyboardEnabled(true);
|
||||||
|
|
|
@ -38,16 +38,16 @@ const recordingSettings = {
|
||||||
outputFormat: AndroidOutputFormat.MPEG_4,
|
outputFormat: AndroidOutputFormat.MPEG_4,
|
||||||
audioEncoder: AndroidAudioEncoder.AAC,
|
audioEncoder: AndroidAudioEncoder.AAC,
|
||||||
sampleRate: 44100,
|
sampleRate: 44100,
|
||||||
numberOfChannels: 2,
|
numberOfChannels: 1,
|
||||||
bitRate: 128000,
|
bitRate: 64000,
|
||||||
},
|
},
|
||||||
ios: {
|
ios: {
|
||||||
extension: ".m4a",
|
extension: ".m4a",
|
||||||
outputFormat: IOSOutputFormat.MPEG4AAC,
|
outputFormat: IOSOutputFormat.MPEG4AAC,
|
||||||
audioQuality: IOSAudioQuality.MAX,
|
audioQuality: IOSAudioQuality.MAX,
|
||||||
sampleRate: 44100,
|
sampleRate: 44100,
|
||||||
numberOfChannels: 2,
|
numberOfChannels: 1,
|
||||||
bitRate: 128000,
|
bitRate: 64000,
|
||||||
linearPCMBitDepth: 16,
|
linearPCMBitDepth: 16,
|
||||||
linearPCMIsBigEndian: false,
|
linearPCMIsBigEndian: false,
|
||||||
linearPCMIsFloat: false,
|
linearPCMIsFloat: false,
|
||||||
|
@ -194,7 +194,7 @@ export default React.memo(function ChatInput({
|
||||||
staysActiveInBackground: true,
|
staysActiveInBackground: true,
|
||||||
});
|
});
|
||||||
const { sound: _sound } = await recording.createNewLoadedSoundAsync({
|
const { sound: _sound } = await recording.createNewLoadedSoundAsync({
|
||||||
isLooping: true,
|
isLooping: false,
|
||||||
isMuted: false,
|
isMuted: false,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
rate: 1.0,
|
rate: 1.0,
|
||||||
|
@ -205,13 +205,12 @@ export default React.memo(function ChatInput({
|
||||||
|
|
||||||
const uploadAudio = useCallback(async () => {
|
const uploadAudio = useCallback(async () => {
|
||||||
const uri = recording.getURI();
|
const uri = recording.getURI();
|
||||||
const filetype = uri.split(".").pop();
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("data[alertId]", alertId);
|
fd.append("data[alertId]", alertId);
|
||||||
fd.append("data[file]", {
|
fd.append("data[file]", {
|
||||||
uri,
|
uri,
|
||||||
type: `audio/${filetype}`,
|
type: "audio/mp4",
|
||||||
name: "audioRecord",
|
name: "audioRecord.m4a",
|
||||||
});
|
});
|
||||||
await network.oaFilesKy.post("audio/upload", {
|
await network.oaFilesKy.post("audio/upload", {
|
||||||
body: fd,
|
body: fd,
|
||||||
|
|
|
@ -24,7 +24,13 @@ export default function MessageRow({
|
||||||
const { contentType, text, audioFileUuid, userId, createdAt, username } = row;
|
const { contentType, text, audioFileUuid, userId, createdAt, username } = row;
|
||||||
|
|
||||||
const audioFileUri =
|
const audioFileUri =
|
||||||
contentType === "audio" ? `${env.MINIO_URL}/audio/${audioFileUuid}` : null;
|
contentType === "audio"
|
||||||
|
? `${env.MINIO_URL}/audio/${audioFileUuid}.m4a`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// if (contentType === "audio" && __DEV__) {
|
||||||
|
// console.log(`[MessageRow] Audio URL: ${audioFileUri}`);
|
||||||
|
// }
|
||||||
|
|
||||||
const isMine = userId === sessionUserId;
|
const isMine = userId === sessionUserId;
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,13 @@ import {
|
||||||
import { createStyles, useTheme } from "~/theme";
|
import { createStyles, useTheme } from "~/theme";
|
||||||
import openSettings from "~/lib/native/openSettings";
|
import openSettings from "~/lib/native/openSettings";
|
||||||
import {
|
import {
|
||||||
RequestDisableOptimization,
|
requestBatteryOptimizationExemption,
|
||||||
BatteryOptEnabled,
|
isBatteryOptimizationEnabled,
|
||||||
} from "react-native-battery-optimization-check";
|
openBatteryOptimizationFallbacks,
|
||||||
|
} from "~/lib/native/batteryOptimization";
|
||||||
|
|
||||||
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
||||||
|
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
|
||||||
import requestPermissionMotion from "~/permissions/requestPermissionMotion";
|
import requestPermissionMotion from "~/permissions/requestPermissionMotion";
|
||||||
import CustomButton from "~/components/CustomButton";
|
import CustomButton from "~/components/CustomButton";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
|
@ -45,6 +47,8 @@ const HeroMode = () => {
|
||||||
useState(null);
|
useState(null);
|
||||||
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
|
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
|
||||||
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
|
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
|
||||||
|
const [batteryOptFallbackOpened, setBatteryOptFallbackOpened] =
|
||||||
|
useState(false);
|
||||||
const permissions = usePermissionsState([
|
const permissions = usePermissionsState([
|
||||||
"locationBackground",
|
"locationBackground",
|
||||||
"motion",
|
"motion",
|
||||||
|
@ -75,16 +79,14 @@ const HeroMode = () => {
|
||||||
setBatteryOptInProgress(true);
|
setBatteryOptInProgress(true);
|
||||||
|
|
||||||
// Check if battery optimization is enabled
|
// Check if battery optimization is enabled
|
||||||
const isEnabled = await BatteryOptEnabled();
|
const isEnabled = await isBatteryOptimizationEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
console.log(
|
console.log("Battery optimization is enabled, requesting exemption...");
|
||||||
"Battery optimization is enabled, requesting to disable...",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Request to disable battery optimization (opens Android Settings)
|
// Request to disable battery optimization (opens Android Settings)
|
||||||
RequestDisableOptimization();
|
await requestBatteryOptimizationExemption();
|
||||||
setBatteryOptAttempted(true);
|
setBatteryOptAttempted(true);
|
||||||
|
|
||||||
// Return false to indicate user needs to complete action in Settings
|
// Return false to indicate user needs to complete action in Settings
|
||||||
|
@ -106,31 +108,45 @@ const HeroMode = () => {
|
||||||
const handleRequestPermissions = useCallback(async () => {
|
const handleRequestPermissions = useCallback(async () => {
|
||||||
setRequesting(true);
|
setRequesting(true);
|
||||||
try {
|
try {
|
||||||
// Don't change step immediately to avoid race conditions
|
|
||||||
console.log("Starting permission requests...");
|
console.log("Starting permission requests...");
|
||||||
|
|
||||||
// Request battery optimization FIRST (opens Android Settings)
|
// 1) Battery optimization (opens Settings)
|
||||||
// This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs
|
|
||||||
const batteryOptDisabled = await handleBatteryOptimization();
|
const batteryOptDisabled = await handleBatteryOptimization();
|
||||||
console.log("Battery optimization disabled:", batteryOptDisabled);
|
console.log("Battery optimization disabled:", batteryOptDisabled);
|
||||||
|
if (!batteryOptDisabled) {
|
||||||
|
// Settings flow opened; wait for user to return before requesting in-app permissions
|
||||||
|
setRequesting(false);
|
||||||
|
setHasAttempted(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Request motion permission second
|
// 2) Foreground location
|
||||||
|
let fgGranted = await requestPermissionLocationForeground();
|
||||||
|
permissionsActions.setLocationForeground(fgGranted);
|
||||||
|
console.log("Location foreground permission:", fgGranted);
|
||||||
|
|
||||||
|
// 3) Background location (only after FG granted)
|
||||||
|
let bgGranted = false;
|
||||||
|
if (fgGranted) {
|
||||||
|
bgGranted = await requestPermissionLocationBackground();
|
||||||
|
permissionsActions.setLocationBackground(bgGranted);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"Skipping background location since foreground not granted",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("Location background permission:", bgGranted);
|
||||||
|
|
||||||
|
// 4) Motion
|
||||||
const motionGranted = await requestPermissionMotion.requestPermission();
|
const motionGranted = await requestPermissionMotion.requestPermission();
|
||||||
permissionsActions.setMotion(motionGranted);
|
permissionsActions.setMotion(motionGranted);
|
||||||
console.log("Motion permission:", motionGranted);
|
console.log("Motion permission:", motionGranted);
|
||||||
|
|
||||||
// Request background location last (after user returns from Settings if needed)
|
// Step after all requests
|
||||||
const locationGranted = await requestPermissionLocationBackground();
|
|
||||||
permissionsActions.setLocationBackground(locationGranted);
|
|
||||||
console.log("Location background permission:", locationGranted);
|
|
||||||
|
|
||||||
// Only set step to tracking after all permission requests are complete
|
|
||||||
permissionWizardActions.setCurrentStep("tracking");
|
permissionWizardActions.setCurrentStep("tracking");
|
||||||
|
|
||||||
// Check if we should proceed to success immediately
|
if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) {
|
||||||
if (locationGranted && motionGranted && batteryOptDisabled) {
|
|
||||||
permissionWizardActions.setHeroPermissionsGranted(true);
|
permissionWizardActions.setHeroPermissionsGranted(true);
|
||||||
// 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);
|
||||||
|
@ -143,7 +159,7 @@ const HeroMode = () => {
|
||||||
// Re-check battery optimization status before retrying
|
// Re-check battery optimization status before retrying
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
try {
|
try {
|
||||||
const isEnabled = await BatteryOptEnabled();
|
const isEnabled = await isBatteryOptimizationEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
// If battery optimization is now disabled, update the store
|
// If battery optimization is now disabled, update the store
|
||||||
|
@ -179,7 +195,7 @@ const HeroMode = () => {
|
||||||
const checkInitialBatteryOptimization = async () => {
|
const checkInitialBatteryOptimization = async () => {
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
try {
|
try {
|
||||||
const isEnabled = await BatteryOptEnabled();
|
const isEnabled = await isBatteryOptimizationEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
// If already disabled, update the store
|
// If already disabled, update the store
|
||||||
|
@ -208,7 +224,7 @@ const HeroMode = () => {
|
||||||
) {
|
) {
|
||||||
console.log("App became active, re-checking battery optimization...");
|
console.log("App became active, re-checking battery optimization...");
|
||||||
try {
|
try {
|
||||||
const isEnabled = await BatteryOptEnabled();
|
const isEnabled = await isBatteryOptimizationEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
|
@ -216,6 +232,19 @@ const HeroMode = () => {
|
||||||
"Battery optimization disabled after returning from settings",
|
"Battery optimization disabled after returning from settings",
|
||||||
);
|
);
|
||||||
permissionsActions.setBatteryOptimizationDisabled(true);
|
permissionsActions.setBatteryOptimizationDisabled(true);
|
||||||
|
} else if (!batteryOptFallbackOpened) {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
"Battery optimization still enabled; opening fallback settings...",
|
||||||
|
);
|
||||||
|
await openBatteryOptimizationFallbacks();
|
||||||
|
setBatteryOptFallbackOpened(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
"Error opening battery optimization fallback settings:",
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -234,7 +263,7 @@ const HeroMode = () => {
|
||||||
return () => {
|
return () => {
|
||||||
subscription?.remove();
|
subscription?.remove();
|
||||||
};
|
};
|
||||||
}, [batteryOptAttempted]);
|
}, [batteryOptAttempted, batteryOptFallbackOpened]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAttempted && allGranted) {
|
if (hasAttempted && allGranted) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as Location from "expo-location";
|
import * as Location from "expo-location";
|
||||||
import { useState, useRef, useEffect, useCallback } from "react";
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { storeLocation, getStoredLocation } from "~/utils/location/storage";
|
import { storeLocation, getStoredLocation } from "~/location/storage";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@ class AudioSlider extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
mapAudioToCurrentTime = async () => {
|
mapAudioToCurrentTime = async () => {
|
||||||
|
if (!this.soundObject) return;
|
||||||
await this.soundObject.setPositionAsync(this.state.currentTime);
|
await this.soundObject.setPositionAsync(this.state.currentTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,6 +123,7 @@ class AudioSlider extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
play = async () => {
|
play = async () => {
|
||||||
|
if (!this.soundObject) return;
|
||||||
if (this.registry && this.pauseAllBeforePlay) {
|
if (this.registry && this.pauseAllBeforePlay) {
|
||||||
const players = this.registry.getAll();
|
const players = this.registry.getAll();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -134,12 +136,14 @@ class AudioSlider extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
pause = async () => {
|
pause = async () => {
|
||||||
|
if (!this.soundObject) return;
|
||||||
await this.soundObject.pauseAsync();
|
await this.soundObject.pauseAsync();
|
||||||
this.setState({ playing: false }); // This is for the play-button to go to pause
|
this.setState({ playing: false }); // This is for the play-button to go to pause
|
||||||
Animated.timing(this.state.dotOffset, { useNativeDriver: false }).stop(); // Will also call animationPausedOrStopped()
|
Animated.timing(this.state.dotOffset, { useNativeDriver: false }).stop(); // Will also call animationPausedOrStopped()
|
||||||
};
|
};
|
||||||
|
|
||||||
startMovingDot = async () => {
|
startMovingDot = async () => {
|
||||||
|
if (!this.soundObject) return;
|
||||||
const status = await this.soundObject.getStatusAsync();
|
const status = await this.soundObject.getStatusAsync();
|
||||||
const durationLeft = status["durationMillis"] - status["positionMillis"];
|
const durationLeft = status["durationMillis"] - status["positionMillis"];
|
||||||
|
|
||||||
|
@ -156,6 +160,7 @@ class AudioSlider extends PureComponent {
|
||||||
// Audio has been paused
|
// Audio has been paused
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.soundObject) return;
|
||||||
// Animation-duration is over (reset Animation and Audio):
|
// Animation-duration is over (reset Animation and Audio):
|
||||||
await sleep(200); // In case animation has finished, but audio has not
|
await sleep(200); // In case animation has finished, but audio has not
|
||||||
this.setState({ playing: false });
|
this.setState({ playing: false });
|
||||||
|
@ -164,6 +169,16 @@ class AudioSlider extends PureComponent {
|
||||||
await this.soundObject.setPositionAsync(0);
|
await this.soundObject.setPositionAsync(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handlePlaybackFinished = async () => {
|
||||||
|
// console.log(`[AudioSlider] Playback finished, resetting for replay`);
|
||||||
|
// Reset for replay instead of unloading
|
||||||
|
this.setState({ playing: false });
|
||||||
|
await this.state.dotOffset.setValue({ x: 0, y: 0 });
|
||||||
|
if (this.soundObject) {
|
||||||
|
await this.soundObject.stopAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
measureTrack = (event) => {
|
measureTrack = (event) => {
|
||||||
this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height}
|
this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height}
|
||||||
};
|
};
|
||||||
|
@ -171,27 +186,92 @@ class AudioSlider extends PureComponent {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
// https://github.com/olapiv/expo-audio-player/issues/13
|
// https://github.com/olapiv/expo-audio-player/issues/13
|
||||||
|
|
||||||
const loadAudio = async () => {
|
const audioUrl = this.props.audio;
|
||||||
try {
|
|
||||||
const { sound: newSound } = await Audio.Sound.createAsync({
|
|
||||||
uri: this.props.audio,
|
|
||||||
});
|
|
||||||
this.soundObject = newSound;
|
|
||||||
|
|
||||||
// // https://github.com/expo/expo/issues/1873
|
const loadAudio = async () => {
|
||||||
|
const tryLoad = async (ext) => {
|
||||||
|
// console.log(`[AudioSlider] Attempting to load with extension: ${ext}`);
|
||||||
|
const { sound } = await Audio.Sound.createAsync({
|
||||||
|
uri: audioUrl,
|
||||||
|
overrideFileExtensionAndroid: ext,
|
||||||
|
});
|
||||||
|
return sound;
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First try with m4a (preferred)
|
||||||
|
const sound = await tryLoad("m4a");
|
||||||
|
// console.log(`[AudioSlider] Successfully loaded with m4a extension`);
|
||||||
|
this.soundObject = sound;
|
||||||
|
await this.soundObject.setIsLoopingAsync(false);
|
||||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||||
if (!status.didJustFinish) return;
|
if (!status.didJustFinish) return;
|
||||||
this.soundObject.unloadAsync().catch(() => {});
|
this.handlePlaybackFinished();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
return;
|
||||||
console.log("Error loading audio:", error);
|
} catch (err1) {
|
||||||
|
// console.log(`[AudioSlider] Failed to load with m4a:`, err1.message);
|
||||||
|
lastError = err1;
|
||||||
|
try {
|
||||||
|
// Fallback to mp4
|
||||||
|
const sound = await tryLoad("mp4");
|
||||||
|
// console.log(`[AudioSlider] Successfully loaded with mp4 extension`);
|
||||||
|
this.soundObject = sound;
|
||||||
|
await this.soundObject.setIsLoopingAsync(false);
|
||||||
|
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||||
|
if (!status.didJustFinish) return;
|
||||||
|
this.handlePlaybackFinished();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (err2) {
|
||||||
|
// console.log(`[AudioSlider] Failed to load with mp4:`, err2.message);
|
||||||
|
lastError = err2;
|
||||||
|
try {
|
||||||
|
// Last fallback to aac
|
||||||
|
const sound = await tryLoad("aac");
|
||||||
|
// console.log(`[AudioSlider] Successfully loaded with aac extension`);
|
||||||
|
this.soundObject = sound;
|
||||||
|
await this.soundObject.setIsLoopingAsync(false);
|
||||||
|
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||||
|
if (!status.didJustFinish) return;
|
||||||
|
this.handlePlaybackFinished();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (err3) {
|
||||||
|
// console.log(`[AudioSlider] Failed to load with aac:`, err3.message);
|
||||||
|
lastError = err3;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All attempts failed
|
||||||
|
console.error(
|
||||||
|
`[AudioSlider] All load attempts failed for ${audioUrl}. Last error:`,
|
||||||
|
lastError,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAudio();
|
await loadAudio();
|
||||||
|
|
||||||
const status = await this.soundObject.getStatusAsync();
|
if (!this.soundObject) {
|
||||||
this.setState({ duration: status.durationMillis });
|
// Loading failed; avoid further calls and leave UI inert or show error
|
||||||
|
console.log(
|
||||||
|
`[AudioSlider] No sound object created, setting duration to 0`,
|
||||||
|
);
|
||||||
|
this.setState({ duration: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = await this.soundObject.getStatusAsync();
|
||||||
|
this.setState({ duration: status.durationMillis });
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error getting audio status:", error);
|
||||||
|
this.setState({ duration: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// This requires measureTrack to have been called.
|
// This requires measureTrack to have been called.
|
||||||
this.state.dotOffset.addListener(() => {
|
this.state.dotOffset.addListener(() => {
|
||||||
|
@ -207,7 +287,9 @@ class AudioSlider extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentWillUnmount() {
|
async componentWillUnmount() {
|
||||||
await this.soundObject.unloadAsync();
|
if (this.soundObject) {
|
||||||
|
await this.soundObject.unloadAsync();
|
||||||
|
}
|
||||||
this.state.dotOffset.removeAllListeners();
|
this.state.dotOffset.removeAllListeners();
|
||||||
if (this.registry) {
|
if (this.registry) {
|
||||||
this.registry.unregister(this);
|
this.registry.unregister(this);
|
||||||
|
|
97
src/lib/native/batteryOptimization.js
Normal file
97
src/lib/native/batteryOptimization.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import SendIntentAndroid from "react-native-send-intent";
|
||||||
|
import {
|
||||||
|
RequestDisableOptimization,
|
||||||
|
BatteryOptEnabled,
|
||||||
|
} from "react-native-battery-optimization-check";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const log = createLogger({
|
||||||
|
module: FEATURE_SCOPES.PERMISSIONS,
|
||||||
|
feature: "battery-optimization",
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if battery optimization is currently ENABLED for this app on Android.
|
||||||
|
* On iOS, returns false (no battery optimization concept).
|
||||||
|
*/
|
||||||
|
export async function isBatteryOptimizationEnabled() {
|
||||||
|
if (Platform.OS !== "android") return false;
|
||||||
|
try {
|
||||||
|
const enabled = await BatteryOptEnabled();
|
||||||
|
log.info("Battery optimization status", { enabled });
|
||||||
|
return enabled;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to read battery optimization status", {
|
||||||
|
error: e?.message,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
// Conservative: assume enabled if unknown
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the primary system flow to request ignoring battery optimizations.
|
||||||
|
* This opens a Settings screen; it does not yield a synchronous result.
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* - false on Android to indicate the user must complete an action in Settings
|
||||||
|
* - true on iOS (no-op)
|
||||||
|
*/
|
||||||
|
export async function requestBatteryOptimizationExemption() {
|
||||||
|
if (Platform.OS !== "android") return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Requesting to disable battery optimization (primary intent)");
|
||||||
|
// This opens the OS dialog/settings. No result is provided, handle via AppState return.
|
||||||
|
RequestDisableOptimization();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Primary request to disable battery optimization failed", {
|
||||||
|
error: e?.message,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
// Even if it throws, we'll guide users via fallbacks.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens best-effort fallback screens to let users disable battery optimization.
|
||||||
|
* Call this AFTER the user returns and status is still enabled.
|
||||||
|
*
|
||||||
|
* Strategy:
|
||||||
|
* - Try the list of battery optimization exceptions
|
||||||
|
* - Fallback to app settings
|
||||||
|
*/
|
||||||
|
export async function openBatteryOptimizationFallbacks() {
|
||||||
|
if (Platform.OS !== "android") return true;
|
||||||
|
|
||||||
|
// Try the generic battery optimization settings list
|
||||||
|
try {
|
||||||
|
log.info("Opening fallback: IGNORE_BATTERY_OPTIMIZATION_SETTINGS");
|
||||||
|
await SendIntentAndroid.openSettings(
|
||||||
|
"android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log.warn("Failed to open IGNORE_BATTERY_OPTIMIZATION_SETTINGS", {
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback: app details settings
|
||||||
|
try {
|
||||||
|
log.info("Opening fallback: APPLICATION_DETAILS_SETTINGS (app details)");
|
||||||
|
await SendIntentAndroid.openAppSettings();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Failed to open APPLICATION_DETAILS_SETTINGS", {
|
||||||
|
error: e?.message,
|
||||||
|
stack: e?.stack,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { Platform, Linking } from "react-native";
|
||||||
import RNImmediatePhoneCall from "react-native-immediate-phone-call";
|
import RNImmediatePhoneCall from "react-native-immediate-phone-call";
|
||||||
|
|
||||||
export function phoneCallEmergency() {
|
export function phoneCallEmergency() {
|
||||||
const emergencyNumber = "+112";
|
const emergencyNumber = "112";
|
||||||
|
|
||||||
if (Platform.OS === "ios") {
|
if (Platform.OS === "ios") {
|
||||||
// Use telprompt URL scheme on iOS
|
// Use telprompt URL scheme on iOS
|
||||||
|
|
296
src/location/backgroundTask.js.bak
Normal file
296
src/location/backgroundTask.js.bak
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { getStoredLocation } from "./storage";
|
||||||
|
import { getAuthState } from "~/stores";
|
||||||
|
import env from "~/env";
|
||||||
|
|
||||||
|
// Constants for persistence
|
||||||
|
const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
|
||||||
|
// const FORCE_SYNC_INTERVAL = 1 * 60 * 1000; // DEBUGGING
|
||||||
|
|
||||||
|
const geolocBgLogger = createLogger({
|
||||||
|
service: "background-task",
|
||||||
|
task: "headless",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions for persisting sync time
|
||||||
|
const getLastSyncTime = async () => {
|
||||||
|
try {
|
||||||
|
const value = await memoryAsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
||||||
|
);
|
||||||
|
return value ? parseInt(value, 10) : Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLastSyncTime = async (time) => {
|
||||||
|
try {
|
||||||
|
await memoryAsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
||||||
|
time.toString(),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// silent error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeSyncAndroid = async () => {
|
||||||
|
await BackgroundGeolocation.changePace(true);
|
||||||
|
await BackgroundGeolocation.sync();
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeSyncIOS = async () => {
|
||||||
|
const debugWebhook =
|
||||||
|
"https://webhook.site/433b6aca-b169-4073-924a-4f089ca30406";
|
||||||
|
|
||||||
|
// Helper function to send debug info
|
||||||
|
const sendDebug = async (step, data = {}) => {
|
||||||
|
try {
|
||||||
|
// Build query string manually since URLSearchParams is not available in React Native
|
||||||
|
const queryData = {
|
||||||
|
step,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...Object.entries(data).reduce((acc, [key, value]) => {
|
||||||
|
acc[key] =
|
||||||
|
typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryString = Object.entries(queryData)
|
||||||
|
.map(
|
||||||
|
([key, value]) =>
|
||||||
|
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
await fetch(`${debugWebhook}?${queryString}`, { method: "GET" });
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore debug errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Debug point 1: Function start
|
||||||
|
await sendDebug("1_function_start", { platform: "iOS" });
|
||||||
|
|
||||||
|
// Debug point 2: Before getStoredLocation
|
||||||
|
await sendDebug("2_before_get_stored_location");
|
||||||
|
|
||||||
|
const locationData = await getStoredLocation();
|
||||||
|
|
||||||
|
// Debug point 3: After getStoredLocation
|
||||||
|
await sendDebug("3_after_get_stored_location", {
|
||||||
|
hasData: !!locationData,
|
||||||
|
timestamp: locationData?.timestamp || "null",
|
||||||
|
hasCoords: !!locationData?.coords,
|
||||||
|
latitude: locationData?.coords?.latitude || "null",
|
||||||
|
longitude: locationData?.coords?.longitude || "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!locationData) {
|
||||||
|
geolocBgLogger.debug("No stored location data found, skipping sync");
|
||||||
|
await sendDebug("3a_no_location_data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timestamp, coords } = locationData;
|
||||||
|
|
||||||
|
// Check if timestamp is too old (> 2 weeks)
|
||||||
|
const now = new Date();
|
||||||
|
const locationTime = new Date(timestamp);
|
||||||
|
const twoWeeksInMs = 14 * 24 * 60 * 60 * 1000; // 2 weeks in milliseconds
|
||||||
|
const locationAge = now - locationTime;
|
||||||
|
|
||||||
|
// Debug point 4: Timestamp validation
|
||||||
|
await sendDebug("4_timestamp_validation", {
|
||||||
|
locationAge: locationAge,
|
||||||
|
maxAge: twoWeeksInMs,
|
||||||
|
isTooOld: locationAge > twoWeeksInMs,
|
||||||
|
timestamp: timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (locationAge > twoWeeksInMs) {
|
||||||
|
geolocBgLogger.debug("Stored location is too old, skipping sync", {
|
||||||
|
locationAge: locationAge,
|
||||||
|
maxAge: twoWeeksInMs,
|
||||||
|
timestamp: timestamp,
|
||||||
|
});
|
||||||
|
await sendDebug("4a_location_too_old");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get auth token
|
||||||
|
const { userToken } = getAuthState();
|
||||||
|
|
||||||
|
// Debug point 5: Auth token check
|
||||||
|
await sendDebug("5_auth_token_check", {
|
||||||
|
hasToken: !!userToken,
|
||||||
|
tokenLength: userToken ? userToken.length : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userToken) {
|
||||||
|
geolocBgLogger.debug("No auth token available, skipping sync");
|
||||||
|
await sendDebug("5a_no_auth_token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate coordinates
|
||||||
|
if (
|
||||||
|
!coords ||
|
||||||
|
typeof coords.latitude !== "number" ||
|
||||||
|
typeof coords.longitude !== "number"
|
||||||
|
) {
|
||||||
|
geolocBgLogger.error("Invalid coordinates in stored location", {
|
||||||
|
coords,
|
||||||
|
});
|
||||||
|
await sendDebug("5b_invalid_coordinates", {
|
||||||
|
hasCoords: !!coords,
|
||||||
|
latType: typeof coords?.latitude,
|
||||||
|
lonType: typeof coords?.longitude,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload according to API spec
|
||||||
|
const payload = {
|
||||||
|
location: {
|
||||||
|
event: "heartbeat",
|
||||||
|
coords: {
|
||||||
|
latitude: coords.latitude,
|
||||||
|
longitude: coords.longitude,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
geolocBgLogger.debug("Syncing location to server", {
|
||||||
|
url: env.GEOLOC_SYNC_URL,
|
||||||
|
coords: payload.location.coords,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debug point 6: Before sync request
|
||||||
|
await sendDebug("6_before_sync_request", {
|
||||||
|
url: env.GEOLOC_SYNC_URL,
|
||||||
|
latitude: payload.location.coords.latitude,
|
||||||
|
longitude: payload.location.coords.longitude,
|
||||||
|
event: payload.location.event,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
const response = await fetch(env.GEOLOC_SYNC_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${userToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
await sendDebug("7a_sync_http_error", {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
if (responseData.ok !== true) {
|
||||||
|
await sendDebug("7b_sync_api_error", {
|
||||||
|
apiOk: responseData.ok,
|
||||||
|
responseData: JSON.stringify(responseData),
|
||||||
|
});
|
||||||
|
throw new Error(`API returned ok: ${responseData.ok}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug point 7: Sync success
|
||||||
|
await sendDebug("7_sync_success", {
|
||||||
|
status: response.status,
|
||||||
|
latitude: payload.location.coords.latitude,
|
||||||
|
longitude: payload.location.coords.longitude,
|
||||||
|
});
|
||||||
|
|
||||||
|
geolocBgLogger.info("iOS location sync completed successfully", {
|
||||||
|
status: response.status,
|
||||||
|
coords: payload.location.coords,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Debug point 8: Error catch
|
||||||
|
await sendDebug("8_error_caught", {
|
||||||
|
errorMessage: error.message,
|
||||||
|
errorName: error.name,
|
||||||
|
errorStack: error.stack ? error.stack.substring(0, 500) : "no_stack",
|
||||||
|
});
|
||||||
|
|
||||||
|
geolocBgLogger.error("iOS location sync failed", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shared heartbeat logic - mutualized between Android and iOS
|
||||||
|
const executeSync = async () => {
|
||||||
|
if (Platform.OS === "ios") {
|
||||||
|
await executeSyncIOS();
|
||||||
|
} else if (Platform.OS === "android") {
|
||||||
|
await executeSyncAndroid();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const executeHeartbeatSync = async () => {
|
||||||
|
const lastSyncTime = await getLastSyncTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastSync = now - lastSyncTime;
|
||||||
|
|
||||||
|
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
||||||
|
geolocBgLogger.info("Forcing location sync", {
|
||||||
|
timeSinceLastSync,
|
||||||
|
forceInterval: FORCE_SYNC_INTERVAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const syncResult = await Promise.race([
|
||||||
|
executeSync(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("changePace timeout")), 20000),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await setLastSyncTime(now);
|
||||||
|
|
||||||
|
geolocBgLogger.info("Force sync completed successfully", {
|
||||||
|
syncResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
} catch (syncError) {
|
||||||
|
geolocBgLogger.error("Force sync failed", {
|
||||||
|
error: syncError.message,
|
||||||
|
timeSinceLastSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncPerformed: true,
|
||||||
|
syncSuccessful: false,
|
||||||
|
error: syncError.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
geolocBgLogger.debug("Sync not needed yet", {
|
||||||
|
timeSinceLastSync,
|
||||||
|
forceInterval: FORCE_SYNC_INTERVAL,
|
||||||
|
timeUntilNextSync: FORCE_SYNC_INTERVAL - timeSinceLastSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncPerformed: false,
|
||||||
|
syncSuccessful: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -8,7 +8,7 @@ import { initEmulatorMode } from "./emulatorService";
|
||||||
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
|
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
|
||||||
|
|
||||||
import setLocationState from "~/location/setLocationState";
|
import setLocationState from "~/location/setLocationState";
|
||||||
import { storeLocation } from "~/utils/location/storage";
|
import { storeLocation } from "~/location/storage";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ export default async function trackLocation() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundGeolocation.onLocation((location) => {
|
BackgroundGeolocation.onLocation(async (location) => {
|
||||||
locationLogger.debug("Location update received", {
|
locationLogger.debug("Location update received", {
|
||||||
coords: location.coords,
|
coords: location.coords,
|
||||||
timestamp: location.timestamp,
|
timestamp: location.timestamp,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
red: "Urgence vitale.\nAppeler immétiatement les secours.",
|
red: "Urgence vitale.\nAppeler immédiatement les secours.",
|
||||||
yellow: "Danger sans risque vital.\nConcerne les services d'intervention.",
|
yellow: "Danger sans risque vital.\nConcerne les services d'intervention.",
|
||||||
green:
|
green:
|
||||||
"Entre-aide citoyenne localisée.\nJ'ai besoin d'une aide rapide à proximité.",
|
"Entre-aide citoyenne localisée.\nJ'ai besoin d'une aide rapide à proximité.",
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import SectionSeparator from "./SectionSeparator";
|
import SectionSeparator from "./SectionSeparator";
|
||||||
import menuItem from "./menuItem";
|
import menuItem from "./menuItem";
|
||||||
|
|
||||||
|
const index1 = 5;
|
||||||
|
const index2 = 9;
|
||||||
|
|
||||||
export default function DrawerItemList(props) {
|
export default function DrawerItemList(props) {
|
||||||
const { state } = props;
|
const { state } = props;
|
||||||
const { index } = state;
|
const { index } = state;
|
||||||
|
@ -17,18 +20,18 @@ export default function DrawerItemList(props) {
|
||||||
|
|
||||||
const { routes } = state;
|
const { routes } = state;
|
||||||
|
|
||||||
const section1 = routes.slice(0, 5);
|
const section1 = routes.slice(0, index1);
|
||||||
const section2 = routes.slice(5, 9);
|
const section2 = routes.slice(index1, index2);
|
||||||
const section3 = routes.slice(9, routes.length);
|
const section3 = routes.slice(index2, routes.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionSeparator label="Mon compte" />
|
<SectionSeparator label="Mon compte" />
|
||||||
{section2.map((props, i) => routeMenuItem(props, i + 5))}
|
{section2.map((props, i) => routeMenuItem(props, i + index1))}
|
||||||
<SectionSeparator label="Alerter" />
|
<SectionSeparator label="Alerter" />
|
||||||
{section1.map((props, i) => routeMenuItem(props, i + 0))}
|
{section1.map((props, i) => routeMenuItem(props, i + 0))}
|
||||||
<SectionSeparator label="Infos pratiques" />
|
<SectionSeparator label="Infos pratiques" />
|
||||||
{section3.map((props, i) => routeMenuItem(props, i + 8))}
|
{section3.map((props, i) => routeMenuItem(props, i + index2))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,364 +0,0 @@
|
||||||
import notifee from "@notifee/react-native";
|
|
||||||
import BackgroundFetch from "react-native-background-fetch";
|
|
||||||
import * as Sentry from "@sentry/react-native";
|
|
||||||
|
|
||||||
import useMount from "~/hooks/useMount";
|
|
||||||
import { createLogger } from "~/lib/logger";
|
|
||||||
|
|
||||||
const logger = createLogger({
|
|
||||||
service: "notifications",
|
|
||||||
task: "auto-cancel-expired",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Background task to cancel expired notifications
|
|
||||||
const backgroundTask = async () => {
|
|
||||||
await Sentry.startSpan(
|
|
||||||
{
|
|
||||||
name: "auto-cancel-expired-notifications",
|
|
||||||
op: "background-task",
|
|
||||||
},
|
|
||||||
async (span) => {
|
|
||||||
try {
|
|
||||||
logger.info("Starting auto-cancel expired notifications task");
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auto-cancel task started",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get displayed notifications with timeout protection
|
|
||||||
let notifications;
|
|
||||||
await Sentry.startSpan(
|
|
||||||
{
|
|
||||||
op: "get-displayed-notifications",
|
|
||||||
description: "Getting displayed notifications",
|
|
||||||
},
|
|
||||||
async (getNotificationsSpan) => {
|
|
||||||
try {
|
|
||||||
// Add timeout protection for the API call
|
|
||||||
notifications = await Promise.race([
|
|
||||||
notifee.getDisplayedNotifications(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout getting notifications")),
|
|
||||||
10000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
getNotificationsSpan.setStatus("ok");
|
|
||||||
} catch (error) {
|
|
||||||
getNotificationsSpan.setStatus("internal_error");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Array.isArray(notifications)) {
|
|
||||||
logger.warn("No notifications array received", { notifications });
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "No notifications array received",
|
|
||||||
category: "notifications",
|
|
||||||
level: "warning",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTime = Math.round(new Date() / 1000);
|
|
||||||
let cancelledCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
logger.info("Processing notifications", {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
currentTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Processing notifications",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
currentTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process notifications with individual error handling
|
|
||||||
for (const notification of notifications) {
|
|
||||||
try {
|
|
||||||
if (!notification || !notification.id) {
|
|
||||||
logger.warn("Invalid notification object", { notification });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const expires = notification.data?.expires;
|
|
||||||
if (!expires) {
|
|
||||||
continue; // Skip notifications without expiry
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expires !== "number" || expires < currentTime) {
|
|
||||||
logger.debug("Cancelling expired notification", {
|
|
||||||
notificationId: notification.id,
|
|
||||||
expires,
|
|
||||||
currentTime,
|
|
||||||
expired: expires < currentTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel notification with timeout protection
|
|
||||||
await Promise.race([
|
|
||||||
notifee.cancelNotification(notification.id),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout cancelling notification")),
|
|
||||||
5000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
cancelledCount++;
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Notification cancelled",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
notificationId: notification.id,
|
|
||||||
expires,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (notificationError) {
|
|
||||||
errorCount++;
|
|
||||||
logger.error("Failed to process notification", {
|
|
||||||
error: notificationError,
|
|
||||||
notificationId: notification?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(notificationError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "cancel-notification",
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
notification: {
|
|
||||||
id: notification?.id,
|
|
||||||
expires: notification?.data?.expires,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Auto-cancel task completed", {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
cancelledCount,
|
|
||||||
errorCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message: "Auto-cancel task completed",
|
|
||||||
category: "notifications",
|
|
||||||
level: "info",
|
|
||||||
data: {
|
|
||||||
totalNotifications: notifications.length,
|
|
||||||
cancelledCount,
|
|
||||||
errorCount,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
span.setStatus("ok");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Auto-cancel task failed", { error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-task",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
span.setStatus("internal_error");
|
|
||||||
throw error; // Re-throw to be handled by caller
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAutoCancelExpired = () => {
|
|
||||||
useMount(() => {
|
|
||||||
// Initialize background fetch
|
|
||||||
BackgroundFetch.configure(
|
|
||||||
{
|
|
||||||
minimumFetchInterval: 180, // Fetch interval in minutes
|
|
||||||
stopOnTerminate: false,
|
|
||||||
startOnBoot: true,
|
|
||||||
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_NONE,
|
|
||||||
enableHeadless: true,
|
|
||||||
},
|
|
||||||
async (taskId) => {
|
|
||||||
logger.info("BackgroundFetch task started", { taskId });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await backgroundTask();
|
|
||||||
logger.info("BackgroundFetch task completed successfully", {
|
|
||||||
taskId,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("BackgroundFetch task failed", { taskId, error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-fetch-task",
|
|
||||||
taskId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// CRITICAL: Always call finish, even on error
|
|
||||||
try {
|
|
||||||
if (taskId) {
|
|
||||||
BackgroundFetch.finish(taskId);
|
|
||||||
logger.debug("BackgroundFetch task finished", { taskId });
|
|
||||||
} else {
|
|
||||||
logger.error("Cannot finish BackgroundFetch task - no taskId");
|
|
||||||
}
|
|
||||||
} catch (finishError) {
|
|
||||||
// This is a critical error - the native side might be in a bad state
|
|
||||||
logger.error("CRITICAL: BackgroundFetch.finish() failed", {
|
|
||||||
taskId,
|
|
||||||
error: finishError,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(finishError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-fetch-finish",
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
task: { taskId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
logger.error("BackgroundFetch failed to start", { error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "background-fetch-configure",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
BackgroundFetch.stop();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register headless task
|
|
||||||
BackgroundFetch.registerHeadlessTask(async (event) => {
|
|
||||||
const taskId = event?.taskId;
|
|
||||||
|
|
||||||
logger.info("Headless task started", { taskId, event });
|
|
||||||
|
|
||||||
// Add timeout protection for the entire headless task
|
|
||||||
const taskTimeout = setTimeout(() => {
|
|
||||||
logger.error("Headless task timeout", { taskId });
|
|
||||||
|
|
||||||
Sentry.captureException(new Error("Headless task timeout"), {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task-timeout",
|
|
||||||
taskId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force finish the task to prevent native side hanging
|
|
||||||
try {
|
|
||||||
if (taskId) {
|
|
||||||
BackgroundFetch.finish(taskId);
|
|
||||||
logger.debug("Headless task force-finished due to timeout", { taskId });
|
|
||||||
}
|
|
||||||
} catch (finishError) {
|
|
||||||
logger.error("CRITICAL: Failed to force-finish timed out headless task", {
|
|
||||||
taskId,
|
|
||||||
error: finishError,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.captureException(finishError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task-timeout-finish",
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
task: { taskId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 30000); // 30 second timeout
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!taskId) {
|
|
||||||
throw new Error("No taskId provided in headless task event");
|
|
||||||
}
|
|
||||||
|
|
||||||
await backgroundTask();
|
|
||||||
logger.info("Headless task completed successfully", { taskId });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Headless task failed", { taskId, error });
|
|
||||||
|
|
||||||
Sentry.captureException(error, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task",
|
|
||||||
taskId,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
event: {
|
|
||||||
taskId,
|
|
||||||
eventData: JSON.stringify(event),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Clear the timeout
|
|
||||||
clearTimeout(taskTimeout);
|
|
||||||
|
|
||||||
// CRITICAL: Always call finish, even on error
|
|
||||||
try {
|
|
||||||
if (taskId) {
|
|
||||||
BackgroundFetch.finish(taskId);
|
|
||||||
logger.debug("Headless task finished", { taskId });
|
|
||||||
} else {
|
|
||||||
logger.error("Cannot finish headless task - no taskId", { event });
|
|
||||||
}
|
|
||||||
} catch (finishError) {
|
|
||||||
// This is a critical error - the native side might be in a bad state
|
|
||||||
logger.error(
|
|
||||||
"CRITICAL: BackgroundFetch.finish() failed in headless task",
|
|
||||||
{
|
|
||||||
taskId,
|
|
||||||
error: finishError,
|
|
||||||
event,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Sentry.captureException(finishError, {
|
|
||||||
tags: {
|
|
||||||
module: "auto-cancel-expired",
|
|
||||||
operation: "headless-task-finish",
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
contexts: {
|
|
||||||
task: { taskId },
|
|
||||||
event: { eventData: JSON.stringify(event) },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -89,7 +89,9 @@ export default async function notifAlert(data) {
|
||||||
|
|
||||||
// Generate notification content
|
// Generate notification content
|
||||||
const { title, body, bigText } = generateAlertContent({
|
const { title, body, bigText } = generateAlertContent({
|
||||||
oneAlert: { alertTag, code, level },
|
alertTag,
|
||||||
|
code,
|
||||||
|
level,
|
||||||
initialDistance,
|
initialDistance,
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||||
|
|
||||||
|
const heartbeatLogger = createLogger({
|
||||||
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
feature: "geolocation-heartbeat-sync",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function notifGeolocationHeartbeatSync(data) {
|
||||||
|
try {
|
||||||
|
heartbeatLogger.info(
|
||||||
|
"Received iOS geolocation heartbeat sync notification",
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is a silent notification - no visible notification is displayed
|
||||||
|
// Instead, we trigger the geolocation heartbeat sync directly
|
||||||
|
|
||||||
|
heartbeatLogger.info("Triggering geolocation heartbeat sync");
|
||||||
|
|
||||||
|
// Execute the heartbeat sync to force location update
|
||||||
|
await executeHeartbeatSync();
|
||||||
|
|
||||||
|
heartbeatLogger.info("Geolocation heartbeat sync completed successfully");
|
||||||
|
} catch (error) {
|
||||||
|
heartbeatLogger.error("Failed to execute geolocation heartbeat sync", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't throw the error - this is a background operation
|
||||||
|
// and we don't want to crash the notification handler
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,9 +88,9 @@ export const generateSuggestKeepOpenContent = (data) => {
|
||||||
|
|
||||||
export const generateBackgroundGeolocationLostContent = (data) => {
|
export const generateBackgroundGeolocationLostContent = (data) => {
|
||||||
return {
|
return {
|
||||||
title: `Alerte-Secours ne peut plus accéder à votre position`,
|
title: `Alerte-Secours ne reçoit plus de mises à jour de votre position`,
|
||||||
body: `Vous ne pouvez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
body: `Vous ne pourrez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
||||||
bigText: `Alerte-Secours ne peut plus accéder à votre position en arrière-plan. Vous ne pouvez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
bigText: `Alerte-Secours ne reçoit plus de mises à jour de votre position en arrière-plan. Vous ne pourrez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import notifSuggestKeepOpen from "./channels/notifSuggestKeepOpen";
|
||||||
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
||||||
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
||||||
import notifBackgroundGeolocationLost from "./channels/notifBackgroundGeolocationLost";
|
import notifBackgroundGeolocationLost from "./channels/notifBackgroundGeolocationLost";
|
||||||
|
// import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync.js.bak";
|
||||||
|
|
||||||
const displayLogger = createLogger({
|
const displayLogger = createLogger({
|
||||||
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
@ -22,6 +23,7 @@ const SUPPORTED_ACTIONS = {
|
||||||
"relative-allow-ask": notifRelativeAllowAsk,
|
"relative-allow-ask": notifRelativeAllowAsk,
|
||||||
"relative-invitation": notifRelativeInvitation,
|
"relative-invitation": notifRelativeInvitation,
|
||||||
"background-geolocation-lost": notifBackgroundGeolocationLost,
|
"background-geolocation-lost": notifBackgroundGeolocationLost,
|
||||||
|
// "geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function displayNotificationHandler(data) {
|
export default async function displayNotificationHandler(data) {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import {
|
||||||
import useMount from "~/hooks/useMount";
|
import useMount from "~/hooks/useMount";
|
||||||
import setActionCategories from "./setActionCategories";
|
import setActionCategories from "./setActionCategories";
|
||||||
import onMessageReceived from "./onMessageReceived";
|
import onMessageReceived from "./onMessageReceived";
|
||||||
import { useAutoCancelExpired } from "./autoCancelExpired";
|
|
||||||
import { requestFcmPermission, setupFcm } from "./firebase";
|
import { requestFcmPermission, setupFcm } from "./firebase";
|
||||||
import {
|
import {
|
||||||
requestNotifeePermission,
|
requestNotifeePermission,
|
||||||
|
@ -204,6 +203,4 @@ export function useFcm() {
|
||||||
notifLogger.debug("Badge count reset");
|
notifLogger.debug("Badge count reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useAutoCancelExpired();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,10 @@ export default withConnectivity(function AlertAggListArchived() {
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
>
|
>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
style={styles.sortByButton}
|
style={[
|
||||||
|
styles.sortByButton,
|
||||||
|
sortBy === "createdAt" && styles.sortByButtonSelected,
|
||||||
|
]}
|
||||||
icon={() => (
|
icon={() => (
|
||||||
// <MaterialIcons
|
// <MaterialIcons
|
||||||
// name="access-time"
|
// name="access-time"
|
||||||
|
@ -79,18 +82,29 @@ export default withConnectivity(function AlertAggListArchived() {
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="clock-time-four-outline"
|
name="clock-time-four-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.surfaceSecondary}
|
color={
|
||||||
|
sortBy === "createdAt"
|
||||||
|
? colors.onPrimary
|
||||||
|
: colors.onSurfaceVariant
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
value="createdAt"
|
value="createdAt"
|
||||||
/>
|
/>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
style={styles.sortByButton}
|
style={[
|
||||||
|
styles.sortByButton,
|
||||||
|
sortBy === "alphabetical" && styles.sortByButtonSelected,
|
||||||
|
]}
|
||||||
icon={() => (
|
icon={() => (
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="alphabetical"
|
name="alphabetical"
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.surfaceSecondary}
|
color={
|
||||||
|
sortBy === "alphabetical"
|
||||||
|
? colors.onPrimary
|
||||||
|
: colors.onSurfaceVariant
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
value="alphabetical"
|
value="alphabetical"
|
||||||
|
@ -156,6 +170,12 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
||||||
sortByButton: {
|
sortByButton: {
|
||||||
height: 32,
|
height: 32,
|
||||||
width: 32,
|
width: 32,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
sortByButtonSelected: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { deepEqual } from "fast-equals";
|
import { deepEqual } from "fast-equals";
|
||||||
|
|
||||||
import { useAlertState } from "~/stores";
|
import { useAlertState } from "~/stores";
|
||||||
import { storeLocation } from "~/utils/location/storage";
|
import { storeLocation } from "~/location/storage";
|
||||||
import useLocation from "~/hooks/useLocation";
|
import useLocation from "~/hooks/useLocation";
|
||||||
|
|
||||||
import withConnectivity from "~/hoc/withConnectivity";
|
import withConnectivity from "~/hoc/withConnectivity";
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getDistance } from "geolib";
|
||||||
import { routeToInstructions } from "~/lib/geo/osrmTextInstructions";
|
import { routeToInstructions } from "~/lib/geo/osrmTextInstructions";
|
||||||
import getRouteState from "~/lib/geo/getRouteState";
|
import getRouteState from "~/lib/geo/getRouteState";
|
||||||
import shallowCompare from "~/utils/array/shallowCompare";
|
import shallowCompare from "~/utils/array/shallowCompare";
|
||||||
import { storeLocation } from "~/utils/location/storage";
|
import { storeLocation } from "~/location/storage";
|
||||||
import useLocation from "~/hooks/useLocation";
|
import useLocation from "~/hooks/useLocation";
|
||||||
|
|
||||||
import withConnectivity from "~/hoc/withConnectivity";
|
import withConnectivity from "~/hoc/withConnectivity";
|
||||||
|
|
|
@ -10,9 +10,9 @@ 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 {
|
import {
|
||||||
RequestDisableOptimization,
|
requestBatteryOptimizationExemption,
|
||||||
BatteryOptEnabled,
|
isBatteryOptimizationEnabled,
|
||||||
} from "react-native-battery-optimization-check";
|
} from "~/lib/native/batteryOptimization";
|
||||||
import openSettings from "~/lib/native/openSettings";
|
import openSettings from "~/lib/native/openSettings";
|
||||||
|
|
||||||
import requestPermissionFcm from "~/permissions/requestPermissionFcm";
|
import requestPermissionFcm from "~/permissions/requestPermissionFcm";
|
||||||
|
@ -26,23 +26,19 @@ 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 () => {
|
const requestBatteryOptimizationDisable = async () => {
|
||||||
if (Platform.OS !== "android") {
|
if (Platform.OS !== "android") return true;
|
||||||
return true; // iOS doesn't have battery optimization
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isEnabled = await BatteryOptEnabled();
|
const enabled = await isBatteryOptimizationEnabled();
|
||||||
if (isEnabled) {
|
if (enabled) {
|
||||||
console.log("Battery optimization enabled, requesting to disable...");
|
console.log("Battery optimization enabled, requesting exemption...");
|
||||||
RequestDisableOptimization();
|
await requestBatteryOptimizationExemption();
|
||||||
// Return false as the user needs to interact with the system dialog
|
// User must interact in Settings; will re-check on AppState 'active'
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
console.log("Battery optimization already disabled");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
console.log("Battery optimization already disabled");
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling battery optimization:", error);
|
console.error("Error handling battery optimization:", error);
|
||||||
return false;
|
return false;
|
||||||
|
@ -110,8 +106,8 @@ const checkPermissionStatus = async (permission) => {
|
||||||
return true; // iOS doesn't have battery optimization
|
return true; // iOS doesn't have battery optimization
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const isEnabled = await BatteryOptEnabled();
|
const enabled = await isBatteryOptimizationEnabled();
|
||||||
return !isEnabled; // Return true if optimization is disabled
|
return !enabled; // true if optimization is disabled
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking battery optimization:", error);
|
console.error("Error checking battery optimization:", error);
|
||||||
return false;
|
return false;
|
||||||
|
@ -200,24 +196,33 @@ export default function Permissions() {
|
||||||
|
|
||||||
const handleRequestPermission = async (permission) => {
|
const handleRequestPermission = async (permission) => {
|
||||||
try {
|
try {
|
||||||
const granted = await requestPermissions[permission]();
|
let granted = false;
|
||||||
setPermissions[permission](granted);
|
|
||||||
|
|
||||||
// For battery optimization, we need to handle the async nature differently
|
if (permission === "locationBackground") {
|
||||||
if (
|
// Ensure foreground location is granted first
|
||||||
permission === "batteryOptimizationDisabled" &&
|
const fgGranted = await checkPermissionStatus("locationForeground");
|
||||||
Platform.OS === "android"
|
if (!fgGranted) {
|
||||||
) {
|
const fgReq = await requestPermissionLocationForeground();
|
||||||
// Give a short delay for the system dialog to potentially complete
|
setPermissions.locationForeground(fgReq);
|
||||||
setTimeout(async () => {
|
if (!fgReq) {
|
||||||
const actualStatus = await checkPermissionStatus(permission);
|
granted = false;
|
||||||
setPermissions[permission](actualStatus);
|
} else {
|
||||||
}, 1000);
|
granted = await requestPermissionLocationBackground();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
granted = await requestPermissionLocationBackground();
|
||||||
|
}
|
||||||
|
setPermissions.locationBackground(granted);
|
||||||
} else {
|
} else {
|
||||||
// Double-check the status to ensure UI is in sync
|
granted = await requestPermissions[permission]();
|
||||||
const actualStatus = await checkPermissionStatus(permission);
|
setPermissions[permission](granted);
|
||||||
setPermissions[permission](actualStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Double-check the status to ensure UI is in sync.
|
||||||
|
// For battery optimization, this immediate check may still be 'enabled';
|
||||||
|
// we'll re-check again on AppState 'active' after returning from Settings.
|
||||||
|
const actualStatus = await checkPermissionStatus(permission);
|
||||||
|
setPermissions[permission](actualStatus);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error requesting ${permission} permission:`, error);
|
console.error(`Error requesting ${permission} permission:`, error);
|
||||||
}
|
}
|
||||||
|
|
109
src/services/backgroundFetch.js.bak
Normal file
109
src/services/backgroundFetch.js.bak
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import BackgroundFetch from "react-native-background-fetch";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||||
|
|
||||||
|
const backgroundFetchLogger = createLogger({
|
||||||
|
service: "background-fetch",
|
||||||
|
task: "service",
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize BackgroundFetch according to the documentation best practices.
|
||||||
|
* This should be called once when the root component mounts.
|
||||||
|
*/
|
||||||
|
export const initializeBackgroundFetch = async () => {
|
||||||
|
try {
|
||||||
|
backgroundFetchLogger.info("Initializing BackgroundFetch service");
|
||||||
|
|
||||||
|
// Configure BackgroundFetch for both platforms
|
||||||
|
const status = await BackgroundFetch.configure(
|
||||||
|
{
|
||||||
|
minimumFetchInterval: 15, // Only valid option - gives best chance of execution
|
||||||
|
},
|
||||||
|
// Event callback - handles both default fetch events and custom scheduled tasks
|
||||||
|
async (taskId) => {
|
||||||
|
backgroundFetchLogger.info("BackgroundFetch event received", {
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
|
||||||
|
let syncResult = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute the shared heartbeat logic and get result
|
||||||
|
syncResult = await executeHeartbeatSync();
|
||||||
|
backgroundFetchLogger.debug("Heartbeat sync completed", {
|
||||||
|
syncResult,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
backgroundFetchLogger.error("Heartbeat sync failed", {
|
||||||
|
error: error.message,
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
// CRITICAL: Always call finish with appropriate result
|
||||||
|
try {
|
||||||
|
if (taskId) {
|
||||||
|
let fetchResult;
|
||||||
|
|
||||||
|
if (syncResult?.error || !syncResult?.syncSuccessful) {
|
||||||
|
// Task failed
|
||||||
|
fetchResult = BackgroundFetch.FETCH_RESULT_FAILED;
|
||||||
|
} else if (
|
||||||
|
syncResult?.syncPerformed &&
|
||||||
|
syncResult?.syncSuccessful
|
||||||
|
) {
|
||||||
|
// Force sync was performed successfully - new data
|
||||||
|
fetchResult = BackgroundFetch.FETCH_RESULT_NEW_DATA;
|
||||||
|
} else {
|
||||||
|
// No sync was needed - no new data
|
||||||
|
fetchResult = BackgroundFetch.FETCH_RESULT_NO_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackgroundFetch.finish(taskId, fetchResult);
|
||||||
|
backgroundFetchLogger.debug("BackgroundFetch task finished", {
|
||||||
|
taskId,
|
||||||
|
fetchResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (finishError) {
|
||||||
|
backgroundFetchLogger.error(
|
||||||
|
"Failed to finish BackgroundFetch task",
|
||||||
|
{
|
||||||
|
error: finishError.message,
|
||||||
|
taskId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Timeout callback (REQUIRED by BackgroundFetch API)
|
||||||
|
async (taskId) => {
|
||||||
|
backgroundFetchLogger.warn("BackgroundFetch task timeout", { taskId });
|
||||||
|
// CRITICAL: Must call finish on timeout with FAILED result
|
||||||
|
try {
|
||||||
|
BackgroundFetch.finish(taskId, BackgroundFetch.FETCH_RESULT_FAILED);
|
||||||
|
} catch (error) {
|
||||||
|
backgroundFetchLogger.error("Failed to finish timed out task", {
|
||||||
|
error: error.message,
|
||||||
|
taskId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
backgroundFetchLogger.info("BackgroundFetch configured successfully", {
|
||||||
|
status,
|
||||||
|
platform: Platform.OS,
|
||||||
|
});
|
||||||
|
|
||||||
|
return status;
|
||||||
|
} catch (error) {
|
||||||
|
backgroundFetchLogger.error("Failed to initialize BackgroundFetch", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
platform: Platform.OS,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue