diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9cb68e..77e6948 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
+## [1.11.11](https://github.com/alerte-secours/as-app/compare/v1.11.10...v1.11.11) (2025-07-23)
+
+## [1.11.10](https://github.com/alerte-secours/as-app/compare/v1.11.9...v1.11.10) (2025-07-22)
+
+## [1.11.9](https://github.com/alerte-secours/as-app/compare/v1.11.8...v1.11.9) (2025-07-22)
+
+## [1.11.8](https://github.com/alerte-secours/as-app/compare/v1.11.7...v1.11.8) (2025-07-22)
+
+## [1.11.7](https://github.com/alerte-secours/as-app/compare/v1.11.6...v1.11.7) (2025-07-22)
+
+## [1.11.6](https://github.com/alerte-secours/as-app/compare/v1.11.5...v1.11.6) (2025-07-21)
+
+## [1.11.5](https://github.com/alerte-secours/as-app/compare/v1.11.4...v1.11.5) (2025-07-21)
+
+## [1.11.4](https://github.com/alerte-secours/as-app/compare/v1.11.3...v1.11.4) (2025-07-20)
+
+## [1.11.3](https://github.com/alerte-secours/as-app/compare/v1.11.2...v1.11.3) (2025-07-20)
+
## [1.11.2](https://github.com/alerte-secours/as-app/compare/v1.11.1...v1.11.2) (2025-07-19)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 72278ef..058454b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -83,8 +83,8 @@ android {
applicationId 'com.alertesecours'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 192
- versionName "1.11.2"
+ versionCode 201
+ versionName "1.11.11"
multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
diff --git a/app.config.js b/app.config.js
index 78b583e..09fc17a 100644
--- a/app.config.js
+++ b/app.config.js
@@ -132,6 +132,7 @@ let config = {
"telprompt",
],
BGTaskSchedulerPermittedIdentifiers: [
+ "com.transistorsoft",
"com.transistorsoft.fetch",
"com.transistorsoft.customtask",
],
diff --git a/index.js b/index.js
index bfbcf01..074958b 100644
--- a/index.js
+++ b/index.js
@@ -5,6 +5,9 @@ import "./warnFilter";
import "expo-splash-screen";
import BackgroundGeolocation from "react-native-background-geolocation";
+import { Platform } from "react-native";
+import BackgroundFetch from "react-native-background-fetch";
+
import notifee from "@notifee/react-native";
import messaging from "@react-native-firebase/messaging";
@@ -18,9 +21,7 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
import onMessageReceived from "~/notifications/onMessageReceived";
import { createLogger } from "~/lib/logger";
-import * as Sentry from "@sentry/react-native";
-import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
-import { STORAGE_KEYS } from "~/storage/storageKeys";
+import { executeHeartbeatSync } from "~/location/backgroundTask";
// setup notification, this have to stay in index.js
notifee.onBackgroundEvent(notificationBackgroundEvent);
@@ -31,239 +32,82 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
// the environment is set up appropriately
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({
service: "background-geolocation",
task: "headless",
});
const HeadlessTask = async (event) => {
- // Add timeout protection for the entire headless task
- const taskTimeout = setTimeout(() => {
- geolocBgLogger.error("HeadlessTask timeout", { event });
-
- Sentry.captureException(new Error("HeadlessTask timeout"), {
- tags: {
- module: "background-geolocation",
- operation: "headless-task-timeout",
- eventName: event?.name,
- },
- });
- }, 60000); // 60 second timeout
-
- // Simple performance tracking without deprecated APIs
- const taskStartTime = Date.now();
-
try {
- // 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) {
+ switch (event?.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 });
- }
- }
+ await executeHeartbeatSync();
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);
+if (Platform.OS === "android") {
+ BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
+} else if (Platform.OS === "ios") {
+ BackgroundGeolocation.onLocation(async (_location) => {
+ await executeHeartbeatSync();
+ });
+
+ // Configure BackgroundFetch for iOS (iOS-specific configuration)
+ BackgroundFetch.configure(
+ {
+ minimumFetchInterval: 15, // Only valid option for iOS - gives best chance of execution
+ },
+ // Event callback
+ async (taskId) => {
+ let syncResult = null;
+
+ try {
+ // Execute the shared heartbeat logic and get result
+ syncResult = await executeHeartbeatSync();
+ } catch (error) {
+ // silent error
+ } finally {
+ // CRITICAL: Always call finish with appropriate result
+ try {
+ if (taskId) {
+ let fetchResult;
+
+ if (syncResult?.error || !syncResult?.syncSuccessful) {
+ // Task failed
+ fetchResult = BackgroundFetch.FETCH_RESULT_FAILED;
+ } else if (
+ syncResult?.syncPerformed &&
+ syncResult?.syncSuccessful
+ ) {
+ // Force sync was performed successfully - new data
+ fetchResult = BackgroundFetch.FETCH_RESULT_NEW_DATA;
+ } else {
+ // No sync was needed - no new data
+ fetchResult = BackgroundFetch.FETCH_RESULT_NO_DATA;
+ }
+
+ BackgroundFetch.finish(taskId, fetchResult);
+ }
+ } catch (finishError) {
+ // silent error
+ }
+ }
+ },
+ // Timeout callback (REQUIRED by BackgroundFetch API)
+ async (taskId) => {
+ // CRITICAL: Must call finish on timeout with FAILED result
+ BackgroundFetch.finish(taskId, BackgroundFetch.FETCH_RESULT_FAILED);
+ },
+ ).catch(() => {
+ // silent error
+ });
+}
diff --git a/ios/AlerteSecours.xcodeproj/xcshareddata/xcschemes/AlerteSecours.xcscheme b/ios/AlerteSecours.xcodeproj/xcshareddata/xcschemes/AlerteSecours.xcscheme
index abbba04..e3f2603 100644
--- a/ios/AlerteSecours.xcodeproj/xcshareddata/xcschemes/AlerteSecours.xcscheme
+++ b/ios/AlerteSecours.xcodeproj/xcshareddata/xcschemes/AlerteSecours.xcscheme
@@ -44,7 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
- launchStyle = "0"
+ launchStyle = "1"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
diff --git a/ios/AlerteSecours/Info.plist b/ios/AlerteSecours/Info.plist
index c3647cf..6f39065 100644
--- a/ios/AlerteSecours/Info.plist
+++ b/ios/AlerteSecours/Info.plist
@@ -4,6 +4,7 @@
BGTaskSchedulerPermittedIdentifiers
+ com.transistorsoft
com.transistorsoft.fetch
com.transistorsoft.customtask
@@ -24,7 +25,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.11.2
+ 1.11.11
CFBundleSignature
????
CFBundleURLTypes
@@ -47,7 +48,7 @@
CFBundleVersion
- 192
+ 201
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
@@ -94,23 +95,14 @@
alertesecours.fr
- NSIncludesSubdomains
-
NSExceptionAllowsInsecureHTTPLoads
- NSExceptionRequiresForwardSecrecy
-
NSExceptionMinimumTLSVersion
TLSv1.0
-
- sentry.io
-
+ NSExceptionRequiresForwardSecrecy
+
NSIncludesSubdomains
- NSExceptionRequiresForwardSecrecy
-
- NSExceptionMinimumTLSVersion
- TLSv1.0
localhost
@@ -119,6 +111,15 @@
NSIncludesSubdomains
+ sentry.io
+
+ NSExceptionMinimumTLSVersion
+ TLSv1.0
+ NSExceptionRequiresForwardSecrecy
+
+ NSIncludesSubdomains
+
+
NSCameraUsageDescription
@@ -150,6 +151,7 @@
fetch
location
remote-notification
+ processing
UILaunchStoryboardName
SplashScreen
diff --git a/ios/RNBackgroundFetch+AppDelegate.m b/ios/RNBackgroundFetch+AppDelegate.m
new file mode 100644
index 0000000..986b11c
--- /dev/null
+++ b/ios/RNBackgroundFetch+AppDelegate.m
@@ -0,0 +1,22 @@
+//
+// RNBackgroundGeolocation+AppDelegate.m
+// RNBackgroundGeolocationSample
+//
+// Created by Christopher Scott on 2016-08-01.
+// Copyright © 2016 Facebook. All rights reserved.
+//
+
+#import
+#import "AppDelegate.h"
+#import
+
+@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
diff --git a/package.json b/package.json
index 8184fe8..39cfecf 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "alerte-secours",
- "version": "1.11.2",
+ "version": "1.11.11",
"main": "index.js",
"scripts": {
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
@@ -50,8 +50,8 @@
"screenshot:android": "scripts/screenshot-android.sh"
},
"customExpoVersioning": {
- "versionCode": 192,
- "buildNumber": 192
+ "versionCode": 201,
+ "buildNumber": 201
},
"commit-and-tag-version": {
"scripts": {
diff --git a/src/location/backgroundTask.js b/src/location/backgroundTask.js
new file mode 100644
index 0000000..2ba859e
--- /dev/null
+++ b/src/location/backgroundTask.js
@@ -0,0 +1,93 @@
+import BackgroundGeolocation from "react-native-background-geolocation";
+import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
+import { STORAGE_KEYS } from "~/storage/storageKeys";
+import { createLogger } from "~/lib/logger";
+
+// Constants for persistence
+const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
+// const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
+
+const geolocBgLogger = createLogger({
+ service: "background-task",
+ task: "headless",
+});
+
+// Helper functions for persisting sync time
+const getLastSyncTime = async () => {
+ try {
+ const value = await memoryAsyncStorage.getItem(
+ STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
+ );
+ return value ? parseInt(value, 10) : Date.now();
+ } catch (error) {
+ return 0;
+ }
+};
+
+const setLastSyncTime = async (time) => {
+ try {
+ await memoryAsyncStorage.setItem(
+ STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
+ time.toString(),
+ );
+ } catch (error) {
+ // silent error
+ }
+};
+
+// Shared heartbeat logic - mutualized between Android and iOS
+const executeSync = async () => {
+ let syncPerformed = false;
+ let syncSuccessful = false;
+
+ try {
+ syncPerformed = true;
+
+ try {
+ // Change pace to ensure location updates
+ await BackgroundGeolocation.changePace(true);
+
+ // Perform sync
+ await BackgroundGeolocation.sync();
+
+ syncSuccessful = true;
+ } catch (syncError) {
+ syncSuccessful = false;
+ }
+
+ // Return result information for BackgroundFetch
+ return {
+ syncPerformed,
+ syncSuccessful,
+ };
+ } catch (error) {
+ // Return error result for BackgroundFetch
+ return {
+ syncPerformed,
+ syncSuccessful: false,
+ error: error.message,
+ };
+ }
+};
+export const executeHeartbeatSync = async () => {
+ const lastSyncTime = await getLastSyncTime();
+ const now = Date.now();
+ const timeSinceLastSync = now - lastSyncTime;
+ if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
+ geolocBgLogger.info("Forcing location sync");
+ try {
+ await Promise.race([
+ async () => {
+ await executeSync();
+ },
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("changePace timeout")), 10000),
+ ),
+ ]);
+
+ await setLastSyncTime(now);
+ } catch (syncError) {
+ geolocBgLogger.error("Force sync failed", { error: syncError });
+ }
+ }
+};
diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js
index 2081a2e..a3e2797 100644
--- a/src/location/trackLocation.js
+++ b/src/location/trackLocation.js
@@ -132,7 +132,7 @@ export default async function trackLocation() {
}
}
- BackgroundGeolocation.onLocation((location) => {
+ BackgroundGeolocation.onLocation(async (location) => {
locationLogger.debug("Location update received", {
coords: location.coords,
timestamp: location.timestamp,
diff --git a/src/notifications/autoCancelExpired.js b/src/notifications/autoCancelExpired.js
deleted file mode 100644
index 8f0169d..0000000
--- a/src/notifications/autoCancelExpired.js
+++ /dev/null
@@ -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) },
- },
- });
- }
- }
-});
diff --git a/src/notifications/index.js b/src/notifications/index.js
index fad5e9b..eab782b 100644
--- a/src/notifications/index.js
+++ b/src/notifications/index.js
@@ -15,7 +15,6 @@ import {
import useMount from "~/hooks/useMount";
import setActionCategories from "./setActionCategories";
import onMessageReceived from "./onMessageReceived";
-import { useAutoCancelExpired } from "./autoCancelExpired";
import { requestFcmPermission, setupFcm } from "./firebase";
import {
requestNotifeePermission,
@@ -204,6 +203,4 @@ export function useFcm() {
notifLogger.debug("Badge count reset");
});
});
-
- useAutoCancelExpired();
}