import BackgroundGeolocation from "react-native-background-geolocation"; import { TRACK_MOVE } from "~/misc/devicePrefs"; import { createLogger } from "~/lib/logger"; import { BACKGROUND_SCOPES } from "~/lib/logger/scopes"; import jwtDecode from "jwt-decode"; import { initEmulatorMode } from "./emulatorService"; import * as Sentry from "@sentry/react-native"; import { SPAN_STATUS_OK, SPAN_STATUS_ERROR } from "@sentry/react-native"; import throttle from "lodash.throttle"; import { getAuthState, subscribeAuthState, authActions, permissionsActions, } from "~/stores"; import setLocationState from "~/location/setLocationState"; import { storeLocation } from "~/utils/location/storage"; import env from "~/env"; const config = { // https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode enableHeadless: true, disableProviderChangeRecord: true, // disableMotionActivityUpdates: true, desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH, distanceFilter: TRACK_MOVE, // debug: true, // Enable debug mode for more detailed logs logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE, // Disable automatic permission requests locationAuthorizationRequest: "Always", stopOnTerminate: false, startOnBoot: true, heartbeatInterval: 900, // Force the plugin to start aggressively foregroundService: true, notification: { title: "Alerte Secours", text: "Suivi de localisation actif", channelName: "Location tracking", priority: BackgroundGeolocation.NOTIFICATION_PRIORITY_HIGH, }, backgroundPermissionRationale: { title: "Autoriser Alerte-Secours à accéder à la localisation en arrière-plan", message: "Alerte-Secours nécessite la localisation en arrière-plan pour vous alerter en temps réel lorsqu'une personne à proximité a besoin d'aide urgente. Cette fonction est essentielle pour permettre une intervention rapide et efficace en cas d'urgence.", positiveAction: "Autoriser", negativeAction: "Désactiver", }, // Enhanced HTTP configuration url: env.GEOLOC_SYNC_URL, method: "POST", // Explicitly set HTTP method httpRootProperty: "location", // Specify the root property for the locations array // Configure persistence maxRecordsToPersist: 1, // Limit the number of records to store maxDaysToPersist: 7, // Limit the age of records to persist batchSync: false, autoSync: true, reset: true, }; const defaultConfig = config; export default async function trackLocation() { const locationLogger = createLogger({ module: BACKGROUND_SCOPES.GEOLOCATION, feature: "tracking", }); // Log the geolocation sync URL for debugging locationLogger.info("Geolocation sync URL configuration", { url: env.GEOLOC_SYNC_URL, isStaging: env.IS_STAGING, }); // Throttling configuration for auth reload only const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle // Handle auth function - no throttling or cooldown async function handleAuth(userToken) { locationLogger.info("Handling auth token update", { hasToken: !!userToken, }); if (!userToken) { locationLogger.info("No auth token, stopping location tracking"); await BackgroundGeolocation.stop(); locationLogger.debug("Location tracking stopped"); return; } // unsub(); locationLogger.debug("Updating background geolocation config"); await BackgroundGeolocation.setConfig({ url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging headers: { Authorization: `Bearer ${userToken}`, }, }); // Log the authorization header that was set locationLogger.debug( "Set Authorization header for background geolocation", { headerSet: true, tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null, }, ); // Verify the current configuration try { const currentConfig = await BackgroundGeolocation.getConfig(); locationLogger.debug("Current background geolocation config", { hasHeaders: !!currentConfig.headers, headerKeys: currentConfig.headers ? Object.keys(currentConfig.headers) : [], authHeader: currentConfig.headers?.Authorization ? currentConfig.headers.Authorization.substring(0, 15) + "..." : "Not set", url: currentConfig.url, }); } catch (error) { locationLogger.error("Failed to get background geolocation config", { error: error.message, }); } const state = await BackgroundGeolocation.getState(); try { const decodedToken = jwtDecode(userToken); locationLogger.debug("Decoded JWT token", { decodedToken }); } catch (error) { locationLogger.error("Failed to decode JWT token", { error: error.message, }); } if (state.enabled) { locationLogger.info("Syncing location data"); try { await BackgroundGeolocation.changePace(true); await BackgroundGeolocation.sync(); locationLogger.debug("Sync initiated successfully"); } catch (error) { locationLogger.error("Failed to sync location data", { error: error.message, stack: error.stack, }); } } else { locationLogger.info("Starting location tracking"); try { await BackgroundGeolocation.start(); locationLogger.debug("Location tracking started successfully"); } catch (error) { locationLogger.error("Failed to start location tracking", { error: error.message, stack: error.stack, }); } } } BackgroundGeolocation.onLocation((location) => { locationLogger.debug("Location update received", { coords: location.coords, timestamp: location.timestamp, activity: location.activity, battery: location.battery, }); // Add Sentry breadcrumb for location updates Sentry.addBreadcrumb({ message: "Location update in trackLocation", category: "geolocation", level: "info", data: { coords: location.coords, activity: location.activity?.type, battery: location.battery?.level, isMoving: location.isMoving, }, }); if ( location.coords && location.coords.latitude && location.coords.longitude ) { setLocationState(location.coords); // Also store in AsyncStorage for last known location fallback storeLocation(location.coords, location.timestamp); } }); // The core auth reload function that will be throttled function _reloadAuth() { locationLogger.info("Refreshing authentication token"); authActions.reload(); // should retriger sync in handleAuth via subscribeAuthState when done } // Create throttled version of auth reload with lodash const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, { leading: true, trailing: false, // Prevent trailing calls to avoid duplicate refreshes }); BackgroundGeolocation.onHttp(async (response) => { // Log the full response including headers if available locationLogger.debug("HTTP response received", { status: response?.status, success: response?.success, responseText: response?.responseText, url: response?.url, method: response?.method, isSync: response?.isSync, requestHeaders: response?.request?.headers || "Headers not available in response", }); // Add Sentry breadcrumb for HTTP responses Sentry.addBreadcrumb({ message: "Background geolocation HTTP response", category: "geolocation-http", level: response?.status === 200 ? "info" : "warning", data: { status: response?.status, success: response?.success, url: response?.url, isSync: response?.isSync, recordCount: response?.count, }, }); // Log the current auth token for comparison const { userToken } = getAuthState(); locationLogger.debug("Current auth state token", { tokenAvailable: !!userToken, tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null, }); const statusCode = response?.status; switch (statusCode) { case 410: // Token expired, logout locationLogger.info("Auth token expired (410), logging out"); Sentry.addBreadcrumb({ message: "Auth token expired - logging out", category: "geolocation-auth", level: "warning", }); authActions.logout(); break; case 401: // Unauthorized, use throttled reload locationLogger.info("Unauthorized (401), attempting to refresh token"); // Add more detailed logging of the error response try { const errorBody = response?.responseText ? JSON.parse(response.responseText) : null; locationLogger.debug("Unauthorized error details", { errorBody, errorType: errorBody?.error?.type, errorMessage: errorBody?.error?.message, errorPath: errorBody?.error?.errors?.[0]?.path, }); Sentry.addBreadcrumb({ message: "Unauthorized - refreshing token", category: "geolocation-auth", level: "warning", data: { errorType: errorBody?.error?.type, errorMessage: errorBody?.error?.message, }, }); } catch (e) { locationLogger.debug("Failed to parse error response", { error: e.message, responseText: response?.responseText, }); } reloadAuth(); break; } }); try { locationLogger.info("Initializing background geolocation"); await BackgroundGeolocation.ready(defaultConfig); await BackgroundGeolocation.setConfig(config); // Only set the permission state if we already have the permission const state = await BackgroundGeolocation.getState(); locationLogger.debug("Background geolocation state", { enabled: state.enabled, trackingMode: state.trackingMode, isMoving: state.isMoving, schedulerEnabled: state.schedulerEnabled, }); if (state.enabled) { locationLogger.info("Background location permission confirmed"); permissionsActions.setLocationBackground(true); } else { locationLogger.warn( "Background location not enabled in geolocation state", ); } // if (LOCAL_DEV) { // // fixing issue on android emulator (which doesn't have accelerometer or gyroscope) by manually enabling location updates // setInterval( // () => { // BackgroundGeolocation.changePace(true); // }, // 30 * 60 * 1000, // ); // } } catch (error) { locationLogger.error("Location tracking initialization failed", { error: error.message, stack: error.stack, code: error.code, }); } const { userToken } = getAuthState(); locationLogger.debug("Setting up auth state subscription"); subscribeAuthState(({ userToken }) => userToken, handleAuth); locationLogger.debug("Performing initial auth handling"); handleAuth(userToken); // Initialize emulator mode if previously enabled initEmulatorMode(); }