Examples¶
Basic tracking¶
import React, { useEffect, useRef } from 'react';
import { Button, View } from 'react-native';
import BackgroundGeolocation, {
Subscription,
} from 'react-native-background-geolocation';
export default function App() {
const subscriptions = useRef<Subscription[]>([]);
useEffect(() => {
const subs = subscriptions.current;
// Register event listeners *before* calling ready()
subs.push(
BackgroundGeolocation.onLocation(location => {
console.log('[onLocation]', location);
})
);
// ready() configures the SDK and restores persisted state.
// It does NOT start tracking β call start()/stop() separately.
BackgroundGeolocation.ready({
geolocation: {
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
distanceFilter: 10,
},
app: {
stopOnTerminate: false,
startOnBoot: true,
},
logger: {
debug: true,
logLevel: BackgroundGeolocation.LogLevel.Verbose,
},
});
return () => subs.forEach(s => s.remove());
}, []);
// e.g. a toggle-button handler in your UI
const onToggleTracking = async () => {
const state = await BackgroundGeolocation.getState();
if (state.enabled) {
BackgroundGeolocation.stop();
} else {
BackgroundGeolocation.start();
}
console.log('[state]', state.enabled, state.trackingMode);
};
return (
<View>
<Button title="Toggle Tracking" onPress={onToggleTracking} />
</View>
);
}
Generate a demo app¶
Paste the following prompt into your AI coding agent (Claude Code, Cursor, Copilot, Codex …) from an empty directory. It interviews you first, then scaffolds the app, applies every piece of native configuration, and builds a working tracking UI.
Create a working React Native demo app for the Transistor Software Background
Geolocation SDK (`react-native-background-geolocation`), then build and run it.
## Step 1 β Ask me first
Before writing any code, ask me these questions and wait for my answers. Offer
the default in brackets so I can just say "defaults".
1. **Bare React Native or Expo?** [bare React Native]
β Bare uses the community CLI and manual native config. Expo uses the
bundled config plugin and `expo prebuild`.
2. **App name?** [`BGGeoDemo`]
3. **Which platforms should I build and run?** [both iOS and Android]
β For iOS I need Xcode + CocoaPods. For Android I need a connected device or
running emulator, **and `ANDROID_HOME` exported**
(`export ANDROID_HOME=$HOME/Library/Android/sdk`) or `sdk.dir` set in
`android/local.properties` β otherwise the build fails with
"SDK location not found".
4. **Do you have a license key?** [no]
β The SDK is fully functional in **debug** builds without one. A key is only
required for release builds. A `LICENSE VALIDATION FAILURE` block appears on
every debug launch β in `adb logcat` and the Xcode console, **not** in the
build output you are watching. It is expected and harmless, and the message
itself says so.
Everything below adapts to those answers β there is one path, not two.
## Step 2 β Non-negotiable API rules
These override anything you may have seen in older tutorials, blog posts or
StackOverflow answers. Violating them produces code that does not compile.
1. **Use the v5 "Compound Config" object.** Config is grouped by domain β
`geolocation`, `app`, `http`, `logger`, `persistence`, `activity`,
`authorization`. The pre-v5 *flat* config still works at runtime but is NOT
in the TypeScript definitions, so it fails `tsc`. Never write
`ready({desiredAccuracy: ..., distanceFilter: ..., stopOnTerminate: ...})`.
2. **Use the enum constants, not the legacy `SCREAMING_CASE` ones.** Write
`BackgroundGeolocation.DesiredAccuracy.High` and
`BackgroundGeolocation.LogLevel.Verbose`. Do NOT use
`BackgroundGeolocation.DESIRED_ACCURACY_HIGH` or
`BackgroundGeolocation.LOG_LEVEL_VERBOSE` β they exist at runtime but are
absent from the shipped `.d.ts` and will fail type-checking.
**This rule applies to the config object only.** `getCurrentPosition()`
takes a `CurrentPositionRequest`, whose `desiredAccuracy` is a **number of
metres** β a different type from `Config.geolocation.desiredAccuracy`. Passing
`desiredAccuracy: 10` there is correct; passing the enum would compile but
mean something else entirely.
3. **Do NOT add anything to `AppDelegate.swift`.** The SDK's `TSBackgroundFetch`
dependency installs itself via an Objective-C `+load` and observes
`UIApplicationDidFinishLaunchingNotification`. Any instruction telling you to
call `TSBackgroundFetch.sharedInstance().didFinishLaunching()` is obsolete.
Leave the AppDelegate exactly as the template generated it.
4. **Register event listeners BEFORE calling `ready()`**, and call `ready()`
exactly once per app launch. `ready()` does not start tracking β `start()`
does. Guard the call with a module-scope boolean if you extend this app:
effects double-invoke under `React.StrictMode`, and Fast Refresh β the normal
development loop β remounts the component and would re-invoke `ready()`.
---
## Where to consult the API
If you need a name, type or default this prompt does not spell out, consult
these in order β do **not** guess from memory, and do not assume this SDK
matches its Flutter / Capacitor siblings, which differ in several places:
1. **The installed type definitions.** Most reliable, and always match the
version you resolved:
`node_modules/@transistorsoft/background-geolocation-types/dist/` β see
`core/config/GeoConfig.d.ts`, `core/config/AppConfig.d.ts`,
`core/config/LoggerConfig.d.ts`, `core/data/Location.d.ts`,
`core/api/State.d.ts`, `core/api/BackgroundGeolocation.d.ts`.
The plugin re-exports all of them from
`node_modules/react-native-background-geolocation/src/index.d.ts`.
The JSDoc in these definitions is maintained and is the source the API
reference is generated from, so its prose and code samples are safe to follow.
2. **The official API reference**, one page per symbol:
<https://docs.transistorsoft.com/react-native/> β e.g. `/react-native/GeoConfig/`,
`/react-native/Config/`, `/react-native/Location/`, `/react-native/DesiredAccuracy/`.
When checking whether a field exists on a type, follow the `extends` chain:
`State extends Config`, so much of what a returned `State` carries is inherited
rather than declared in `State.d.ts`. Grepping one file is not proof of absence.
Run `npx tsc --noEmit` to settle any question about what compiles β it is faster
and more reliable than reasoning about the types.
## Step 3 β Scaffold and configure
### Scaffold
**Bare React Native:**
```bash
npx @react-native-community/cli@latest init <AppName> --pm npm --install-pods false
cd <AppName>
npm install react-native-background-geolocation
```
**Expo:**
```bash
npx create-expo-app@latest <AppName>
cd <AppName>
npx expo install react-native-background-geolocation expo-gradle-ext-vars
```
### Native configuration
Apply these exactly as written. They are transcribed from the official
[React Native Setup guide](https://docs.transistorsoft.com/react-native/setup/) β
do not substitute values from memory or from other versions of this SDK.
**On Expo, skip the per-file steps below.** The bundled config plugin applies all
native configuration during prebuild β do not hand-edit `Info.plist`,
`AndroidManifest.xml` or Gradle files. Instead put this in `app.json` (dropping
both `license` / `TSLocationManagerLicense` entries if I have no key), then run
`npx expo prebuild`:
```json
{
"expo": {
"plugins": [
[
"react-native-background-geolocation",
{
"license": "YOUR_ANDROID_LICENSE_KEY_JWT"
}
],
[
"expo-gradle-ext-vars",
{
"googlePlayServicesLocationVersion": "21.3.0",
"tslocationmanagerVersion": "4.6.+"
}
]
],
"ios": {
"infoPlist": {
"TSLocationManagerLicense": "YOUR_IOS_LICENSE_KEY_JWT",
"UIBackgroundModes": ["location", "fetch", "processing", "audio"],
"BGTaskSchedulerPermittedIdentifiers": [
"com.transistorsoft.fetch"
],
"NSLocationAlwaysAndWhenInUseUsageDescription": "App requires location access at all times for background tracking.",
"NSLocationWhenInUseUsageDescription": "App requires location access while in use.",
"NSMotionUsageDescription": "Motion detection helps determine when the device is stationary."
}
}
}
}
```
**On bare React Native, skip the Gradle `ext` vars.** The plugin already depends
on tested, compatible versions; leaving them unset applies the defaults, which is
right for this demo. (This does not apply to the Expo block above β there
`expo-gradle-ext-vars` is how the config plugin passes those versions through, so
keep it as shown.)
#### iOS β CocoaPods
```bash
cd ios && LANG=en_US.UTF-8 bundle install && LANG=en_US.UTF-8 bundle exec pod install && cd ..
```
(The template pins CocoaPods in a `Gemfile`; React Native prints a deprecation
warning if you call `pod install` directly.)
The `LANG` is not optional. With a non-UTF-8 shell locale β common in
non-interactive agent shells β the pinned CocoaPods crashes with
`Encoding::CompatibilityError` / `unicode_normalize`. If it still fails, fall
back to the system `pod install`.
Note `pod install` rewrites `Info.plist`: it re-sorts keys, drops comments and
adds `RCTNewArchEnabled`. That is normal β do not try to undo it.
#### iOS β `ios/<AppName>/Info.plist`
**Add or replace** these keys β the template already ships an empty
`NSLocationWhenInUseUsageDescription`, so appending yields a duplicate key in the
same dict. Omit `TSLocationManagerLicense` if I have no key:
```xml
<!-- License key -->
<key>TSLocationManagerLicense</key>
<string>YOUR_LICENSE_KEY_JWT</string>
<!-- Background modes -->
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
<string>processing</string>
<string>audio</string>
</array>
<!-- Background task identifier -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.transistorsoft.fetch</string>
</array>
<!-- Location usage descriptions -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>App requires location access at all times for background tracking.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>App requires location access while in use.</string>
<key>NSMotionUsageDescription</key>
<string>Motion detection helps determine when the device is stationary.</string>
```
Before shipping to the App Store, remove `audio` from `UIBackgroundModes` unless
your app genuinely plays audio β an unused background mode is a common review
rejection. It is here so you can *hear* the SDK's debug sound FX while the app is
backgrounded (`logger.debug: true` in the app config).
`audio` is what lets you *hear* the SDK's debug sound FX while the app is
backgrounded (`logger.debug: true` in the app config). Validate the file with
`plutil -lint ios/<AppName>/Info.plist` before building.
#### Android β `android/app/build.gradle`
**Merge the `release { }` settings into the block the template already
generated** inside `android { }` β do not add a second `buildTypes` block:
```groovy
android {
buildTypes {
release {
minifyEnabled true
shrinkResources false // required
}
}
}
```
If your project uses the Kotlin DSL (`build.gradle.kts`), the equivalent is
`isMinifyEnabled = true` and `isShrinkResources = false`.
#### Android β `android/app/src/main/AndroidManifest.xml`
Only if I gave you a license key, add inside `<application>`:
```xml
<application>
<meta-data
android:name="com.transistorsoft.locationmanager.license"
android:value="YOUR_LICENSE_KEY_JWT" />
</application>
```
## Step 4 β Write the app
This app uses `react-native-safe-area-context` for safe-area insets. It ships
with the React Native template; on Expo, install it with
`npx expo install react-native-safe-area-context`. Do **not** substitute React
Native's own `SafeAreaView` β it is deprecated and is a no-op on Android, which
renders the header underneath the status bar and makes the toggle untappable.
Replace `App.tsx` with **exactly** the following file. Use it verbatim β do not
"modernise" the `ready()` config or swap the enum constants for the legacy
`SCREAMING_CASE` ones (see the Non-negotiable API rules above). If I chose Expo
and the template uses `app/` routing, put this component on the index route
instead.
```tsx
/**
* Background Geolocation demo.
*
* - Switch β BackgroundGeolocation.start() / .stop()
* - Button β BackgroundGeolocation.getCurrentPosition()
* - .onMotionChange β renders the current `isMoving` state
* - .onLocation β renders the location coords in a JSON panel
*
* @format
*/
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {
ActivityIndicator,
Platform,
ScrollView,
StatusBar,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
} from 'react-native';
import {
SafeAreaProvider,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import BackgroundGeolocation, {
Location,
LocationError,
MotionChangeEvent,
Subscription,
} from 'react-native-background-geolocation';
function App() {
return (
<SafeAreaProvider>
<StatusBar barStyle="dark-content" />
<AppContent />
</SafeAreaProvider>
);
}
function AppContent() {
const insets = useSafeAreaInsets();
const [ready, setReady] = useState(false);
const [enabled, setEnabled] = useState(false);
const [isMoving, setIsMoving] = useState<boolean | null>(null);
const [location, setLocation] = useState<Location | null>(null);
const [odometer, setOdometer] = useState(0);
const [locationCount, setLocationCount] = useState(0);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// Keep the subscriptions so they can be torn down on unmount.
const subscriptions = useRef<Subscription[]>([]);
useEffect(() => {
const subscribe = (subscription: Subscription) => {
subscriptions.current.push(subscription);
};
/// 1. Register event-listeners BEFORE calling .ready().
subscribe(
BackgroundGeolocation.onLocation(
(loc: Location) => {
console.log('[onLocation]', loc);
setError(null); // a good fix supersedes a stale error
setLocation(loc);
setOdometer(loc.odometer);
setLocationCount(count => count + 1);
},
(err: LocationError) => {
console.warn('[onLocation] ERROR:', err);
setError(`onLocation error: ${JSON.stringify(err)}`);
},
),
);
subscribe(
BackgroundGeolocation.onMotionChange((event: MotionChangeEvent) => {
console.log('[onMotionChange]', event);
setIsMoving(event.isMoving);
setLocation(event.location);
}),
);
/// 2. Configure the SDK. .ready() is called exactly once per app-launch.
/// v5 uses the "Compound Config" structure (grouped by domain).
BackgroundGeolocation.ready({
geolocation: {
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.High,
distanceFilter: 10,
stopTimeout: 5,
},
app: {
stopOnTerminate: false,
startOnBoot: true,
// Android 11+ shows this before asking for "Allow all the time".
// Without it the SDK falls back to a "[CHANGEME]" placeholder.
backgroundPermissionRationale: {
title: 'Allow background location access?',
message:
'This app records your location in the background to demonstrate the SDK.',
positiveAction: 'Change to "Allow all the time"',
negativeAction: 'Cancel',
},
},
logger: {
// Audible sound FX + a persistent notification. Turn OFF in production.
debug: true,
logLevel: BackgroundGeolocation.LogLevel.Verbose,
},
})
.then(state => {
// `state` reflects tracking state persisted across app launches.
console.log('[ready] BackgroundGeolocation is ready:', state);
setEnabled(state.enabled);
setIsMoving(state.isMoving);
setOdometer(state.odometer);
setReady(true);
})
.catch(err => {
console.error('[ready] ERROR:', err);
setError(`ready() error: ${err}`);
setReady(true);
});
/// 3. Tear down on unmount.
return () => {
subscriptions.current.forEach(subscription => subscription.remove());
subscriptions.current = [];
};
}, []);
const onToggleEnabled = useCallback(async (value: boolean) => {
setError(null);
setEnabled(value); // optimistic β reconciled with the returned State below.
try {
if (value) {
const state = await BackgroundGeolocation.start();
console.log('[start]', state);
setEnabled(state.enabled);
setIsMoving(state.isMoving);
} else {
const state = await BackgroundGeolocation.stop();
console.log('[stop]', state);
setEnabled(state.enabled);
setIsMoving(null);
setLocation(null);
setLocationCount(0);
}
} catch (err) {
console.error('[toggle] ERROR:', err);
setError(`${value ? 'start' : 'stop'}() error: ${err}`);
setEnabled(!value); // roll back the optimistic update.
}
}, []);
const onGetCurrentPosition = useCallback(async () => {
setError(null);
setBusy(true);
try {
const loc = await BackgroundGeolocation.getCurrentPosition({
samples: 2,
timeout: 30,
maximumAge: 0,
desiredAccuracy: 10,
extras: {event: 'getCurrentPosition'},
});
console.log('[getCurrentPosition]', loc);
setLocation(loc);
} catch (err) {
console.warn('[getCurrentPosition] ERROR:', err);
setError(`getCurrentPosition error: ${JSON.stringify(err)}`);
} finally {
setBusy(false);
}
}, []);
return (
<View style={styles.container}>
{/* ---------- Header ---------- */}
<View style={[styles.header, {paddingTop: insets.top + 12}]}>
<View>
<Text style={styles.title}>Background Geolocation</Text>
<Text style={styles.subtitle}>
react-native-background-geolocation
</Text>
</View>
<View style={styles.switchGroup}>
<Text style={styles.switchLabel}>{enabled ? 'ON' : 'OFF'}</Text>
<Switch
value={enabled}
onValueChange={onToggleEnabled}
disabled={!ready}
trackColor={{false: '#767577', true: '#8bc34a'}}
thumbColor={enabled ? COLORS.green : '#f4f3f4'}
/>
</View>
</View>
{!ready && (
<View style={styles.readyBanner}>
<ActivityIndicator color={COLORS.blue} />
<Text style={styles.readyBannerText}>Configuring SDKβ¦</Text>
</View>
)}
{/* ---------- isMoving (onMotionChange) ---------- */}
<View
style={[
styles.motionPanel,
{
backgroundColor:
isMoving === null
? COLORS.grey
: isMoving
? COLORS.green
: COLORS.red,
},
]}>
<Text style={styles.motionLabel}>onMotionChange Β· isMoving</Text>
<Text style={styles.motionValue}>
{isMoving === null ? 'β' : isMoving ? 'MOVING' : 'STATIONARY'}
</Text>
</View>
{/* ---------- Stats ---------- */}
<View style={styles.statsRow}>
<Stat label="onLocation" value={String(locationCount)} />
<Stat label="odometer" value={`${(odometer / 1000).toFixed(2)} km`} />
<Stat label="state" value={enabled ? 'tracking' : 'stopped'} />
</View>
{/* ---------- getCurrentPosition ---------- */}
<TouchableOpacity
style={[styles.button, (!ready || busy) && styles.buttonDisabled]}
onPress={onGetCurrentPosition}
disabled={!ready || busy}>
{busy ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>getCurrentPosition</Text>
)}
</TouchableOpacity>
{error && (
<View style={styles.errorPanel}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
{/* ---------- JSON panel (onLocation) ---------- */}
<Text style={styles.jsonHeading}>Location JSON</Text>
<JsonPanel location={location} />
<View style={{height: insets.bottom}} />
</View>
);
}
/**
* Rendered in its own memoized component so that a fast stream of `onLocation`
* events re-renders only this panel β not the header, which would otherwise
* make the Switch feel sluggish while tracking at speed.
*/
const JsonPanel = React.memo(function JsonPanel({
location,
}: {
location: Location | null;
}) {
const json = useMemo(() => {
if (!location) {
return '// Waiting for a locationβ¦\n// Toggle tracking ON or tap getCurrentPosition.';
}
return JSON.stringify(
{
uuid: location.uuid,
timestamp: location.timestamp,
is_moving: location.is_moving,
event: location.event ?? null,
odometer: Math.round(location.odometer),
coords: {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
accuracy: location.coords.accuracy,
speed: location.coords.speed,
heading: location.coords.heading,
altitude: location.coords.altitude,
},
activity: location.activity,
battery: location.battery,
},
null,
2,
);
}, [location]);
return (
<ScrollView
style={styles.jsonPanel}
contentContainerStyle={styles.jsonContent}>
<Text style={styles.json} selectable>
{json}
</Text>
</ScrollView>
);
});
function Stat({label, value}: {label: string; value: string}) {
return (
<View style={styles.stat}>
<Text style={styles.statValue}>{value}</Text>
<Text style={styles.statLabel}>{label}</Text>
</View>
);
}
const COLORS = {
header: '#fedd1e',
green: '#16BE42',
red: '#FE381E',
grey: '#777',
blue: '#337ab7',
dark: '#1a1a1a',
};
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#fff'},
header: {
backgroundColor: COLORS.header,
paddingHorizontal: 16,
paddingBottom: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {fontSize: 20, fontWeight: '700', color: '#000'},
subtitle: {fontSize: 11, color: '#5a5000', marginTop: 2},
switchGroup: {flexDirection: 'row', alignItems: 'center'},
switchLabel: {
fontSize: 12,
fontWeight: '700',
color: '#000',
marginRight: 8,
width: 28,
textAlign: 'right',
},
readyBanner: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
backgroundColor: '#eef4fa',
},
readyBannerText: {marginLeft: 8, color: COLORS.blue, fontSize: 13},
motionPanel: {paddingVertical: 18, alignItems: 'center'},
motionLabel: {
color: 'rgba(255,255,255,0.85)',
fontSize: 11,
letterSpacing: 1,
textTransform: 'uppercase',
},
motionValue: {
color: '#fff',
fontSize: 30,
fontWeight: '800',
letterSpacing: 1,
marginTop: 2,
},
statsRow: {
flexDirection: 'row',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#ddd',
},
stat: {flex: 1, alignItems: 'center', paddingVertical: 12},
statValue: {fontSize: 18, fontWeight: '700', color: COLORS.dark},
statLabel: {fontSize: 10, color: '#888', marginTop: 2},
button: {
backgroundColor: COLORS.blue,
margin: 16,
marginBottom: 8,
height: 48,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
buttonDisabled: {backgroundColor: '#aaa'},
buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'},
errorPanel: {
marginHorizontal: 16,
marginBottom: 8,
padding: 10,
borderRadius: 6,
backgroundColor: '#fdecea',
borderWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.red,
},
errorText: {color: '#a8261a', fontSize: 12},
jsonHeading: {
marginHorizontal: 16,
marginTop: 4,
marginBottom: 6,
fontSize: 11,
letterSpacing: 1,
color: '#888',
textTransform: 'uppercase',
},
jsonPanel: {
flex: 1,
marginHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
backgroundColor: COLORS.dark,
},
jsonContent: {padding: 12},
json: {
color: '#8fef9f',
// 'Courier New' does not exist on Android and silently falls back to a
// proportional font β pick a real monospace face per platform.
fontFamily: Platform.select({ios: 'Menlo', android: 'monospace'}),
fontSize: 12,
lineHeight: 17,
},
});
export default App;
```
Notes on why it is written this way, so you don't refactor the behaviour out:
- Listeners are registered **before** `ready()`, and removed on unmount.
- `start()` / `stop()` each return a `State`; the switch is reconciled from
`state.enabled` rather than assuming the call succeeded, and rolls back on throw.
- `JsonPanel` is a `React.memo` component with a `useMemo`'d `JSON.stringify`.
`onLocation` can fire several times per second at vehicle speed; without this,
every location re-renders the header and the toggle feels unresponsive.
- The JSON font is `Platform.select({ios: 'Menlo', android: 'monospace'})`.
`'Courier New'` does not exist on Android and falls back to a proportional
font with no warning.
`logger.debug: true` also asks for **notification permission** on iOS and posts
on-screen debug notifications ("π΄ Location-services OFF"). That prompt is
expected β it is the debug logger, not a bug.
## Step 5 β Verify
Type-check, then build and run on the platforms I chose. Leave Metro running in
its own terminal first β the instance spawned by `run-ios` / `run-android` dies
with the command in a non-interactive shell, leaving the app with no bundle:
```bash
npx react-native start # terminal 1, leave running
```
```bash
npx tsc --noEmit # terminal 2
npx react-native run-android # or: npx expo run:android
npx react-native run-ios # or: npx expo run:ios
```
Do not report success until the app actually launches.
Confirm at runtime that toggling the switch starts and stops tracking, that
`getCurrentPosition` returns a location, and that the `isMoving` and JSON panels
update. On the iOS Simulator use **Features β Location β Freeway Drive** to
generate movement.
Simulator caveats
activity.type is always "unknown" with confidence: 0 on the iOS
Simulator β CoreMotion's activity classifier is unavailable there. Test
motion-activity behaviour on a physical device.