diff --git a/docs/location-tracking-qa.md b/docs/location-tracking-qa.md
new file mode 100644
index 0000000..8bbdc89
--- /dev/null
+++ b/docs/location-tracking-qa.md
@@ -0,0 +1,92 @@
+# Location tracking QA checklist
+
+Applies to the BackgroundGeolocation integration:
+- [`trackLocation()`](src/location/trackLocation.js:34)
+- [`TRACKING_PROFILES`](src/location/backgroundGeolocationConfig.js:126)
+
+## Goals
+
+1. Updates only when moved enough
+ - IDLE: record/upload only after moving ~200m.
+ - ACTIVE: record/upload after moving ~25m.
+2. Works in foreground, background and terminated (Android + iOS).
+3. Avoid uploads while stationary.
+
+## Current implementation notes
+
+- Movement-driven recording only:
+ - IDLE uses `distanceFilter: 200` (aim: no updates while not moving).
+ - ACTIVE uses `distanceFilter: 25`.
+ - JS may request a persisted fix when entering ACTIVE (see [`applyProfile()`](src/location/trackLocation.js:351)).
+- Upload strategy is intentionally simple:
+ - Keep only the latest persisted geopoint: `maxRecordsToPersist: 1`.
+ - No batching / thresholds: `batchSync: false`, `autoSyncThreshold: 0`.
+ - When authenticated, each persisted location should upload immediately via native HTTP (works while JS is suspended).
+ - Pre-auth: tracking may persist locally but `url` is empty so nothing is uploaded until auth is ready.
+
+## Basic preconditions
+
+- Location permissions: foreground + background granted.
+- Motion permission granted.
+- Network reachable.
+
+## Foreground behavior
+
+### IDLE (no open alert)
+
+1. Launch app, ensure no open alert.
+2. Stay stationary for 5+ minutes.
+ - Expect: no repeated server updates.
+3. Walk/drive ~250m.
+ - Expect: at least one location persisted + uploaded.
+
+### ACTIVE (open alert)
+
+1. Open an alert owned by the current user.
+2. Move ~30m.
+ - Expect: at least one location persisted + uploaded.
+3. Continue moving.
+ - Expect: periodic updates roughly aligned with movement, not time.
+
+## Background behavior
+
+### IDLE
+
+1. Put app in background.
+2. Stay stationary.
+ - Expect: no periodic uploads.
+3. Move ~250m.
+ - Expect: a persisted record and upload.
+
+### ACTIVE
+
+1. Put app in background.
+2. Move ~30m.
+ - Expect: updates continue.
+
+## Terminated behavior
+
+### Android
+
+1. Ensure tracking enabled and authenticated.
+2. Force-stop the app task.
+3. Move ~250m in IDLE.
+ - Expect: native service still records + uploads.
+4. Move ~30m in ACTIVE.
+ - Expect: native service still records + uploads.
+
+### iOS
+
+1. Swipe-kill the app.
+2. Move significantly (expect iOS to relaunch app on stationary-geofence exit).
+ - Expect: tracking resumes and uploads after movement.
+
+## Debugging tips
+
+- Observe logs in app:
+ - `tracking_ctx` extras are updated on AppState changes and profile changes.
+ - See [`updateTrackingContextExtras()`](src/location/trackLocation.js:63).
+- Correlate:
+ - `onLocation` events
+ - `onHttp` events
+ - pending queue (`BackgroundGeolocation.getCount()` in logs)
diff --git a/index.js b/index.js
index 74316f6..71e7ffb 100644
--- a/index.js
+++ b/index.js
@@ -22,11 +22,13 @@ notifee.onBackgroundEvent(notificationBackgroundEvent);
messaging().setBackgroundMessageHandler(onMessageReceived);
// Android Headless Mode for react-native-background-geolocation.
-// Required because [`enableHeadless`](src/location/backgroundGeolocationConfig.js:16) is enabled and
-// we run with [`stopOnTerminate: false`](src/location/backgroundGeolocationConfig.js:40).
//
-// IMPORTANT: keep this handler lightweight. In headless state, the JS runtime may be launched
-// briefly and then torn down; long tasks can be terminated by the OS.
+// We currently run with `enableHeadless: false` (see
+// [`BASE_GEOLOCATION_CONFIG.enableHeadless`](src/location/backgroundGeolocationConfig.js:16)),
+// meaning we do not rely on JS callbacks while the app is terminated.
+//
+// This registration is kept only as a safety-net: if `enableHeadless` is ever turned on again,
+// we'll at least have a minimal handler.
BackgroundGeolocation.registerHeadlessTask(async (event) => {
// eslint-disable-next-line no-console
console.log("[BGGeo HeadlessTask]", event?.name, event?.params);
diff --git a/src/hooks/useLocation.js b/src/hooks/useLocation.js
index 86d3e4a..e7fb4c1 100644
--- a/src/hooks/useLocation.js
+++ b/src/hooks/useLocation.js
@@ -26,6 +26,9 @@ export default function useLocation() {
const [location, setLocation] = useState({ coords: DEFAULT_COORDS });
const [isLastKnown, setIsLastKnown] = useState(false);
const [lastKnownTimestamp, setLastKnownTimestamp] = useState(null);
+ // UI-facing realtime tracking (foreground).
+ // We intentionally keep this separate from BGGeo background tracking.
+ // Note: this watcher should be managed by screen lifecycle (mounted maps).
const watcher = useRef();
const timeoutRef = useRef();
const isWatchingRef = useRef(false);
@@ -41,7 +44,6 @@ export default function useLocation() {
await watcher.current.remove();
watcher.current = null;
}
-
// Reset flags
isWatchingRef.current = false;
hasLocationRef.current = false;
diff --git a/src/location/backgroundGeolocationConfig.js b/src/location/backgroundGeolocationConfig.js
index 1904dc3..a77a919 100644
--- a/src/location/backgroundGeolocationConfig.js
+++ b/src/location/backgroundGeolocationConfig.js
@@ -1,9 +1,18 @@
import BackgroundGeolocation from "react-native-background-geolocation";
-import { Platform } from "react-native";
import env from "~/env";
// Common config: keep always-on tracking enabled, but default to an IDLE low-power profile.
-// High-accuracy and moving mode are enabled only when an active alert is open.
+// High-accuracy and tighter distance thresholds are enabled only when an active alert is open.
+//
+// Expected behavior (both Android + iOS):
+// - Foreground: locations recorded only after moving beyond `distanceFilter`.
+// - Background: same rule; native service continues even if JS is suspended.
+// - Terminated:
+// - Android: native service continues (`stopOnTerminate:false`); JS headless is NOT required.
+// - iOS: OS will relaunch app on significant movement / stationary-geofence exit.
+//
+// NOTE: We avoid creating persisted records from UI-only lookups (eg map refresh), since
+// persisted records can trigger native HTTP uploads even while stationary.
//
// Product goals:
// - IDLE (no open alert): minimize battery; server updates are acceptable only on OS-level significant movement.
@@ -14,8 +23,10 @@ import env from "~/env";
// In dev, `reset: true` is useful to avoid config drift while iterating.
// - `maxRecordsToPersist` must be > 1 to support offline catch-up.
export const BASE_GEOLOCATION_CONFIG = {
- // Android Headless Mode (requires registering a headless task entrypoint in index.js)
- enableHeadless: true,
+ // Android Headless Mode
+ // We do not require JS execution while terminated. Native tracking + native HTTP upload
+ // are sufficient for our needs (stopOnTerminate:false).
+ enableHeadless: false,
// Default to low-power (idle) profile; will be overridden when needed.
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
@@ -27,8 +38,6 @@ export const BASE_GEOLOCATION_CONFIG = {
// Activity-recognition stop-detection.
// NOTE: Transistorsoft defaults `stopTimeout` to 5 minutes (see
// [`node_modules/react-native-background-geolocation/src/declarations/interfaces/Config.d.ts:79`](node_modules/react-native-background-geolocation/src/declarations/interfaces/Config.d.ts:79)).
- // We keep the default in BASE and override it in the IDLE profile to reduce
- // 5-minute stationary cycles observed on Android.
stopTimeout: 5,
// debug: true,
@@ -70,19 +79,24 @@ export const BASE_GEOLOCATION_CONFIG = {
},
// HTTP configuration
- url: env.GEOLOC_SYNC_URL,
+ // IMPORTANT: Default to uploads disabled until we have an auth token.
+ // Authenticated mode will set `url` + `Authorization` header and enable `autoSync`.
+ url: "",
method: "POST",
httpRootProperty: "location",
- batchSync: false,
- // We intentionally disable autoSync and perform controlled uploads from explicit triggers
- // (startup, identity-change, moving-edge, active-alert). This prevents stationary "ghost"
- // uploads from low-quality locations produced by some Android devices.
+ // Keep uploads simple: 1 location record -> 1 HTTP request.
+ // (We intentionally keep only the latest record; batching provides no benefit.)
autoSync: false,
+ // Ensure no persisted config can keep batching/threshold behavior.
+ batchSync: false,
+ autoSyncThreshold: 0,
- // Persistence: keep enough records for offline catch-up.
- // (The SDK already constrains with maxDaysToPersist; records are deleted after successful upload.)
- maxRecordsToPersist: 1000,
- maxDaysToPersist: 7,
+ // Persistence
+ // Product requirement: keep only the latest geopoint. This reduces on-device storage
+ // and avoids building up a queue.
+ // NOTE: This means we intentionally do not support offline catch-up of multiple points.
+ maxRecordsToPersist: 1,
+ maxDaysToPersist: 1,
// Development convenience
reset: !!__DEV__,
@@ -94,7 +108,7 @@ export const BASE_GEOLOCATION_CONFIG = {
// Options we want to be stable across launches even when the plugin loads a persisted config.
// NOTE: We intentionally do *not* include HTTP auth headers here.
export const BASE_GEOLOCATION_INVARIANTS = {
- enableHeadless: true,
+ enableHeadless: false,
stopOnTerminate: false,
startOnBoot: true,
foregroundService: true,
@@ -105,48 +119,32 @@ export const BASE_GEOLOCATION_INVARIANTS = {
method: "POST",
httpRootProperty: "location",
autoSync: false,
- maxRecordsToPersist: 1000,
- maxDaysToPersist: 7,
+ batchSync: false,
+ autoSyncThreshold: 0,
+ maxRecordsToPersist: 1,
+ maxDaysToPersist: 1,
};
export const TRACKING_PROFILES = {
idle: {
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
- // Max battery-saving strategy for IDLE:
- // Use Android/iOS low-power significant-change tracking where the OS produces
- // only periodic fixes (several times/hour). Note many config options like
- // `distanceFilter` / `stationaryRadius` are documented as having little/no
- // effect in this mode.
- // Some devices / OEMs can be unreliable with significant-change only.
- // Use standard motion tracking for reliability, with conservative distanceFilter.
- useSignificantChangesOnly: false,
-
- // Defensive: if some devices/platform conditions fall back to standard tracking,
- // keep the distanceFilter conservative to avoid battery drain.
+ // Defensive: keep the distanceFilter conservative to avoid battery drain.
distanceFilter: 200,
+ // Keep the plugin's speed-based distanceFilter scaling enabled (default).
+ // This yields fewer updates as speed increases (highway speeds) and helps battery.
+ // We intentionally do NOT set `disableElasticity: true`.
+
// Android-only: reduce false-positive motion triggers due to screen-on/unlock.
// (This is ignored on iOS.)
motionTriggerDelay: 30000,
-
- // Keep the default stop-detection timing (minutes). In significant-changes
- // mode, stop-detection is not the primary driver of updates.
- stopTimeout: 5,
-
- // No periodic wakeups while idle.
- heartbeatInterval: 0,
},
active: {
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
- // Ensure we exit significant-changes mode when switching from IDLE.
- useSignificantChangesOnly: false,
- distanceFilter: 50,
- heartbeatInterval: 60,
+ // ACTIVE target: frequent updates while moving.
+ distanceFilter: 25,
// Android-only: do not delay motion triggers while ACTIVE.
motionTriggerDelay: 0,
-
- // Keep default responsiveness during an active alert.
- stopTimeout: 5,
},
};
diff --git a/src/location/index.js b/src/location/index.js
index 43796dd..1760c33 100644
--- a/src/location/index.js
+++ b/src/location/index.js
@@ -61,9 +61,11 @@ export async function getCurrentLocation() {
return null;
}
+ // UI lookup: do not persist. Persisting can create a DB record and trigger
+ // native HTTP upload even if the user has not moved.
const location = await BackgroundGeolocation.getCurrentPosition({
timeout: 30,
- persist: true,
+ persist: false,
maximumAge: 5000,
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
samples: 1,
diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js
index 0018131..f077308 100644
--- a/src/location/trackLocation.js
+++ b/src/location/trackLocation.js
@@ -1,4 +1,5 @@
import BackgroundGeolocation from "react-native-background-geolocation";
+import { AppState } from "react-native";
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import jwtDecode from "jwt-decode";
@@ -42,6 +43,7 @@ export default function trackLocation() {
let currentProfile = null;
let authReady = false;
+ let appState = AppState.currentState;
let stopAlertSubscription = null;
let stopSessionSubscription = null;
@@ -58,38 +60,38 @@ export default function trackLocation() {
// Track identity so we can force a first geopoint when the effective user changes.
let lastSessionUserId = null;
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
-
- const pruneBadLocations = async () => {
+ const updateTrackingContextExtras = async (reason) => {
try {
- const locations = await BackgroundGeolocation.getLocations();
- if (!Array.isArray(locations) || !locations.length) return 0;
-
- let deleted = 0;
- // Defensive: only scan a bounded amount to avoid heavy work.
- const toScan = locations.slice(-200);
- for (const loc of toScan) {
- const acc = loc?.coords?.accuracy;
- const uuid = loc?.uuid;
- if (
- typeof acc === "number" &&
- acc > BAD_ACCURACY_THRESHOLD_M &&
- uuid
- ) {
- try {
- await BackgroundGeolocation.destroyLocation(uuid);
- deleted++;
- } catch (e) {
- // ignore
- }
- }
- }
- return deleted;
+ const { userId } = getSessionState();
+ await BackgroundGeolocation.setConfig({
+ extras: {
+ tracking_ctx: {
+ reason,
+ app_state: appState,
+ profile: currentProfile,
+ auth_ready: authReady,
+ session_user_id: userId || null,
+ at: new Date().toISOString(),
+ },
+ },
+ });
} catch (e) {
- return 0;
+ // Non-fatal: extras are only for observability/debugging.
+ locationLogger.debug("Failed to update BGGeo tracking extras", {
+ reason,
+ error: e?.message,
+ });
}
};
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+ // NOTE: Do not delete records from JS as a primary upload-filtering mechanism.
+ // When JS is suspended in background, deletions won't happen but native autoSync will.
+ // We keep this as a placeholder in case we later introduce a *server-side* filtering
+ // strategy or a safer native-side filter.
+ const pruneBadLocations = async () => 0;
+
const safeSync = async (reason) => {
// Sync can fail transiently (SDK busy, network warming up, etc). Retry a few times.
for (let attempt = 1; attempt <= 3; attempt++) {
@@ -181,8 +183,10 @@ export default function trackLocation() {
let startupFixInFlight = null;
// Startup fix should be persisted so it can be auto-synced immediately (user expects
- // to appear on server soon after first app open). This is intentionally different
- // from auth-refresh fixes, which are non-persisted to avoid unlock/resume noise.
+ // to appear on server soon after first app open).
+ //
+ // IMPORTANT: restrict this to the ACTIVE profile only.
+ // Persisted startup fixes while IDLE can create "no-move" uploads on some devices.
const requestStartupPersistedFix = async () => {
try {
const before = await BackgroundGeolocation.getState();
@@ -192,6 +196,13 @@ export default function trackLocation() {
isMoving: before.isMoving,
});
+ if (currentProfile !== "active") {
+ locationLogger.info("Skipping startup persisted fix (not ACTIVE)", {
+ currentProfile,
+ });
+ return;
+ }
+
const t0 = Date.now();
const location = await BackgroundGeolocation.getCurrentPosition({
samples: 1,
@@ -230,7 +241,7 @@ export default function trackLocation() {
}
};
- // When auth changes, we want a fresh persisted point for the newly effective identity.
+ // When auth changes, we want a fresh location fix (UI-only) to refresh the app state.
// Debounced to avoid spamming `getCurrentPosition` if auth updates quickly (refresh/renew).
let authFixDebounceTimerId = null;
let authFixInFlight = null;
@@ -301,6 +312,9 @@ export default function trackLocation() {
timestamp: location?.timestamp,
});
+ // NOTE: This is a non-persisted fix; it updates UI only.
+ // We intentionally do not trigger sync here to avoid network activity
+ // without a movement-triggered persisted record.
lastAuthFixAt = Date.now();
} catch (error) {
locationLogger.warn("Auth-change location fix failed", {
@@ -410,6 +424,9 @@ export default function trackLocation() {
currentProfile = profileName;
+ // Update extras for observability (profile transitions are a key lifecycle change).
+ updateTrackingContextExtras(`profile:${profileName}`);
+
try {
const state = await BackgroundGeolocation.getState();
locationLogger.info("Tracking profile applied", {
@@ -508,6 +525,9 @@ export default function trackLocation() {
authReady = false;
currentProfile = null;
+ // Ensure server/debug can see the app lifecycle context even pre-auth.
+ updateTrackingContextExtras("auth:anonymous");
+
if (authFixDebounceTimerId) {
clearTimeout(authFixDebounceTimerId);
authFixDebounceTimerId = null;
@@ -528,7 +548,11 @@ export default function trackLocation() {
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
- autoSync: false,
+ // IMPORTANT: enable native uploading when authenticated.
+ // This ensures uploads continue even if JS is suspended in background.
+ autoSync: true,
+ batchSync: false,
+ autoSyncThreshold: 0,
headers: {
Authorization: `Bearer ${userToken}`,
},
@@ -536,6 +560,8 @@ export default function trackLocation() {
authReady = true;
+ updateTrackingContextExtras("auth:ready");
+
// Log the authorization header that was set
locationLogger.debug(
"Set Authorization header for background geolocation",
@@ -600,16 +626,16 @@ export default function trackLocation() {
}
}
- // Always request a fresh persisted point on any token update.
- // This ensures a newly connected user gets an immediate point even if they don't move.
+ // Always request a fresh UI-only fix on any token update.
scheduleAuthFreshFix();
// Request a single fresh location-fix on each app launch when tracking is enabled.
// - We do this only after auth headers are configured so the persisted point can sync.
// - We do NOT force moving mode.
+ // - We only persist in ACTIVE.
if (!didRequestStartupFix) {
didRequestStartupFix = true;
- startupFixInFlight = requestStartupPersistedFix();
+ // Profile isn't applied yet. We'll request the startup fix after we apply the profile.
} else if (authFixInFlight) {
// Avoid concurrent fix calls if auth updates race.
await authFixInFlight;
@@ -620,6 +646,11 @@ export default function trackLocation() {
const shouldBeActive = computeHasOwnOpenAlert();
await applyProfile(shouldBeActive ? "active" : "idle");
+ // Now that profile is applied, execute the persisted startup fix if needed.
+ if (didRequestStartupFix && !startupFixInFlight) {
+ startupFixInFlight = requestStartupPersistedFix();
+ }
+
// Subscribe to changes that may require switching profiles.
if (!stopSessionSubscription) {
stopSessionSubscription = subscribeSessionState(
@@ -655,27 +686,14 @@ export default function trackLocation() {
// The final persisted location will arrive with sample=false.
if (location.sample) return;
- // Quality gate: delete very poor-accuracy locations to prevent them from being synced
- // and ignore them for UI/state.
+ // Quality gate (UI-only): if accuracy is very poor, ignore for UI/state.
+ // Do NOT delete the record here; native uploads may happen while JS is suspended.
const acc = location?.coords?.accuracy;
if (typeof acc === "number" && acc > BAD_ACCURACY_THRESHOLD_M) {
locationLogger.info("Ignoring poor-accuracy location", {
accuracy: acc,
uuid: location?.uuid,
});
- if (location?.uuid) {
- try {
- await BackgroundGeolocation.destroyLocation(location.uuid);
- locationLogger.debug("Destroyed poor-accuracy location", {
- uuid: location.uuid,
- });
- } catch (e) {
- locationLogger.warn("Failed to destroy poor-accuracy location", {
- uuid: location?.uuid,
- error: e?.message,
- });
- }
- }
return;
}
@@ -812,6 +830,24 @@ export default function trackLocation() {
locationLogger.info("Initializing background geolocation");
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
+ // Tag app foreground/background transitions so we can reason about uploads & locations.
+ // Note: there is no reliable JS signal for "terminated" when `enableHeadless:false`.
+ try {
+ const sub = AppState.addEventListener("change", (next) => {
+ const prev = appState;
+ appState = next;
+ locationLogger.info("AppState changed", { from: prev, to: next });
+ updateTrackingContextExtras("app_state");
+ });
+ // Keep the subscription alive for the app lifetime.
+ // (trackLocation is a singleton init; no teardown is expected.)
+ void sub;
+ } catch (e) {
+ locationLogger.debug("Failed to register AppState listener", {
+ error: e?.message,
+ });
+ }
+
// Ensure critical config cannot drift due to persisted plugin state.
// (We intentionally keep auth headers separate and set them in handleAuth.)
try {
@@ -823,6 +859,9 @@ export default function trackLocation() {
});
}
+ // Initial extras snapshot (even before auth) for observability.
+ updateTrackingContextExtras("startup");
+
// Only set the permission state if we already have the permission
const state = await BackgroundGeolocation.getState();
locationLogger.debug("Background geolocation state", {
diff --git a/src/scenes/Developer/index.js b/src/scenes/Developer/index.js
index df7a098..f88d5fc 100644
--- a/src/scenes/Developer/index.js
+++ b/src/scenes/Developer/index.js
@@ -46,6 +46,8 @@ export default function Developer() {
const [emulatorMode, setEmulatorMode] = useState(false);
const [syncStatus, setSyncStatus] = useState(null); // null, 'syncing', 'success', 'error'
const [syncResult, setSyncResult] = useState("");
+ const [bgGeoStatus, setBgGeoStatus] = useState(null); // null, 'loading', 'success', 'error'
+ const [bgGeoResult, setBgGeoResult] = useState("");
const [logLevel, setLogLevel] = useState(LOG_LEVELS.DEBUG);
// Initialize emulator mode and log level when component mounts
@@ -80,15 +82,23 @@ export default function Developer() {
await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
+ const state = await BackgroundGeolocation.getState();
+
// Get the count of pending records first
const count = await BackgroundGeolocation.getCount();
// Perform the sync
const records = await BackgroundGeolocation.sync();
+ const pendingAfter = await BackgroundGeolocation.getCount();
+
const result = `Synced ${
records?.length || 0
- } records (${count} pending)`;
+ } records (${count} pending before, ${pendingAfter} pending after). enabled=${String(
+ state?.enabled,
+ )} isMoving=${String(state?.isMoving)} trackingMode=${String(
+ state?.trackingMode,
+ )}`;
setSyncResult(result);
setSyncStatus("success");
} catch (error) {
@@ -97,6 +107,31 @@ export default function Developer() {
setSyncStatus("error");
}
};
+
+ const showBgGeoStatus = async () => {
+ try {
+ setBgGeoStatus("loading");
+ setBgGeoResult("");
+
+ await ensureBackgroundGeolocationReady(BASE_GEOLOCATION_CONFIG);
+ const [state, count] = await Promise.all([
+ BackgroundGeolocation.getState(),
+ BackgroundGeolocation.getCount(),
+ ]);
+
+ const result = `enabled=${String(state?.enabled)} isMoving=${String(
+ state?.isMoving,
+ )} trackingMode=${String(state?.trackingMode)} schedulerEnabled=${String(
+ state?.schedulerEnabled,
+ )} pending=${String(count)}`;
+ setBgGeoResult(result);
+ setBgGeoStatus("success");
+ } catch (error) {
+ console.error("BGGeo status failed:", error);
+ setBgGeoResult(`Status failed: ${error.message}`);
+ setBgGeoStatus("error");
+ }
+ };
const triggerNullError = () => {
try {
// Wrap the null error in try-catch
@@ -276,6 +311,32 @@ export default function Developer() {
{syncResult}
)}
+
+
+
+ {bgGeoStatus && bgGeoResult ? (
+
+ {bgGeoResult}
+
+ ) : null}