npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@movexgeo/react-native-movex

v2.0.1

Published

React Native bridge for movex.geo. Wraps the native Android (Kotlin) + iOS (Swift) SDKs as a TurboModule with TypeScript types.

Downloads

305

Readme

@movexgeo/react-native-movex

React Native bridge for movex.geo — background location, geofencing, and motion-aware tracking on iOS and Android. Wraps the native Kotlin and Swift SDKs as a TurboModule with TypeScript types.

  • Min RN: 0.73 (TurboModules + Bridgeless)
  • Min platforms: Android API 24 (Android 7) / iOS 14
  • Architecture: New Architecture supported; legacy bridge falls back automatically

What's new in 2.0.0: ACTIVITY_RECOGNITION is now host-opt-in (avoids Play's Health Apps classification by default). See CHANGELOG.md.

Looking for the native SDKs directly? See sdk/android and sdk/ios.


Install

yarn add @movexgeo/react-native-movex
# or
npm install @movexgeo/react-native-movex

The package autolinks on RN ≥ 0.60. After install:

# iOS — install Pods
cd ios && pod install && cd ..

# Android — nothing extra; the manifest merger pulls our permissions in

Get a location — no key needed

captureCurrentPosition is pre-init-safe — it works without an API key and without calling initialize(). All it needs is the location permission. Use it for delivery marking, OTP/RVP scans, any moment where you need a precise one-shot fix without the full tracking setup.

import MovexGeo from '@movexgeo/react-native-movex';

// No initialize() call, no API key — just permissions granted.
const fix = await MovexGeo.captureCurrentPosition();
console.log(fix.lat, fix.lng, fix.accuracy_m);

Options:

const fix = await MovexGeo.captureCurrentPosition({
  enable_high_accuracy: true,   // default: true — highest chip accuracy
  timeout_ms: 10_000,           // default: 10 s — raise for cold-start GPS indoors
});

Returns a LocationFix. Does not POST the fix to the server — it is purely a device-side capture. To also record it server-side, follow up with trackOnce(fix.lat, fix.lng, fix.accuracy_m) (which does require initialize()).

Rejects with MovexGeoError { code: 'permission_denied' } if location permission is not granted, and { code: 'location_timeout' } if the chip doesn't return a fix within timeout_ms.


Full tracking quick start

For background tracking with geofences and server events, you need an API key:

import MovexGeo from '@movexgeo/react-native-movex';

// 1. Initialize once at app startup. Sync, no network.
await MovexGeo.initialize('pub_live_<your-key>');

// 2. Associate fixes with a user (optional but recommended)
await MovexGeo.setUserId('u_42');

// 3. Verify the key resolves and pull project info
const info = await MovexGeo.verify();
console.log(info.project_id, info.tier);  // "prj_…" , "paid" | "shared" | "local"

// 4. One-shot location capture — also posts the fix to the server
const r = await MovexGeo.trackOnce(12.97, 77.59, 12);
console.log('accepted:', r.accepted, 'nearby fences:', r.nearby_fences?.length);

For the background loop with motion gating:

// Subscribe BEFORE startTracking so you don't miss early events.
const sub = MovexGeo.onEvents(events => {
  for (const e of events) {
    console.log(e.event_type, e.event_id); // user.entered_geofence, etc.
  }
});

await MovexGeo.startTracking('responsive');

// later (e.g. on logout or shift end)
await MovexGeo.stopTracking();
sub.remove();

Platform setup

iOS — Info.plist

Add the location strings users see in the OS permission prompt. Without these, CLLocationManager silently denies the request.

<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to track deliveries / rides / etc.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We track your location in the background to deliver real-time updates even when the app is closed.</string>

<!-- Required for background location updates -->
<key>UIBackgroundModes</key>
<array>
  <string>location</string>
  <string>fetch</string>
</array>

App Store review: Apple rejects "Always" requests without a clear user benefit — pair the prompt with explanatory in-app UI.

Android — manifest

All required permissions are declared in the SDK's manifest and merged into your app automatically (INTERNET, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION, FOREGROUND_SERVICE, FOREGROUND_SERVICE_LOCATION, POST_NOTIFICATIONS, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, RECEIVE_BOOT_COMPLETED, ACCESS_NETWORK_STATE).

You don't need to touch your AndroidManifest.xml for the SDK to work. You may want to override the FGS notification icon and copy — defaults are the host app's launcher icon (silhouetted by Android) + the generic system-optimisation copy; pass a custom icon and channel via setNotificationConfig({...}) if you want branding.

ACTIVITY_RECOGNITION is host-opt-in since 2.0.0. The SDK no longer auto-declares it (avoids Play's Health Apps classification by default). Hosts that want LocationFix.activity populated + the AR-driven STILL hint feeding the motion FSM add these two lines to their own AndroidManifest.xml:

<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />

Hosts that don't add it: tracking, geofences, trips, battery savings all unchanged — motion FSM falls back to Significant Motion + geometric stop detection. Details in the Android SDK wiki.


Permission flow (real implementation)

Android permissions are a moving target across API levels: ACCESS_BACKGROUND_LOCATION only exists from API 29, POST_NOTIFICATIONS from API 33, ACTIVITY_RECOGNITION is a runtime grant from API 29 but implicit before that (and host-opt-in since SDK 2.0.0 — included in the returned list only when you declare it in your own manifest), and FOREGROUND_SERVICE_LOCATION is a manifest declaration on API 34+. The SDK handles all of that for you — your app asks for a single list and forwards it to react-native-permissions.

You almost certainly also want a permission-request library for the runtime grant flow on Android:

yarn add react-native-permissions

We don't bundle a permission request helper — the ecosystem already solved it. We do expose a one-call permission read surface (getPermissionStatus) for when you're ready to start tracking.

Android — one call, no version branching

import { requestMultiple, type Permission } from 'react-native-permissions';
import MovexGeo from '@movexgeo/react-native-movex';

// 'all' = foreground location + background location + notifications
//         (+ activity recognition IF the host declared it in their
//         manifest — see "Android — manifest" above). Version-fenced
//         + manifest-aware since SDK 2.0.0.
// 'mandatory' = foreground-only flows (trackOnce, foreground-only startTracking).
const required = await MovexGeo.getRequiredPermissions('all');

const results = await requestMultiple(required as Permission[]);
// results is a record of { 'android.permission.X': 'granted' | 'denied' | ... }

That's it. The SDK decides which permissions to include for the device's Android version; the host app never branches on API level.

iOS

iOS doesn't have the same multi-permission cascade. getRequiredPermissions() returns [] on iOS. Use react-native-permissions directly:

import { Platform } from 'react-native';
import { PERMISSIONS, request } from 'react-native-permissions';

if (Platform.OS === 'ios') {
  await request(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE);
  await request(PERMISSIONS.IOS.LOCATION_ALWAYS); // only if background needed
}

Verify before starting tracking

Before calling startTracking, confirm permissions are in place using the SDK's one-call read API:

const perms = await MovexGeo.getPermissionStatus();
if (!perms.foregroundLocation) return showOnboardingRetry();
if (!perms.backgroundLocation) return showSettingsExplainer();
await MovexGeo.startTracking('responsive');

getPermissionStatus() is cheap, read-only, and does not require initialize() — safe to call from a permission-gating screen.

Background-location prompt on Android 11+

ACCESS_BACKGROUND_LOCATION on API 30+ can only be granted from system Settings — Android doesn't allow an in-app dialog for it. react-native-permissions correctly routes the user there. After they return, re-poll getPermissionStatus() to check what they chose. Walk the user through it once at first-run, then surface the deeplink again from a "trouble tracking?" screen.


Battery optimization & OEM autostart

This is where most "tracking stopped" bug reports come from on Android. AOSP's Doze and OEM-specific autostart killers (MIUI, ColorOS, EMUI, FuntouchOS, OneUI, OxygenOS, ASUS) reap foreground services aggressively unless the user has whitelisted the app.

The SDK ships four helpers — these never change device state by themselves; they only navigate the user to the right system screen.

// AOSP Doze whitelist — every Android phone since API 23.
const ignored = await MovexGeo.isIgnoringBatteryOptimizations();
if (!ignored) {
  await MovexGeo.requestIgnoreBatteryOptimizations();
  // The user sees a system prompt; you can't read their answer from here,
  // so re-poll isIgnoringBatteryOptimizations() after the prompt returns.
}

// OEM-specific autostart / background-protection screens.
const hasOem = await MovexGeo.hasOemBatterySettings();
if (hasOem) {
  // We detect the manufacturer (Build.MANUFACTURER + Build.BRAND) and
  // deep-link to the matching OEM activity. Falls back to AOSP battery
  // settings → app details page if the OEM screen is missing.
  await MovexGeo.openOemBatterySettings();
}

You cannot programmatically tell whether the user actually toggled the OEM setting — Android doesn't expose that state. The pragmatic flow is to walk the user through it once at first-run, then surface the deeplink again from a "trouble tracking?" / support screen.

All four methods are no-ops on iOS (Apple manages background tasks centrally; there's nothing to configure).


Diagnostics — one call, everything that matters

getDiagnostics() aggregates every state that determines whether background tracking will actually work into one object:

const d = await MovexGeo.getDiagnostics(); // requires initialize()

// permissions
if (!d.permissions.background_location) {
  showRequestBgPermissionUi();
}
if (!d.permissions.notification) {
  showRequestNotificationPermissionUi();
}

// battery / OEM
if (!d.battery_optimizations_ignored) {
  await MovexGeo.requestIgnoreBatteryOptimizations();
}
if (d.has_oem_battery_settings) {
  showOemAutostartButton();
}

// location backend
if (d.location_provider === 'AOSP') {
  // De-Googled / Huawei without HMS — limited accuracy is expected.
  showDegoogledRomBanner();
}

// queued data
if (d.buffered_fix_count > 100) {
  showSyncWaitingBanner(d.buffered_fix_count);
}

// support tickets — paste this into your bug report
console.log('support paste:', JSON.stringify(d));

Full shape (snake_case to match the rest of the bridge's JSON shape — ProjectInfo, LocationFix, etc.):

interface Diagnostics {
  permissions: {
    foreground_location: boolean;
    background_location: boolean;
    notification: boolean;
    activity_recognition: boolean;
    sdk_int: number;
  };
  battery_optimizations_ignored: boolean;   // Doze whitelist
  has_oem_battery_settings: boolean;        // MIUI / ColorOS / EMUI / etc
  location_provider: 'GMS' | 'HMS' | 'AOSP' | 'CoreLocation' | null;
  gms_available: boolean;
  hms_available: boolean;
  autostart_on_boot: boolean;
  buffered_fix_count: number;
  manufacturer: string;
  model: string;
  sdk_version: string;
}

On iOS every field returns a sensible stub since most have no Apple analogue (no Doze, no OEM autostart, no GMS/HMS).


Tracking presets

startTracking(preset) accepts one of four presets. Pick the one that matches your delivery / fleet / consumer-app shape:

| Preset | When to use | Cadence | Foreground service | |---|---|---|---| | 'efficient' | Background-quiet apps that only need to catch geofence events (delivery confirmations, store visits). OS-level geofencing wakes the app cheaply. | ~3 min / 100 m | No | | 'responsive' (default) | Most fleet / delivery apps. Motion-gated; cadence ramps with detected speed. | 30 s–5 min on motion, silent at rest | Yes | | 'adaptive' | Apps that need a regular firehose for analytics, OR-gated by distance + time. | 10 m or 5 s, whichever fires first | Yes | | 'continuous' | Driver telematics, asset tracking — every fix matters. | ~1 s | Yes (required) |

The motion state machine is on by default (Android, since 0.1.4). At rest, GPS is powered off entirely; Activity Recognition + TYPE_SIGNIFICANT_MOTION + a geofence around the last fix detect motion resumption and re-arm GPS. This drops the fix rate at rest by ~10× without missing trips.

To disable the FSM (for apps that need a true parked heartbeat):

await MovexGeo.setMotionStateMachineEnabled(false);
// Now efficient/responsive emit at preset cadence regardless of motion.

To tune the FSM:

await MovexGeo.setMotionConfig({
  stopDetectionDistance: 75,          // meters
  stopDetectionFixCount: 4,           // consecutive within-radius fixes → stop
  stopTimeoutSec: 300,                // PendingStop → Stationary delay
  motionTriggerDelaySec: 30,          // PendingStart → Moving delay
  disableStopDetection: false,
});

Source of truth for defaults: sdk/android/sdk/src/main/kotlin/movex/geo/MotionConfig.kt.


Events

Geofence transitions, motion changes, and other server-side notifications stream through a single onEvents subscription:

const sub = MovexGeo.onEvents(events => {
  for (const e of events) {
    switch (e.event_type) {
      case 'user.entered_geofence':
        console.log('entered fence:', e.geofence_id);
        break;
      case 'user.exited_geofence':
        console.log('exited fence:', e.geofence_id);
        break;
      // see types.ts for the full vocabulary
    }
  }
});

// later
sub.remove();

Events come from a 5-second long-poll of GET /v1/events on the server. Foreground geofence events on Android also come from the OS geofencing client (sub-second).

LOCAL-tier projects don't generate server events — geofences are evaluated entirely on-device and surfaced through the same onEvents callback.


Offline buffer

When the device loses network mid-trip, fixes queue in an on-device ring buffer (10k cap, oldest evicted). They drain automatically when connectivity returns.

const pending = await MovexGeo.bufferedCount();
if (pending > 0) {
  const drained = await MovexGeo.drainBuffer();
  console.log('drained', drained, 'pending', await MovexGeo.bufferedCount());
}

You usually don't need to call drainBuffer() manually — the SDK runs a ConnectivityManager.registerNetworkCallback (Android) / NWPathMonitor (iOS) drain on every reconnect. Use it only when you want to force a drain (e.g. user pressed "Sync now").


Error handling

All async APIs reject with a typed code so you can branch:

import type { MovexGeoErrorPayload } from '@movexgeo/react-native-movex';

try {
  await MovexGeo.verify();
} catch (e) {
  const err = e as MovexGeoErrorPayload;
  switch (err.code) {
    case 'invalid_key':       return showOnboardingError('Publishable key rejected.');
    case 'key_revoked':       return showOnboardingError('Key revoked. Get a new one.');
    case 'project_suspended': return showOnboardingError('Project suspended. Contact us.');
    case 'permission_denied': return showPermissionUi();
    case 'location_timeout':  return retryCapture();
    case 'network_error':     return retryLater();
    default:                  return reportToSentry(err);
  }
}

The vocabulary matches the backend's 4xx envelope — branchy error handling works regardless of which platform raised it.


API reference

| TS API | Key required | Description | |---|---|---| | captureCurrentPosition(options?) | No | One-shot device-side location capture. No initialize() needed — just location permission. Does not post to server. | | getPermissionStatus() | No | One-call snapshot of SDK-relevant permissions. Safe to call before initialize(). | | getRequiredPermissions(scope?) | No | Permission strings to pass to react-native-permissions for 'mandatory' or 'all' scope, version-fenced + manifest-aware. | | initialize(publishableKey, endpoints?) | Yes | Sync; sets up the singleton. No network. | | setUserId(userId \| null) | Yes | Associate fixes with an external user id. | | getDeviceId() | Yes | Stable per-install ID (dev_<32 hex>). | | verify() | Yes | GET /v1/me — pulls project info + tier. | | trackOnce(lat, lng, accuracyM?) | Yes | POST /v1/track — one-shot fix posted to server. | | startTracking(preset?) | Yes | Start the FGS / Core Location loop. | | stopTracking() | Yes | Stop tracking; FGS is removed. | | onEvents(handler) | Yes | Subscribe to geofence / motion / server events. | | bufferedCount() | Yes | Fixes queued in the offline buffer. | | drainBuffer() | Yes | Force a flush of the offline buffer. | | close() | Yes | Tear down the SDK (rare; usually leave running). | | setNotificationConfig({...}) | Yes | Override the FGS notification (Android). | | setMotionConfig({...}) | Yes | Tune the motion state machine (Android). | | setMotionStateMachineEnabled(boolean) | Yes | Disable the FSM (returns to preset-cadence tracking). | | setAdaptiveConfig(distanceMeters, intervalMs) | Yes | Tune 'adaptive' preset's OR gate. | | setAutostartOnBoot(boolean) | Yes | Respawn the FGS on device reboot (off by default). | | setDevMode(boolean) | Yes | Verbose logging + dev-only relaxations. | | isIgnoringBatteryOptimizations() | Yes | Is host exempt from Doze? iOS: always true. | | requestIgnoreBatteryOptimizations() | Yes | Show the Doze whitelist prompt. iOS: no-op. | | hasOemBatterySettings() | Yes | Is an OEM autostart screen reachable on this device? | | openOemBatterySettings() | Yes | Open the OEM autostart screen. | | getDiagnostics() | Yes | Full SDK + device diagnostic snapshot. Requires initialize(). |

Full TypeScript types live in src/types.ts. The native API is mirrored 1:1 — Android impls are in sdk/android/, iOS in sdk/ios/.


Troubleshooting

"My app is force-stopped after 10 minutes in the background." That's an OEM battery saver. Walk the user through openOemBatterySettings(). On stock Android, ensure isIgnoringBatteryOptimizations() is true. Check getDiagnostics().locationProvider — if it's 'AOSP', you're on a de-Googled ROM where background reliability is fundamentally weaker.

"Tracking works in the foreground but not background." Check getDiagnostics().permissions.backgroundLocation is true. On Android 11+, the OS routes the request to system Settings — there's no in-app dialog the user can tap through.

"startTracking('responsive') succeeds but I get one fix and then nothing for 20 minutes." The motion state machine entered Stationary. Move the device (or call setMotionStateMachineEnabled(false)). The FSM is on by default since 0.1.4; opt-out via setMotionStateMachineEnabled(false) if you need a true parked heartbeat.

"No notification appears on Android 13+." POST_NOTIFICATIONS wasn't granted. The foreground service still runs; it just renders invisibly. Request the permission with react-native-permissions, then call startTracking again.

"getDiagnostics() throws IllegalStateException." You called it before MovexGeo.initialize(...). Initialize first. For a pre-init permission check, use getPermissionStatus() instead — it doesn't require initialization.

"Events stopped arriving." For LOCAL tier, only on-device fence transitions fire onEvents. For PAID / SHARED tiers, events come from a 5 s server long-poll — confirm the device is online (getDiagnostics().bufferedFixCount will climb if offline) and that the project's API key still resolves (verify()).


Public API parity

Every TS method maps 1:1 to the native singletons. Useful if you're reading the platform SDK source.

| TS API | Android (MovexGeo object) | iOS (MovexGeo.shared) | |---|---|---| | captureCurrentPosition(options?) | MovexGeo.captureCurrentPosition(context, options) (suspend) | real impl via CLLocationManager.requestLocation() | | getPermissionStatus() | Permissions.snapshot(...) | real impl via CLAuthorizationStatus + UNUserNotificationCenter + CMMotionActivityManager | | getRequiredPermissions(scope?) | Permissions.required(scope, sdkInt) | [] — iOS uses one-shot LOCATION_WHEN_IN_USE / LOCATION_ALWAYS prompts, no cascade | | initialize(...) | initialize(...) | initialize(...) | | setUserId(...) | setUserId(...) | setUserId(...) | | getDeviceId() | deviceId | deviceId | | verify() | verify() (suspend) | verify() (async) | | trackOnce(...) | trackOnce(...) | trackOnce(...) | | startTracking(...) | startTracking(...) | startTracking(...) | | stopTracking() | stopTracking() | stopTracking() | | onEvents(...) | onEvents { ... } | onEvents { ... } | | bufferedCount() | bufferedCount | bufferedCount (async) | | drainBuffer() | drainBuffer() | drainBuffer() | | close() | close() | close() | | isIgnoringBatteryOptimizations() | BatteryOptimization.isIgnoringBatteryOptimizations(...) | stub (true) — no Doze on iOS | | requestIgnoreBatteryOptimizations() | BatteryOptimization.requestIgnoreBatteryOptimizations(...) | stub (false) — no analogue on iOS | | hasOemBatterySettings() | BatteryOptimization.hasOemSettings(...) | stub (false) — no OEM autostart on iOS | | openOemBatterySettings() | BatteryOptimization.openOemSettings(...) | stub (false) — no analogue on iOS | | getDiagnostics() | MovexGeo.diagnose() (suspend) | real impl, Apple defaults for non-applicable fields |


License

Apache 2.0 — see LICENSE. Copyright 2026 moveXgeo.

Contributing

The bridge lives in the movex-geo monorepo. See sdk/react-native/CONTRIBUTING.md for dev workflow, building against local native SDKs, and Codegen.