Turbo Module wrapping the MotionTag tracking SDK for React Native (new architecture only).
The JS surface mirrors the official Flutter SDK so payloads are interchangeable. Bridges the iOS SDK v6.5.x and the Android SDK v7.2.x — the platform asymmetry is hidden behind a shared TS contract.
- Install in your app
- Setup with Expo (recommended)
- Setup with bare React Native
- Public API
- Pre-RN events
- Native dependency versions
- Running the example app
- Repo layout
- Developing the package
- Upgrading
- Releasing
yarn add @panter/react-native-motiontag
# or: npm install @panter/react-native-motiontagThen follow either the Expo path (recommended) or the bare React Native path below — pick the one that matches your project.
import MotionTag from '@panter/react-native-motiontag'
await MotionTag.setUserToken(jwt)
await MotionTag.start()
const active = await MotionTag.isTrackingActive()
await MotionTag.stop()
const subscription = MotionTag.addListener(event => {
if (event.type === 'transmissionError' && event.errorCode === 401) {
// re-auth flow
}
})
subscription.remove()addListener supports multiple subscribers; each addListener returns its
own EventSubscription. The MotionTagEvent discriminated union covers
started, stopped, location, transmissionSuccess, transmissionError,
authorization (iOS), powerSaveModeChanged (Android),
batteryOptimizationsChanged (Android), and a fall-through log channel
that carries the diagnostic string format the underlying SDKs emit.
The platform-only methods (isPowerSaveModeEnabled,
isBatteryOptimizationsEnabled on Android; getWifiOnlyDataTransfer /
setWifiOnlyDataTransfer / clearData on Android) resolve to safe defaults
(false) or reject ('UNSUPPORTED') on iOS, matching the Flutter SDK's
behaviour.
The package ships an Expo config plugin that wires up everything for you on
expo prebuild: AppDelegate bootstrap (iOS), MainApplication bootstrap
(Android), Info.plist permission + background-mode keys, foreground-service
notification factory, the Azure DevOps Maven repo, and the extra Android
permissions (POST_NOTIFICATIONS, FOREGROUND_SERVICE).
Add the plugin to app.json:
Then run npx expo prebuild --clean followed by npx expo run:ios /
npx expo run:android. Expo Go is not supported — the library has native
code, you need a development client build.
The MotionTag SDKs need to be initialised before React Native starts.
Turbo modules are instantiated lazily on first JS access, so they cannot run
this themselves — the host app must call a small bootstrap from its
AppDelegate (iOS) and Application.onCreate (Android).
import RNMotionTag
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
MotionTagBootstrap.bootstrap(launchOptions: launchOptions)
// … rest of RN bootstrap …
}
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
MotionTagBootstrap.processBackgroundSessionEvents(
identifier: identifier,
completionHandler: completionHandler
)
}The host's Info.plist must declare:
NSLocationAlwaysAndWhenInUseUsageDescription,NSLocationWhenInUseUsageDescription,NSMotionUsageDescription— user-facing copy stays host-owned.UIBackgroundModescontaininglocation,fetch,processing.BGTaskSchedulerPermittedIdentifierscontainingcom.motiontag.sdk.backgroundrefreshandcom.motiontag.sdk.backgroundtask(required from SDK 6.5.0).FirebaseAppDelegateProxyEnabled = falseif the app uses Firebase, so its swizzling doesn't interfere with MotionTag's background URLSession.
Tested with both CocoaPods' default static-library linkage and
use_frameworks! :linkage => :static (commonly enabled by Firebase,
MapBox, and other Swift-only iOS SDKs) — no host-side workaround needed
in either mode.
import de.motiontag.reactnative.MotionTagBootstrap
override fun onCreate() {
super.onCreate()
loadReactNative(this)
MotionTagBootstrap.init(this, createNotification())
}The host owns the foreground-service Notification (channel id, title,
text, icon) — the package does not impose copy or branding. Required
SDK permissions (ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION,
ACCESS_BACKGROUND_LOCATION, ACTIVITY_RECOGNITION,
FOREGROUND_SERVICE_LOCATION) are merged into the host manifest by Gradle.
Events that fire between native init (in MotionTagBootstrap.init /
MotionTagBootstrap.bootstrap) and the JS subscription being installed are
dropped — only the diagnostic log line goes to logcat / Console. In
practice this is rare and only affects authorization-status changes; the
3-second polling reconciler in useTrackingChecks re-establishes state.
| Platform | Version | Source |
|---|---|---|
| iOS | MotionTagSDK ~> 6.5.0 |
CocoaPods trunk (transitive from this pod) |
| Android | de.motiontag:tracker:7.2.5 |
pkgs.dev.azure.com/motiontag/releases (Maven repo declared in this package) |
The two SDKs are intentionally out of sync — aligning them is tracked as a follow-up.
The example/ folder is a working Expo dev-client app that exercises the
package end-to-end: paste a JWT, request permissions, start tracking, watch
events stream in.
Requirements: Node 18+, Xcode for iOS, Android Studio for Android. Expo Go won't work — the package has native code, so you need a development client build.
git clone https://github.com/panter/react-native-motiontag
cd react-native-motiontag
yarn install # installs root devDeps + runs `bob build`
cd example
yarn install # installs Expo deps + links the parent
npx expo prebuild --clean # runs the config plugin, generates ios/ + android/
npx expo run:ios # build + boot iOS simulator
# or
npx expo run:android # needs an Android emulator running firstAfter the first run, the dev loop is:
cd example
npx expo start --dev-client # Metro only; reuses the installed appPress r to reload the JS bundle. Edits to App.tsx, src/, or the
library's src/index.ts hot-reload via Fast Refresh. Native (Swift/Kotlin)
edits require another expo run:ios / expo run:android.
For real motion-tracking validation use a physical device
(npx expo run:ios --device) — simulators can't generate motion-sensor
data and only fake locations via Xcode's debug-location menu / the Android
emulator's location controls.
react-native-motiontag/
├── src/ # TS source (the JS API surface)
├── android/ # Android Turbo Module (Kotlin, AAR)
├── ios/ # iOS Turbo Module (Swift + ObjC++)
├── plugin/ # Expo config plugin (plain JS, no build)
├── app.plugin.js # Plugin entrypoint loaded by `expo prebuild`
├── react-native-motiontag.podspec
├── package.json # Published npm package
└── example/ # Standalone Expo demo app consuming the package
The library and the example are independent npm packages. The example declares the parent via a relative path:
{ "dependencies": { "@panter/react-native-motiontag": "file:.." } }When you yarn install in example/, that resolves as a symlink to the
parent — edit library source and changes show up in the example without a
re-install.
- Build the library:
yarn installat the root runsreact-native-builder-bobvia thepreparescript. Output lands inlib/{commonjs,module,typescript}. These are produced for npm consumers; the example usessrc/directly via thereact-nativefield inpackage.json. - Type-check the source:
npx tsc -p tsconfig.build.json --noEmit. - Dedup peer deps:
react/react-nativeare installed both at the root (devDeps, forbob build+ tsc) and inexample/(runtime). Metro is configured inexample/metro.config.jsto block the root copies and redirect every'react-native'/'react'import to the example's copy — a single physical instance at runtime. .npmrcat root setslegacy-peer-deps=trueso npm doesn't try to auto-install our declared peers when someone runsnpm installat the library root.- Iterate on the Expo config plugin by editing files in
plugin/and re-runningnpx expo prebuild --cleaninexample/. The injected blocks are wrapped in@generatedmarkers and de-duplicated on re-run.
The repo has four independent upgrade axes — keep them as separate PRs / commits so release-please classifies each one correctly. The iOS and Android SDK versions drift on purpose (see AGENTS.md) — don't align them just because they're both bumpable.
| What | Source |
|---|---|
| MotionTag iOS SDK changelog | api.motion-tag.de/developer/ios?locale=en&os_aspect=changelog |
| MotionTag iOS integration guide | api.motion-tag.de/developer/ios?locale=en&os_aspect=sdk |
| MotionTag Android SDK changelog | api.motion-tag.de/developer/android?locale=en&os_aspect=changelog |
| MotionTag Android integration guide | api.motion-tag.de/developer/android?locale=en&os_aspect=sdk |
| Expo SDK upgrade walkthrough | docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough |
| Expo config-plugins changelog | github.com/expo/expo/.../config-plugins/CHANGELOG.md |
| react-native-builder-bob releases | github.com/callstack/react-native-builder-bob/releases |
| React Native upgrade helper (rarely needed here) | react-native-community.github.io/upgrade-helper |
The two MotionTag changelog endpoints are the authoritative source — the vendor
doesn't publish a GitHub release feed. Both locale=en and locale=de work.
| Axis | Pinned in | Currently |
|---|---|---|
| iOS SDK | react-native-motiontag.podspec |
MotionTagSDK ~> 6.5.0 |
| Android SDK | android/build.gradle |
de.motiontag:tracker:7.2.5 |
| Example Expo SDK | example/package.json |
expo ~55 |
| Library build tooling | package.json |
react-native-builder-bob, @expo/config-plugins, typescript |
A version bump is not "just" a version bump when the changelog touches:
- iOS
Info.plistkeys (especiallyBGTaskSchedulerPermittedIdentifiersandUIBackgroundModes) → update both the bare-RN snippet above and the Expo plugin's Info.plist injection inplugin/. - iOS bootstrap signature (
MotionTagBootstrap.bootstrap,processBackgroundSessionEvents) → updateios/MotionTagBootstrap.swift, the README snippet, and the plugin's AppDelegate injection. - Android manifest permissions or foreground-service contract → update
android/src/main/AndroidManifest.xml, the plugin's manifest edits, and the Android section above. - Android bootstrap signature (
MotionTagBootstrap.init) → update the Kotlin source, the README snippet, and the plugin's MainApplication injection.
Treat these as feat: (or feat!: if hosts must change code) rather than
fix: so release-please bumps the minor/major correctly.
The library itself is SDK-agnostic — only example/ pins an Expo version:
cd example
npx expo install expo@<target>
npx expo install --fix # realigns react, react-native, expo-*
npx expo prebuild --clean # mandatory across SDK majors
npx expo run:ios && npx expo run:androidIf the new Expo SDK pulls in an RN version above this package's peer range
(react-native >=0.79.0), widen the peer range in the root package.json in
the same PR. Don't tighten the lower bound.
After any axis bump:
yarn install && yarn prepare # at the root
npx tsc -p tsconfig.build.json --noEmit # typecheck
cd example && yarn install
npx expo prebuild --clean
npx expo run:ios # ideally on a device
npx expo run:android # ideally on a deviceWalk the golden path in the example: paste JWT → start → events stream → stop. Simulators can't generate motion-sensor data, so a physical device is the only way to fully validate a MotionTag SDK bump.
Releases are fully automated via release-please
and npm Trusted Publishing — no manual npm publish, no tokens, no
hand-edited changelog.
- Land commits on
mainusing Conventional Commits:feat: …— minor bump (0.1.0→0.2.0)fix: …— patch bump (0.1.0→0.1.1)feat!: …orBREAKING CHANGE:in body — major bumpchore:,docs:,refactor:,test:,ci:— no bump, but appear in the changelog under their respective sections
- The Release Please workflow opens (or updates) a PR titled
chore(main): release X.Y.Z. The PR contains the version bump inpackage.json, an updatedCHANGELOG.mdassembled from the commit subjects since the last release, and an updated.release-please-manifest.json. - Merge that PR when you're ready to release. Release-please then creates
the git tag (
vX.Y.Z) and a matching GitHub Release with the changelog body. - The
publishjob in the same workflow runsyarn prepare(bob build) andnpm publishwith npm provenance attached via OIDC.
On npmjs.com → @panter/react-native-motiontag → Settings →
Trusted Publisher → Add:
- Publisher: GitHub Actions
- Organization:
panter - Repository:
react-native-motiontag - Workflow filename:
release-please.yml - Environment: (leave blank)
No NPM_TOKEN secret is required.
If you need to publish a version that release-please can't produce (e.g. a
hotfix from a non-main branch), bump package.json + push manually:
yarn prepare
npm publish --access publicYou'll need a local npm auth token for the manual path — Trusted Publishing only covers the GitHub Actions workflow.
{ "expo": { "newArchEnabled": true, "plugins": [ [ "@panter/react-native-motiontag", { "iosPermissions": { "locationAlwaysAndWhenInUse": "We use your location to track your trips.", "locationWhenInUse": "We use your location to track your trips.", "motion": "We use motion data to detect transport modes." }, "androidNotification": { "channelId": "motiontag_tracking", "channelName": "Tracking", "title": "MyApp", "text": "Tracking is active" } } ] ] } }