{"_id":"@aalillou/mo-bg-location","_rev":"7-a5c9929c1379401b4d07f1260d4cd1cd","name":"@aalillou/mo-bg-location","dist-tags":{"latest":"0.1.5"},"versions":{"0.1.1":{"name":"@aalillou/mo-bg-location","version":"0.1.1","keywords":["react-native","expo","mo-bg-location","MoBGLocation","background-location","geolocation"],"author":{"name":"Aalillou","email":"info@aalillou.be"},"license":"SEE LICENSE IN LICENSE","_id":"@aalillou/mo-bg-location@0.1.1","maintainers":[{"name":"aalillou","email":"mo@aalillou.be"}],"homepage":"https://github.com/aalillou/mo-bg-location#readme","bugs":{"url":"https://github.com/aalillou/mo-bg-location/issues"},"dist":{"shasum":"bb46ab1a0ab162dfe7b379887f6aef629fdf1e5f","tarball":"https://registry.npmjs.org/@aalillou/mo-bg-location/-/mo-bg-location-0.1.1.tgz","fileCount":60,"integrity":"sha512-AGK9HdsNao9QVuS6zEF1401hvuoy8nUO7Jr1sTYIFlIqFyhK3EW6PHs5yKAc65N7DkwHs4aMsNeFtueIhn3wsg==","signatures":[{"sig":"MEQCIGLQAU3LEix9RVi0LJDiuW28htZZPblKd1u50VpWYbStAiBDQGWvjxGWcp+RwZyoIVHGDTQPHovuPKjPbpdmSA2vMw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4474250},"main":"build/index.js","types":"build/index.d.ts","gitHead":"e3a1c70a97d0cb550ccbb559665f49d82c76f06b","_npmUser":{"name":"aalillou","email":"mo@aalillou.be"},"repository":{"url":"git+https://github.com/aalillou/mo-bg-location.git","type":"git"},"_npmVersion":"10.9.0","description":"Background geolocation Expo module with a self-computed activity classifier (binary distribution).","directories":{},"sideEffects":false,"_nodeVersion":"22.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"expo":"*","react":"*","react-native":"*"},"_npmOperationalInternal":{"tmp":"tmp/mo-bg-location_0.1.1_1783956217300_0.8425201290125359","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@aalillou/mo-bg-location","version":"0.1.2","keywords":["react-native","expo","mo-bg-location","MoBGLocation","background-location","geolocation"],"author":{"name":"Aalillou","email":"info@aalillou.be"},"license":"SEE LICENSE IN LICENSE","_id":"@aalillou/mo-bg-location@0.1.2","maintainers":[{"name":"aalillou","email":"mo@aalillou.be"}],"homepage":"https://github.com/aalillou/mo-bg-location#readme","bugs":{"url":"https://github.com/aalillou/mo-bg-location/issues"},"dist":{"shasum":"985a6737bff212e1805027d5353af5d186a26858","tarball":"https://registry.npmjs.org/@aalillou/mo-bg-location/-/mo-bg-location-0.1.2.tgz","fileCount":60,"integrity":"sha512-VVWGA/F+0H9a6jO9OAqalAp0Kx9YqWikaFlMbT+vBrudkBUimanGgjiaPXqHQClNiEhHRmMR3GptUOATrtvN/w==","signatures":[{"sig":"MEQCICHZQfE8eopEH/mmh4w0RGHy19R39zEmnxobZNW0hDWmAiAZ2HRf43O0wEfS3lqmYieDG/0G22KJjXG34ME7hWiggQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4474260},"main":"build/index.js","types":"build/index.d.ts","gitHead":"fe570ed0a16f0a911b40c822b400706ba30ffe26","_npmUser":{"name":"aalillou","email":"mo@aalillou.be"},"repository":{"url":"git+https://github.com/aalillou/mo-bg-location.git","type":"git"},"_npmVersion":"10.9.0","description":"Background geolocation Expo module with a self-computed activity classifier (binary distribution).","directories":{},"sideEffects":false,"_nodeVersion":"22.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"expo":"*","react":"*","react-native":"*"},"_npmOperationalInternal":{"tmp":"tmp/mo-bg-location_0.1.2_1783956687229_0.3553279075115947","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@aalillou/mo-bg-location","version":"0.1.3","keywords":["react-native","expo","mo-bg-location","MoBGLocation","background-location","geolocation"],"author":{"name":"Aalillou","email":"info@aalillou.be"},"license":"SEE LICENSE IN LICENSE","_id":"@aalillou/mo-bg-location@0.1.3","maintainers":[{"name":"aalillou","email":"mo@aalillou.be"}],"homepage":"https://github.com/aalillou/mo-bg-location#readme","bugs":{"url":"https://github.com/aalillou/mo-bg-location/issues"},"dist":{"shasum":"982515afca34c135bd9305735e7e2b618aa97040","tarball":"https://registry.npmjs.org/@aalillou/mo-bg-location/-/mo-bg-location-0.1.3.tgz","fileCount":60,"integrity":"sha512-3E0z2gz4O3HVQgdQM6vhHTq4IXHPwVeBCAHrRBw5/pj1+WdE+M7aQ7lHjUDcZxLRKzRv1AgSqG2uMJkWlLuPBA==","signatures":[{"sig":"MEYCIQCPhYTH6rmNtUvNBJzoGuFmPDBziF98b7ww5RvZYLn2hwIhANedEY6Lf1QSNaB0ndo7prajMalQE7PkbKashyFp6ssN","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":5526316},"main":"build/index.js","types":"build/index.d.ts","gitHead":"fe570ed0a16f0a911b40c822b400706ba30ffe26","_npmUser":{"name":"aalillou","email":"mo@aalillou.be"},"repository":{"url":"git+https://github.com/aalillou/mo-bg-location.git","type":"git"},"_npmVersion":"10.9.0","description":"Background geolocation Expo module with a self-computed activity classifier (binary distribution).","directories":{},"sideEffects":false,"_nodeVersion":"22.12.0","dependencies":{},"_hasShrinkwrap":false,"peerDependencies":{"expo":"*","react":"*","react-native":"*"},"_npmOperationalInternal":{"tmp":"tmp/mo-bg-location_0.1.3_1784227186203_0.9100707635109464","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@aalillou/mo-bg-location","version":"0.1.4","keywords":["react-native","expo","mo-bg-location","MoBGLocation","background-location","geolocation"],"author":{"name":"Aalillou","email":"info@aalillou.be"},"license":"SEE LICENSE IN LICENSE","_id":"@aalillou/mo-bg-location@0.1.4","maintainers":[{"name":"aalillou","email":"mo@aalillou.be"}],"homepage":"https://github.com/aalillou/mo-bg-location#readme","bugs":{"url":"https://github.com/aalillou/mo-bg-location/issues"},"dist":{"shasum":"904a8327798cdf201a442b7b64134418b071d72b","tarball":"https://registry.npmjs.org/@aalillou/mo-bg-location/-/mo-bg-location-0.1.4.tgz","fileCount":60,"integrity":"sha512-sHrbbFkl/GewfmcIaL+yoig3FGQfAfQHkjUoXpKM6vdZ1oHu9SoNpUartzwitYrPlGMT5VFulstcpB15Ske2jw==","signatures":[{"sig":"MEYCIQCbVchSEUzuTLZJ0GDAJB1aZa1vczT4BmTXyVrTT4AoYAIhAOHnd+20OPipC+TKOYyap/pcsRmDG0h8HQZnLGO2+DCu","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":5528643},"main":"build/index.js","types":"build/index.d.ts","gitHead":"fe570ed0a16f0a911b40c822b400706ba30ffe26","_npmUser":{"name":"aalillou","email":"mo@aalillou.be"},"repository":{"url":"git+https://github.com/aalillou/mo-bg-location.git","type":"git"},"_npmVersion":"10.9.0","description":"Background geolocation Expo module with a self-computed activity classifier (binary distribution).","directories":{},"sideEffects":false,"_nodeVersion":"22.12.0","dependencies":{},"_hasShrinkwrap":false,"peerDependencies":{"expo":"*","react":"*","react-native":"*"},"_npmOperationalInternal":{"tmp":"tmp/mo-bg-location_0.1.4_1784270028543_0.5149514872856149","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@aalillou/mo-bg-location","version":"0.1.5","description":"Background geolocation Expo module with a self-computed activity classifier (binary distribution).","main":"build/index.js","types":"build/index.d.ts","sideEffects":false,"keywords":["react-native","expo","mo-bg-location","MoBGLocation","background-location","geolocation"],"repository":{"type":"git","url":"git+https://github.com/aalillou/mo-bg-location.git"},"bugs":{"url":"https://github.com/aalillou/mo-bg-location/issues"},"author":{"name":"Aalillou","email":"info@aalillou.be"},"license":"SEE LICENSE IN LICENSE","homepage":"https://github.com/aalillou/mo-bg-location#readme","dependencies":{},"peerDependencies":{"expo":"*","react":"*","react-native":"*"},"_id":"@aalillou/mo-bg-location@0.1.5","gitHead":"38ca9932d0898dee9f1501b928e08d5d613f9726","_nodeVersion":"22.12.0","_npmVersion":"10.9.0","dist":{"integrity":"sha512-96d1ckLLvQ8zl72H6UbM/KoH+q2ymRIAg0pFHkEFueoYl5MH1PDWVb3yl44Vcv7xHwxMyKlC0118Iah5X6+Axg==","shasum":"3e731949a54090b36a66880b8f6bc851a38b7507","tarball":"https://registry.npmjs.org/@aalillou/mo-bg-location/-/mo-bg-location-0.1.5.tgz","fileCount":60,"unpackedSize":5148964,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGfOkm4P515EI8k6N0lD9ZI0zbRFLe+d5H32TSQWxNyyAiEAho7elX0rmSPNMfP+mTMSuikWsdJVnwfge3B0ezR0B50="}]},"_npmUser":{"name":"aalillou","email":"mo@aalillou.be"},"directories":{},"maintainers":[{"name":"aalillou","email":"mo@aalillou.be"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mo-bg-location_0.1.5_1785773749805_0.9844731075527748"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-13T15:23:37.059Z","modified":"2026-08-03T16:15:50.235Z","0.1.0":"2026-07-13T12:21:14.452Z","0.1.1":"2026-07-13T15:23:37.450Z","0.1.2":"2026-07-13T15:31:27.512Z","0.1.3":"2026-07-16T18:39:46.478Z","0.1.4":"2026-07-17T06:33:48.747Z","0.1.5":"2026-08-03T16:15:50.066Z"},"bugs":{"url":"https://github.com/aalillou/mo-bg-location/issues"},"author":{"name":"Aalillou","email":"info@aalillou.be"},"license":"SEE LICENSE IN LICENSE","homepage":"https://github.com/aalillou/mo-bg-location#readme","keywords":["react-native","expo","mo-bg-location","MoBGLocation","background-location","geolocation"],"repository":{"type":"git","url":"git+https://github.com/aalillou/mo-bg-location.git"},"description":"Background geolocation Expo module with a self-computed activity classifier (binary distribution).","maintainers":[{"name":"aalillou","email":"mo@aalillou.be"}],"readme":"# mo-bg-location\n\nBackground geolocation for Expo / React Native with a self-computed activity\nclassifier and a battery-aware motion state machine. The native engine ships as\nprebuilt binaries (Android `.aar`, iOS `.xcframework`); the JavaScript API and\nExpo config plugin are open in this package.\n\n> Commercial SDK — free in development/debug, license key required for release\n> builds. See [Licensing](#licensing) below.\n\n---\n\n## Why mo-bg-location\n\nMost background geolocation SDKs keep the GPS chip polling continuously — even\nwhen parked — and depend on geofences or timed heartbeats to detect departure.\nThat pattern burns battery proportional to park duration and is fragile on\nAndroid Doze, where the CPU wakes infrequently and network-based geofences\nstall.\n\n**mo-bg-location takes a different approach:**\n\n- **GPS-independent wake from idle.** Departure detection is driven by the\n  hardware significant-motion sensor (Android) and Apple's CLLocationUpdate\n  stationarity engine (iOS 17+), not by GPS geofences. The motion trigger fires\n  inside Doze at the hardware interrupt level — no GPS required to detect that\n  the vehicle started moving.\n\n- **Self-computed activity classifier.** Activity labels (`walking`,\n  `in_vehicle`, `still`, …) come from our own classifier built on step counts,\n  Doppler speed, and covering-ground logic — not from the OS activity\n  recognition API, whose labels can lag by minutes and vary by device. Two\n  selectable models: `residual` (Doppler/covering-ground, the default) and\n  `steps` (GPS-free, instant label transitions, suited for fleet/driver apps\n  where the meaningful on-foot signal is active walking).\n\n- **Battery-aware stationary tiers.** Three automatic tiers — tight 60-second\n  poll (first 45 min of a stop, catches the quick errand), relaxed 5-minute\n  poll (longer stops), and deep-idle rare poll (overnight park, after 90 min of\n  stillness). The deep-idle tier eliminates the overnight polling floor that\n  otherwise drains the battery over an all-night park.\n\n- **Measured wake latency.** On iOS with the `liveUpdates` arm: 4–22 s /\n  10–77 m from wheels rolling to first location update in field tests. Classic\n  geofence-exit approaches measured at ~60–90 s on the same hardware.\n\n- **Delivery that survives a killed JS context — into *your* database shape.**\n  The optional native RTDB sink writes location, motion, and diagnostic events\n  from the native foreground service (Android) or `TrackingRuntime` (iOS) with\n  no JS alive — and an [output template](#nativesynctemplate--writing-your-own-rtdb-shape)\n  lets you declare the exact paths and value tree it writes, so the events land\n  in the schema your app already reads instead of one imposed by the SDK.\n\n---\n\n## Install\n\n```bash\nnpx expo install @aalillou/mo-bg-location\n```\n\nThis is an Expo module with a config plugin. In a managed app, run a prebuild\nso the plugin can apply the required Android permissions, foreground-service\ndeclarations, and iOS background modes:\n\n```bash\nnpx expo prebuild\n```\n\n### iOS: apps that don't already use Firebase\n\nThe SDK's optional native Firebase sink links the Firebase iOS pods. If your\napp does **not** already depend on React Native Firebase, `pod install` fails\non Firebase's modular headers unless CocoaPods builds frameworks statically.\nAdd [`expo-build-properties`](https://docs.expo.dev/versions/latest/sdk/build-properties/)\nto your `app.json`:\n\n```jsonc\n\"plugins\": [\n  [\"expo-build-properties\", { \"ios\": { \"useFrameworks\": \"static\" } }]\n]\n```\n\n(Apps already on `@react-native-firebase/*` have this set — its install docs\nrequire the same flag.)\n\n### Supported Expo SDK range\n\nEach release pins the Expo SDK range it is built against (native binaries are\ncompiled against a specific `expo-modules-core`). Installing outside the\nsupported range is not supported.\n\n| mo-bg-location | Expo SDK |\n|----------------|----------|\n| (set per release) | (set per release) |\n\n---\n\n## Auth & security setup\n\nWith the native sink (`nativeSync`), the SDK writes to your Firebase Realtime\nDatabase **as your app's own logged-in user**. That is the entire security model,\nso it's worth stating the boundary plainly:\n\n**What the module does**\n\n- Tracks location (motion-gated) and, with `nativeSync`, writes location / motion\n  / wake events straight to your RTDB from the **native** layer — so delivery\n  survives the JS context dying (swipe-kill, crash, OS relaunch).\n- Writes into **your** schema, not one it imposes — see\n  [`nativeSyncTemplate`](#nativesynctemplate--writing-your-own-rtdb-shape).\n\n**What the module does NOT do**\n\n- It **never signs your app in to Firebase**, and never signs out. It holds no\n  credentials of its own.\n- It inherits whatever session your app already established\n  (`FirebaseAuth.getInstance().currentUser`) and writes under that user's\n  `auth.uid` and custom claims.\n- It does **not** bypass your security rules. A write your rules reject stays\n  rejected (logged once, then quiet).\n\nSo a secure setup is three steps, all on **your** side:\n\n**1. Sign your app in to Firebase** — however you already do (anonymous,\nemail/password, or your own custom-token flow). Do it before the first write is\nexpected; the SDK rides that session.\n\n```ts\nimport auth from '@react-native-firebase/auth';\n\n// YOUR auth — the SDK never does this for you.\nawait auth().signInWithCustomToken(tokenFromYourBackend); // or signInAnonymously(), etc.\n\nawait configure({ /* … */, nativeSync: true, nativeSyncTemplate, nativeSyncParams });\nawait start();\n```\n\n**2. Write RTDB security rules** that authorize the template's paths under that\nsession. The `{param.*}` values in a path are only a *claim* of identity; your\nrules verify the logged-in user may write there. Patterns — any-logged-in-user →\ntenant claim → per-user claim — are in\n[Securing template writes](#securing-template-writes).\n\n**3. Use auth that persists across process death.** The point of `nativeSync` is\nwriting when no JS is alive, so the revived native process must already hold a\nsession. Once your app has signed in, the native Firebase layer persists the\nsession to disk and refreshes it itself — so **anonymous, email/password, and\ncustom-token sessions all survive a kill** (a custom token is exchanged for a\npersistent session at sign-in; the revived process refreshes that session without\nre-minting the token). The only gap is a process that was **never** signed in on\nthis install, or was signed out.\n\n> **Not using `nativeSync`?** If you instead subscribe to\n> [`onLocation`](#onlocationcallback-subscription) and write in your own JS, none\n> of the above is an SDK concern — auth is just your app's normal Firebase session\n> and there is no JS-less revival to secure.\n\n---\n\n## Quick start\n\n```ts\nimport {\n  configure,\n  requestPermissions,\n  start,\n  stop,\n  onLocation,\n  onMotionChange,\n  onMotionWake,\n} from '@aalillou/mo-bg-location';\n\n// 1. Configure before requesting permissions or starting\nawait configure({\n  desiredAccuracy: 'high@5s',\n  distanceFilter: 10,\n  stopTimeout: 90,\n  notificationTitle: 'Tracking active',\n  notificationBody: 'Location tracking is running in the background.',\n});\n\n// 2. Request permissions\nawait requestPermissions({ background: true, activity: true });\n\n// 3. Subscribe to events\nconst locSub = onLocation((e) => {\n  console.log(e.latitude, e.longitude, e.activity, e.isMoving);\n});\n\nconst motionSub = onMotionChange((e) => {\n  // Fires on stationary ↔ moving transitions and label promotions\n  console.log('motion:', e.isMoving, e.activity);\n});\n\nconst wakeSub = onMotionWake((e) => {\n  // Fires when the stationary engine wakes due to motion (reason, displacement)\n  console.log('wake reason:', e.reason);\n});\n\n// 4. Start tracking\nawait start();\n\n// 5. Stop and clean up\nawait stop();\nlocSub.remove();\nmotionSub.remove();\nwakeSub.remove();\n```\n\n---\n\n## Configuration reference\n\nPass a complete config object to `configure()` before calling `start()`. The\nnative side stashes it; re-calling `configure()` between runs is safe and is\nthe way to switch A/B flags without a rebuild.\n\n```ts\nawait configure({\n  // ── Core tracking ──────────────────────────────────────────────────────\n  desiredAccuracy: 'high@5s', // 'high' | 'high@5s' | 'balanced'\n  distanceFilter: 10,          // metres between updates while moving\n  stopTimeout: 90,             // seconds of stillness before going stationary\n\n  // ── Android foreground-service notification ────────────────────────────\n  notificationTitle: 'Tracking active',\n  notificationBody:  'Running in the background.',\n\n  // ── Battery / stationary tiers ─────────────────────────────────────────\n  deepIdleAfter: 90,           // minutes of stillness → deep idle (0 = disable)\n  deepIdlePollInterval: 30,    // minutes between polls in deep idle\n  tightPollWindowMinutes: 45,  // minutes of tight 60-second polling after a stop\n\n  // ── Activity classifier ────────────────────────────────────────────────\n  labelMode: 'residual',       // 'residual' (Doppler, default) | 'steps' (GPS-free)\n  stepsModeQuietMs: 8000,      // ms of no step → in_vehicle  (steps model only)\n  stepsStillWindowMs: 0,       // ms of trailing 'still' band before in_vehicle\n  stepReportLatencyMs: 10000,  // Android step-detector FIFO latency (0 = real-time)\n\n  // ── iOS power layer ────────────────────────────────────────────────────\n  powerMode: 'liveUpdates',    // 'liveUpdates' (iOS 17+, default) | 'arbiter'\n\n  // ── Lifecycle ─────────────────────────────────────────────────────────\n  wakeOnTerminate: false,      // true = survive swipe-kill and revive tracking\n\n  // ── Native RTDB sink (optional, for Firebase-backed apps) ─────────────\n  nativeSync: false,           // true = native layer writes events to RTDB\n  nativeSyncRootPath: '/tests/locations',   // built-in schema only\n  nativeSyncTemplate: undefined,            // your own paths + value tree\n  nativeSyncParams:   undefined,            // static {param.*} values\n\n  // ── License (required for release builds) ─────────────────────────────\n  licenseKey: process.env.EXPO_PUBLIC_MOBG_LICENSE_KEY,\n});\n```\n\n### `desiredAccuracy`\n\n| Value | Android | iOS |\n|-------|---------|-----|\n| `'high'` | HIGH_ACCURACY priority @ 1 s | Best accuracy, dense cadence |\n| `'high@5s'` | HIGH_ACCURACY priority @ 5 s | Same as `'high'` (cadence is not a CL concept) |\n| `'balanced'` | BALANCED_POWER @ 5 s — WiFi/cell fused, no GNSS chip | WiFi/cell |\n\n`'balanced'` never lights the GNSS chip and suits dense urban areas. In\nWiFi-sparse terrain (rural, motorways) use `'high@5s'` — `'balanced'` can\nproduce Doppler-free fixes that confuse the classifier.\n\n### `stopTimeout`\n\nSeconds of undetected motion before the state machine transitions to stationary\nand pauses active GPS. Note: the field is **seconds** here — some other\nlibraries use minutes for the same concept.\n\n### `labelMode`\n\n| Value | Classifier | Transitions | GPS needed |\n|-------|-----------|-------------|-----------|\n| `'residual'` | Doppler speed + covering-ground sustain | ~5–15 s after motion changes | Yes (speed from GPS) |\n| `'steps'` | Step recency: step present → walking; quiet window elapsed → in_vehicle | ~instant | No |\n\n`'steps'` is ideal for fleet/driver apps where the meaningful signal is *active\nwalking* (between stops). The trade-off is that stationary-on-foot reads\n`in_vehicle` after the quiet window. Pair with `stepReportLatencyMs: 0` for\ninstant transitions; the default 10 s batch causes per-batch flicker on a\ncontinuous walk.\n\n### `deepIdleAfter` / `deepIdlePollInterval` (Android)\n\nAfter `deepIdleAfter` minutes of undisturbed park, the 5-minute stationary poll\ndrops to the rare `deepIdlePollInterval`-minute safety poll. This eliminates\ncontinuous overnight polling — a phone parked all night consumes a fraction of\nthe battery compared to a steady 5-min poll floor.\n\nThe only downside is that a *silent* rolling departure (car rolling away with no\nperson walking up to it) is detected at worst one poll interval late. Walk-up\nand pickup departures are still caught immediately by the motion sensors regardless.\n\n### `tightPollWindowMinutes` (Android)\n\nFor the first `tightPollWindowMinutes` minutes after a stop, the poll runs at\n60 seconds and departure requires 2 fixes ≥ 150 m with a relaxed accuracy gate\n(cold Doze-poll fixes can read poor accuracy but are real). After the window,\nthe poll relaxes to 5 minutes and the strict departure rule resumes (1 fix ≥ 120 m,\naccuracy ≤ 30 m).\n\nSize this to the longest errand you want to catch under tight polling. Default: 45.\n\n### `powerMode` (iOS)\n\n| Value | Wake mechanism | Measured latency |\n|-------|---------------|-----------------|\n| `'liveUpdates'` (default) | Apple's CLLocationUpdate stationarity engine — motion-triggered delivery resume | 4–22 s / 10–77 m (field runs F4) |\n| `'arbiter'` | Our stillness arbiter + CLMonitor region-exit + SLC | ~60–90 s (geometric) |\n\nRegion exit and Significant Location Change stay armed as backstops in **both**\narms, so every drive yields a which-fired-first row. Switch arms with\n`stop()` → `configure({ powerMode })` → `start()`. iOS < 17 falls back to\n`'arbiter'` automatically.\n\n### `wakeOnTerminate`\n\n- `false` (default): swipe-kill stops tracking. Clean teardown — no background\n  revival until the user reopens the app.\n- `true`: tracking survives user termination. iOS uses region-exit/SLC/visit\n  relaunch; Android uses `START_STICKY` + broadcast receivers.\n  OS deaths (jetsam, crashes, reboots) revive tracking in both modes.\n\n### `nativeSync` + `nativeSyncRootPath`\n\nWhen `true`, the native layer (foreground service on Android; `TrackingRuntime`\non iOS) writes every location, motion, wake, and diagnostic event directly to\nFirebase RTDB — without a live JS context. This means events land even after a\nswipe-kill, JS crash, or an OS-initiated background relaunch that never warms\nup the JS layer.\n\nRequires a default `FirebaseApp` in the host app (add `google-services.json`\n/ `GoogleService-Info.plist` and call `configure()` in the native layer).\nWithout a template (below) the sink writes the SDK's **built-in schema** under\n`nativeSyncRootPath`, keyed by the anonymous-auth UID the JS side creates.\n\n**Single-writer rule:** when `nativeSync` is on, disable JS-side sink writes\nto avoid duplicating events under two session keys.\n\n### `nativeSyncTemplate` — writing your own RTDB shape\n\nThe built-in schema is almost certainly not the tree your app reads. A\n**template** tells the native sink exactly which paths to write and what value\ntree to put there — so you keep everything that makes the native sink worth\nhaving (delivery that survives a dead JS context, offline queueing, background\nwrites) while the data lands in *your* shape.\n\n`nativeSync: true` is still the master switch: the template says *what* to\nwrite, that flag says *whether* the native layer writes at all. When a template\nis set, `nativeSyncRootPath` is ignored — template paths are absolute.\n\n```ts\nawait configure({\n  // … tracking config …\n\n  nativeSync: true,\n  nativeSyncParams: {                 // static identity → {param.*}\n    sessionKey: 'shift-42',\n    nodeKey: '12_Doe',\n    driverId: 12,\n  },\n  nativeSyncTemplate: {\n    targets: [\n      {\n        trigger: 'location',\n        path: 'fleet/locations/{param.sessionKey}/{param.nodeKey}',\n        // A map does not need 1 Hz. Either gate opens the write: a slow crawl\n        // still reports every 5 s, a fast drive reports every 25 m.\n        throttle: { minIntervalMs: 5000, minDistanceM: 25 },\n        // Presence: the server deletes this node when the device goes offline.\n        onDisconnectRemove: true,\n        value: {\n          g: '{geohash}',             // GeoFire-compatible query key\n          l: '{latlng}',              // [lat, lng]\n          data: {\n            driverId: '{param.driverId}',\n            activity: '{activity}',   // still | on_foot | in_vehicle | …\n            updated_at: '{isoTime}',\n            battery: { level: '{battery.level}', is_charging: '{battery.isCharging}' },\n            $extras: true,            // merge in whatever setSyncExtras() holds\n          },\n        },\n      },\n    ],\n  },\n});\n```\n\n**How values resolve**\n\n- A string that is **exactly one placeholder** becomes that native type:\n  `\"{lat}\"` writes a number, `\"{latlng}\"` writes an array, `\"{isMoving}\"` a boolean.\n- A placeholder **inside a longer string** is interpolated as text\n  (`\"driver {param.driverId}\"`). Fractional numbers (`{lat}`, `{speed}`) may not\n  be embedded this way — use them as exact-one placeholders.\n- A placeholder that **resolves to nothing drops its key** rather than writing `null`.\n\n**Placeholders**\n\n| Scope | Available |\n|-------|-----------|\n| every trigger | `{ts}` `{isoTime}` `{timeLocal}` `{timeKey}` `{sessionId}` `{platform}` `{battery.level}` (0..1) `{battery.isCharging}` `{param.*}` `{extra.*}` |\n| `location` | `{lat}` `{lng}` `{latlng}` `{geohash}` `{accuracy}` `{speed}` `{activity}` `{isMoving}` |\n| `motion` | `{activity}` `{isMoving}` |\n| `wake` | wake reason / displacement fields |\n| `diagnostic` | `{kind}`, plus `\"$event\": true` to spread the raw event |\n\n**Targets**\n\n| Field | Meaning |\n|-------|---------|\n| `trigger` | `'location'` \\| `'motion'` \\| `'wake'` \\| `'diagnostic'` |\n| `path` | Absolute path from the database root |\n| `mode` | `'set'` (default, overwrite) or `'update'` (merge into the node) |\n| `throttle` | `location` only. `{ minIntervalMs, minDistanceM }` — with both, **either** gate opens the write |\n| `onDisconnectRemove` | Delete the node when the device's connection drops. Static paths only; re-armed on every reconnect, and **not** cancelled by `stop()` — a killed device must not stay on your map forever |\n| `value` | The JSON tree to write |\n\nUp to 8 targets. `\"$extras\": true` inside an object merges in the\n[`setSyncExtras`](#setsyncextrasextras-promisevoid) bag; explicit keys win.\n\n**Paths are validated, not trusted.** Only placeholders guaranteed to produce a\nlegal RTDB key may appear in `path` (`{param.*}`, `{sessionId}`, `{timeKey}`,\n`{activity}`, `{geohash}`, `{ts}`, `{platform}`, `{isMoving}`, `{kind}`). A\n`{lat}` in a path would turn `51.2` into two nested nodes, so `configure()`\nrejects it with **`ERR_SYNC_TEMPLATE`** — as it does for unknown placeholders,\nillegal path characters, and any `{param.*}` you forgot to supply.\n\n**Don't run two writers.** If your JS also writes these paths while `nativeSync`\nis on, you have two writers racing on the same nodes. Pick one.\n\n### Securing template writes\n\nThe template writes through **your app's existing Firebase login** — the SDK\nnever signs in and never signs out, it inherits whatever user your app already\nauthenticated (`FirebaseAuth.getInstance().currentUser`), including that user's\n`auth.uid` and any custom claims. Your **RTDB security rules** are the\nenforcement; the `{param.*}` values in a path are only a *claim* of identity, and\nyour rules are what verify the logged-in user is allowed to write there.\n\nSecure it with ordinary rules against that session plus the path — coarsest to\ntightest:\n\n```jsonc\n// 1. Any logged-in user\n\"fleet\": { \"locations\": { \".write\": \"auth != null\" } }\n\n// 2. Tenant-scoped — path carries {param.tenant}, rule checks a claim\n\"$tenant\": { \"locations\": { \".write\": \"auth.token.tenant === $tenant\" } }\n\n// 3. Per-driver, securing a HUMAN key via a custom claim\n\"fleet\": { \"locations\": { \"$session\": { \"$driverKey\": {\n  \".write\": \"auth.token.driverKey === $driverKey\"\n} } } }\n```\n\nPattern 3 is how you secure a path keyed by something like `12_Doe` rather than\nthe uid: your backend sets a `driverKey` (or `tenant`, or `role`) **custom claim**\nwhen the driver logs in, you put `{param.nodeKey}` in the path, and the rule\ncross-checks the path key against the claim. So: **params supply the claimed\nidentity, the authenticated session supplies the trusted identity, and your rule\nasserts they match.**\n\n**The revival window.** The whole point of `nativeSync` is writing when no JS is\nalive (swipe-kill, crash, OS background relaunch), so your app's normal sign-in\ncode hasn't run. This is fine for an **established** session: once your app has\nsigned in — anonymous, email/password, **or custom-token** — the native layer\npersists it to disk and refreshes the ID token itself, so `auth.uid` and\ntoken-baked claims survive a kill and rules keyed on them still pass. (A custom\ntoken is exchanged for a persistent session at sign-in; the revived process\nrefreshes that session natively without re-minting the token — verified on-device,\nboth platforms.) The only gap is a process that was **never** signed in on this\ninstall, or was signed out: it has no auth until your app relaunches and\nre-authenticates, and writes in that window are rejected (logged once, then quiet).\n\n---\n\n## API\n\n### `configure(config: Config): Promise<void>`\n\nStash the configuration. Safe to call at any time, including before permissions\nare granted. Changes take effect at the next `start()` call (or immediately for\nfields that don't require a restart, like `notificationBody`).\n\nRejects with `ERR_SYNC_TEMPLATE` if `nativeSyncTemplate` is invalid.\n\n### `requestPermissions(options): Promise<PermissionStatus>`\n\nRequest the OS permissions needed for background tracking:\n\n```ts\nawait requestPermissions({\n  background: true,   // ACCESS_BACKGROUND_LOCATION / Always authorization\n  activity: true,     // ACTIVITY_RECOGNITION (Android 10+) / Motion & Fitness (iOS)\n});\n```\n\nThe stages run as a ladder, identical on both platforms: **foreground location\nis always requested first**; the background upgrade (Android\n`ACCESS_BACKGROUND_LOCATION` / iOS Always) runs only when `background: true`\nand foreground was granted; the motion stage (Android `ACTIVITY_RECOGNITION` /\niOS Motion & Fitness) runs only when `activity: true` and foreground was\ngranted. The resolved `PermissionStatus` merges the outcome of every stage.\nOne combined call is therefore all a typical driver app needs.\n\n### `getPermissions(): Promise<PermissionStatus>`\n\nRead current permission state without prompting.\n\n```ts\nconst perms = await getPermissions();\n// { foreground: boolean, background: boolean, activity: boolean,\n//   notifications: boolean, precise: boolean,\n//   batteryOptimizationExempt: boolean, exactAlarms: boolean }\n```\n\nThe last two are not permissions the user grants the SDK — they are the Android\npower restrictions the SDK has to operate under, reported so your app can tell\na driver why tracking may go quiet. `batteryOptimizationExempt: false` means the\nOS may freeze the process mid-drive; `exactAlarms: false` means the stationary\npoll loses its timing precision and a drive-away from a deep-idle park is\nnoticed late. Both are read-only — requesting them takes a user gesture and\nbelongs to your app — and both report `true` on iOS, which has no equivalent.\nNote `batteryOptimizationExempt` reflects stock Doze only: OEM layers such as\nSamsung's \"Sleeping apps\" are not visible to any app-readable API.\n\n### `start(): Promise<void>`\n\nStart tracking. Reads the stashed config and latches it for the run. Rejects\nwith `ERR_LICENSE` on a release build without a valid key.\n\n### `stop(): Promise<void>`\n\nStop tracking. Tears down the foreground service / background task and clears\nactive registrations.\n\n### `getCurrentPosition(): Promise<LocationEvent>`\n\nOne-shot current location (no continuous tracking required).\n\n### `getPowerStats(): Promise<PowerStats>`\n\nRead the current battery and power-state snapshot.\n\n### `setSyncExtras(extras): Promise<void>`\n\nSet the mutable values your `nativeSyncTemplate` writes as `{extra.*}` (or\nspreads with `\"$extras\": true`) — the things that change *during* a shift: a\nbooking id, the parcels currently loaded, a status flag. Identity that doesn't\nchange belongs in `nativeSyncParams` instead.\n\n**Whole-bag replace, not a merge** — pass everything you want written:\n\n```ts\nawait setSyncExtras({ charged: ['pkg-1', 'pkg-2'], bookingId: 42 });\nawait setSyncExtras({ charged: [] });   // bookingId is now GONE, not kept\n```\n\nThe bag is persisted natively and **survives the JS context dying**, which is\nthe point: a process revived without JS keeps writing rows that still carry your\nshift state. It also deliberately survives `stop()` — the next `start()` renders\nwith the last bag you set until you replace it. (Dropping it would mean a revived\nprocess writes rows *missing* your shift state, which is worse than a stale one;\nyou always get the chance to overwrite.)\n\nRejects with `ERR_SYNC_EXTRAS` if a key is not a legal RTDB key, or the bag\nexceeds 16 KB.\n\n### `onLocation(callback): Subscription`\n\nFires on every location update while moving:\n\n```ts\nconst sub = onLocation((e: LocationEvent) => {\n  // e.latitude, e.longitude, e.accuracy\n  // e.speed          — m/s (null if unavailable)\n  // e.activity       — 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'still' | 'unknown'\n  // e.isMoving       — true while in moving state\n  // e.timestamp      — Unix ms\n});\n```\n\n### `onMotionChange(callback): Subscription`\n\nFires on stationary ↔ moving state transitions **and** on activity label\npromotions (e.g. `still` → `walking` → `in_vehicle`):\n\n```ts\nconst sub = onMotionChange((e: MotionEvent) => {\n  // e.isMoving, e.activity, e.timestamp\n});\n```\n\n### `onMotionWake(callback): Subscription`\n\nFires when the stationary engine wakes due to detected motion:\n\n```ts\nconst sub = onMotionWake((e: MotionWakeEvent) => {\n  // e.reason         — 'sig-motion' | 'live-resume' | 'geofence' | 'slc' | …\n  // e.displacement   — metres from the park anchor (if available)\n});\n```\n\n### `onDiagnostic(callback): Subscription`\n\nFires on internal engine events: polls, geofence arm/disarm, soft-stop vetoes.\nUseful for debugging; leave off in production unless you need it.\n\n```ts\nconst sub = onDiagnostic((e: DiagnosticEvent) => {\n  // e.kind: 'poll' | 'geofence' | 'softstop' | 'wake' | …\n  // e.accuracy, e.accuracyGated, e.intervalMs, …\n});\n```\n\n---\n\n## Full example — typical driver app\n\n```tsx\nimport {\n  configure,\n  getPermissions,\n  requestPermissions,\n  setSyncExtras,\n  start,\n  stop,\n  onLocation,\n  onMotionChange,\n  type LocationEvent,\n  type MotionEvent,\n} from '@aalillou/mo-bg-location';\nimport { useEffect, useState } from 'react';\n\nexport default function TrackingScreen() {\n  const [lastLocation, setLastLocation] = useState<LocationEvent | null>(null);\n  const [motion, setMotion] = useState<MotionEvent | null>(null);\n  const [isTracking, setIsTracking] = useState(false);\n\n  useEffect(() => {\n    // Configure once on mount\n    configure({\n      desiredAccuracy: 'high@5s',\n      distanceFilter: 10,\n      stopTimeout: 90,\n      labelMode: 'steps',       // GPS-free, instant activity labels\n      stepReportLatencyMs: 0,   // real-time steps for instant transitions\n      deepIdleAfter: 90,\n      deepIdlePollInterval: 30,\n      tightPollWindowMinutes: 45,\n      notificationTitle: 'On shift',\n      notificationBody: 'Location tracking is active.',\n\n      // Write straight into the tree this app already reads — and keep writing\n      // it even if the OS kills the JS context mid-shift.\n      nativeSync: true,\n      nativeSyncParams: { sessionKey: shiftId, nodeKey: driverKey, driverId },\n      nativeSyncTemplate: {\n        targets: [{\n          trigger: 'location',\n          path: 'fleet/locations/{param.sessionKey}/{param.nodeKey}',\n          throttle: { minIntervalMs: 5000, minDistanceM: 25 },\n          onDisconnectRemove: true,\n          value: {\n            g: '{geohash}',\n            l: '{latlng}',\n            data: {\n              driverId: '{param.driverId}',\n              activity: '{activity}',\n              updated_at: '{isoTime}',\n              $extras: true,        // ← whatever setSyncExtras() holds\n            },\n          },\n        }],\n      },\n\n      licenseKey: process.env.EXPO_PUBLIC_MOBG_LICENSE_KEY,\n    }).catch(console.error);\n\n    const locSub  = onLocation((e) => setLastLocation(e));\n    const motSub  = onMotionChange((e) => setMotion(e));\n\n    return () => { locSub.remove(); motSub.remove(); };\n  }, []);\n\n  const handleStart = async () => {\n    const perms = await getPermissions();\n    if (!perms.background) {\n      await requestPermissions({ background: true, activity: true });\n    }\n    await start();\n    setIsTracking(true);\n  };\n\n  // Shift state that must keep being written even if the app is killed.\n  const handleLoadParcel = async (parcels: string[]) => {\n    await setSyncExtras({ charged: parcels });\n  };\n\n  const handleStop = async () => {\n    await stop();\n    setIsTracking(false);\n  };\n\n  return (\n    // … your UI\n  );\n}\n```\n\n---\n\n## Permissions\n\n### Android\n\nThe config plugin adds these automatically via prebuild:\n\n| Permission | Purpose |\n|------------|---------|\n| `ACCESS_FINE_LOCATION` | Foreground GPS |\n| `ACCESS_BACKGROUND_LOCATION` | Background GPS (Android 10+) |\n| `ACTIVITY_RECOGNITION` | Step detector + AR (Android 10+) |\n| `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_LOCATION` | Location foreground service |\n| `RECEIVE_BOOT_COMPLETED` | Revive tracking after reboot |\n| `SCHEDULE_EXACT_ALARM` | Exact stationary Doze-poll (Android 12+) — see below |\n\n#### Power restrictions the plugin cannot grant\n\nTwo OS conditions decide how well background tracking survives a long park, and\nneither is a permission the SDK can declare for you:\n\n- **Battery-optimization exemption.** Without it, Android may freeze the process\n  mid-drive and the trail simply stops. Showing the exemption dialog needs a user\n  gesture (`ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`), so it belongs to your\n  app — the SDK never prompts.\n- **Exact-alarm capability.** Auto-granted with the exemption above, or by the\n  user via Settings → **Alarms & reminders**. Without it the stationary poll\n  still runs, but relaxes to Doze maintenance-window cadence, so a drive that\n  starts from a deep-idle park is noticed late rather than within a minute. The\n  SDK guards the call and falls back to an inexact alarm — a capability upgrade,\n  not a hard requirement. Note the SDK never uses the Play-restricted\n  `USE_EXACT_ALARM`; apps shipping on Google Play should note the exact-alarm\n  policy declaration.\n\n`getPermissions()` reports both as `batteryOptimizationExempt` and `exactAlarms`\nso your app can show a driver why tracking may go quiet.\n\n### iOS\n\nBackground mode `location` and `motion` entitlements are added by the config\nplugin. The app must supply `NSLocationWhenInUseUsageDescription`,\n`NSLocationAlwaysAndWhenInUseUsageDescription`, and\n`NSMotionUsageDescription` strings in `Info.plist` (set via Expo's\n`infoPlist` in `app.json`).\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause |\n|---------|-------------|\n| `start()` never resolves | Missing foreground location permission — call `requestPermissions` first |\n| Labels stuck on `unknown` | `ACTIVITY_RECOGNITION` not granted (Android) or motion permission denied (iOS) |\n| No updates after swipe-kill | `wakeOnTerminate` is `false` (default) — set `true` if you need tracking to survive a kill |\n| iOS wake latency > 30 s | iOS < 17 falls back to `'arbiter'`; on iOS 17+ confirm `powerMode: 'liveUpdates'` |\n| Overnight battery drain | `deepIdleAfter` not set or set to 0 — set to `90` (minutes) |\n| Android trail stops mid-drive, resumes when the app is opened | App is not battery-optimization exempt — check `batteryOptimizationExempt` from `getPermissions()`, and prompt for the exemption |\n| Drive-away from a long park is noticed minutes late (Android) | Exact-alarm capability missing — check `exactAlarms`; granting the battery exemption grants it too |\n| `ERR_LICENSE` on start | Release build with no key; see Licensing |\n| `ERR_SYNC_TEMPLATE` on configure | Invalid `nativeSyncTemplate` — the message names the offending target and placeholder |\n| Template tree stays empty | Writes rejected by your security rules (the SDK writes as your app's existing auth — check the device log for the one-time rejection), or `nativeSync` is not `true` |\n| `{extra.*}` values missing from writes | `setSyncExtras` is a whole-bag replace — a later call without a key removes it |\n\n---\n\n## Licensing\n\n**Development is free.** The SDK works fully in debug builds, the iOS simulator,\nand dev-signed device builds — no key needed.\n\n**Release builds require a license key** bound to your app's `packageName` /\nbundle ID (one key covers both platforms):\n\n```ts\nawait configure({\n  licenseKey: process.env.EXPO_PUBLIC_MOBG_LICENSE_KEY,\n  // … rest of config\n});\n```\n\nThe key is a signed public statement — safe to commit and ship in your JS\nbundle. Contact <info@aalillou.be> to obtain one.\n\nA release build with a missing, expired, or wrong-app key **rejects `start()`\nloudly** with code `ERR_LICENSE` so you catch it in your first release-build\nsmoke test. `configure()` also logs a one-line verdict on every launch\n(`license: development mode — no key required` / `license: valid for <appId>` /\nthe failure reason).\n\nTo exercise the release gate on a dev-signed iOS build (free-team codesign),\nadd `licenseEnforceRelease: true` to `configure()`. This flag can only *add*\nenforcement, never bypass it — leave it off in production.\n\n---\n\n## License\n\nProprietary — see [LICENSE](./LICENSE). The engine binaries contain no\nthird-party code; required components (Expo, React Native, Firebase, Google\nPlay services) are resolved by your app and licensed separately — see\n[THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md).\n","readmeFilename":"README.md"}