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(); }