@uselira/core
v1.1.0
Published
React Native wrapper for the Uselira SDK
Maintainers
Readme
@uselira/core
React Native bridge for the Uselira SDK — a background location-tracking library that verifies a customer's residential address over a configurable period. The bridge wraps the native Android (Kotlin) and iOS (Swift) SDKs and exposes a single, promise-based JavaScript API.
Built by Lira Inc.
Table of Contents
- Requirements
- Installation
- Quick Start
- API Reference
- Configuration Reference
- Permissions
- Error Handling
- Troubleshooting
- Architecture Notes
Requirements
| Requirement | Minimum version | |-------------------|-----------------| | React Native | 0.73.0 | | Android | API 21 (Android 5.0) | | iOS | 16.0 | | Node | 18+ |
The library supports both the Old Architecture (Bridge) and New Architecture (TurboModules / Fabric). No additional Codegen configuration is required in the consuming app.
Installation
# npm
npm install @uselira/core
# yarn
yarn add @uselira/coreAndroid Setup
1. Add the native SDK dependency
The Uselira Android SDK is distributed as a Gradle library. Add it to your project using a composite build so it keeps its own Gradle settings:
In android/settings.gradle, add:
// Lira Address Verify Android SDK
includeBuild('../path/to/lira-android-sdk') {
dependencySubstitution {
substitute(module('com.uselira.sdk:core')).using(project(':sdk'))
}
}Replace ../path/to/lira-android-sdk with the actual path to the SDK on your machine or CI environment. If the SDK is published to a Maven repository instead, add the repository to your android/build.gradle and declare the dependency normally:
// android/build.gradle (project-level)
allprojects {
repositories {
maven { url 'https://repo.uselira.com/android' } // example URL
google()
mavenCentral()
}
}2. Add location permissions to AndroidManifest.xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required: coarse location for network-based fixes -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Required: fine location for GPS fixes -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Required: background location for tracking during the verification period -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Required: network access to sync location data -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Recommended: lets the SDK survive device restarts -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Recommended: prevents the OS from killing tracking in low-memory conditions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application ...>
...
</application>
</manifest>Note on Android 10+ (API 29+):
ACCESS_BACKGROUND_LOCATIONmust be requested separately from foreground location — Android will show a second prompt that takes the user to system settings. Request foreground location first, then request background. The Lira setup flow guides users through this automatically.
Note on Android 12+ (API 31+): You must declare
android:foregroundServiceType="location"on any<service>elements used by the SDK. The SDK handles this internally — no additional manifest changes are needed in your app.
3. Request runtime permissions
The SDK setup flow will prompt the user through the location consent screens, but you must request the initial location permission before calling launchSetup. Use the react-native-permissions library or React Native's built-in PermissionsAndroid:
import { PermissionsAndroid, Platform } from 'react-native';
async function requestLocationPermissions() {
if (Platform.OS !== 'android') return;
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
]);
const fineGranted =
granted[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION] ===
PermissionsAndroid.RESULTS.GRANTED;
if (!fineGranted) {
throw new Error('Location permission denied');
}
}Background location (ACCESS_BACKGROUND_LOCATION) is handled by the Lira SDK's setup UI on Android 10+.
iOS Setup
1. Set the iOS deployment target and enable dynamic frameworks
UseliraCore requires iOS 16+ and ships as a binary .xcframework. Two
edits to your app's ios/Podfile are required before pod install will work:
- Set
platform :ios, '16.0'. The default RN scaffold usesmin_ios_version_supported(~13.4 on RN 0.74), which failspod installwith a platform-mismatch error againstRNUselira. - Use dynamic linking. Either run
USE_FRAMEWORKS=dynamic pod install, or adduse_frameworks! :linkage => :dynamicnear the top of the Podfile. Without dynamic linking, the Swift build fails withNo such module 'UseliraCore'even though the pod is installed — static linking does not surface a binary xcframework's Swift module to dependent pods.
2. Add the native SDK via CocoaPods
In your app's ios/Podfile:
platform :ios, '16.0' # see step 1
use_frameworks! :linkage => :dynamic # see step 1
target 'YourApp' do
config = use_native_modules!
# Lira Address Verify iOS SDK — adjust the path to where the SDK lives locally
pod 'UseliraCore', :path => '../path/to/lira-ios-sdk'
use_react_native!(
:path => config[:reactNativePath],
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
endThen install pods:
cd ios && pod installIf the SDK is published to a private CocoaPods spec repo, replace the
:pathreference with a version specifier and ensure yourPodfilesources that spec repo:source 'https://cdn.cocoapods.org/' source 'https://specs.uselira.com' # example private source pod 'UseliraCore', '~> 1.0'
3. Add location usage descriptions to Info.plist
iOS requires user-facing descriptions for every location permission your app requests. Add these keys to ios/YourApp/Info.plist:
<!-- ios/YourApp/Info.plist -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Lira needs your location to confirm your residential address.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Lira needs continuous access to your location to verify your address over the required period.</string>
<!-- Required for iOS 13 and below; ignored but harmless on iOS 14+ -->
<key>NSLocationAlwaysUsageDescription</key>
<string>Lira needs continuous access to your location to verify your address over the required period.</string>Failure to include these keys will cause a crash when the SDK first requests location access.
4. Enable background modes
In Xcode, select your app target → Signing & Capabilities → + Capability → Background Modes, and enable:
- Location updates — required for background tracking
Or add this directly to your Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>5. Wire the AppDelegate for hands-free reboot resume
To let a verification job resume after a device reboot without the user opening
the app, the host AppDelegate must forward launch to the SDK. iOS only
relaunches a terminated app in the background for the SDK's
significant-location-change monitor when this hook runs on launch. Without it, a
job interrupted by a reboot resumes only the next time the user opens the app.
Objective-C (AppDelegate.mm — the default React Native template):
#import <UseliraCore/UseliraCore-Swift.h>
// inside application:didFinishLaunchingWithOptions:, before [super ...]
[UseliraLaunch didFinishLaunching:launchOptions];Use the
#importform (not@import UseliraCore;): React Native'sAppDelegate.mmis Objective-C++, where Clang module imports are disabled by default, so@importfails to compile. The generated header import works in both Objective-C and Objective-C++.
Swift (AppDelegate.swift):
import UseliraCore
// inside application(_:didFinishLaunchingWithOptions:)
Uselira.didFinishLaunching(launchOptions)The call is a no-op when no verification job is persisted, so it is safe to run on every launch.
Requirements for the hands-free (background) path:
UIBackgroundModesincludeslocation(see step 4).- An
NSLocationAlwaysAndWhenInUseUsageDescriptionstring (see step 3). - The user grants Always location authorization.
Under When-In-Use authorization the job still resumes — but only when the app is next opened (the documented fallback), since iOS will not relaunch the app in the background without Always access.
Quick Start
import React, { useEffect, useRef, useState } from 'react';
import { Button, View, Text } from 'react-native';
import {
launchSetup,
stop,
addLocationListener,
type TrackedLocation,
} from '@uselira/core';
export default function App() {
const [jobId, setJobId] = useState<string | null>(null);
const subscriptionRef = useRef<ReturnType<typeof addLocationListener> | null>(null);
useEffect(() => {
// Register location listener before setup so no events are missed
subscriptionRef.current = addLocationListener((location: TrackedLocation) => {
console.log('Location recorded:', location);
});
return () => {
// Always remove the listener and stop tracking on unmount
subscriptionRef.current?.remove();
stop();
};
}, []);
const handleStart = async () => {
try {
const result = await launchSetup(
{
apiKey: 'YOUR_API_KEY',
customerRef: 'CUSTOMER_UNIQUE_ID',
duration: 14,
environment: 'sandbox',
},
{
theme: { brand: { accent: '#1565C0' } },
}
);
if (result) {
setJobId(result.jobId);
console.log('Verification started, job ID:', result.jobId);
} else {
console.log('User cancelled setup');
}
} catch (err) {
console.error('Setup failed:', err);
}
};
const handleStop = () => {
stop();
setJobId(null);
};
return (
<View>
{jobId && <Text>Active job: {jobId}</Text>}
<Button title="Start Verification" onPress={handleStart} />
<Button title="Stop" onPress={handleStop} />
</View>
);
}API Reference
launchSetup
function launchSetup(
config: SdkConfig,
uiConfig?: UiConfig
): Promise<{ jobId: string } | null>Launches the native Lira verification setup flow. This presents a full-screen native UI that walks the user through address confirmation, a consent checkbox, and location permission prompts.
Returns:
{ jobId: string }— setup completed successfully. Store thisjobIdon your server to track the verification.null— the user cancelled the setup flow without completing it.- Throws (rejects) on configuration errors or unexpected native failures.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| config | SdkConfig | Yes | SDK and tracking configuration. See SdkConfig. |
| uiConfig | UiConfig | No | UI customisation options. Defaults to the SDK's built-in theme. See UiConfig. |
Example:
const result = await launchSetup(
{
apiKey: 'your-api-key',
customerRef: 'customer-unique-ref',
duration: 14,
environment: 'sandbox',
scheduleStart: '22:00',
scheduleEnd: '06:00',
},
{
theme: {
brand: { accent: '#0D47A1', onAccent: '#FFFFFF' },
copy: {
reviewSaveButton: 'Yes, this is my address',
continueButtonText: 'I understand, continue',
},
},
}
);
if (result) {
// result.jobId — persist this
} else {
// user tapped cancel
}stop
function stop(): voidStops all background location tracking and clears SDK state. Safe to call multiple times. Should be called when the verification period ends or when the user explicitly cancels.
Example:
import { stop } from '@uselira/core';
stop();Call stop() in your component's useEffect cleanup to ensure tracking halts when the component unmounts:
useEffect(() => {
return () => {
stop();
};
}, []);addLocationListener
function addLocationListener(
callback: (location: TrackedLocation) => void
): EmitterSubscriptionSubscribes to location events emitted by the SDK during the tracking period. Returns an EmitterSubscription that must be removed when no longer needed to prevent memory leaks.
Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| callback | (location: TrackedLocation) => void | Called each time the SDK records a new location fix. |
Returns: An EmitterSubscription with a .remove() method.
Example:
import { addLocationListener, type TrackedLocation } from '@uselira/core';
const subscription = addLocationListener((location: TrackedLocation) => {
console.log(`Lat: ${location.latitude}, Lng: ${location.longitude}`);
console.log(`Accuracy: ±${location.accuracyMeters}m via ${location.sourceType}`);
console.log(`Time: ${new Date(location.timestampMs).toISOString()}`);
if (location.powerEvent) {
console.log('This fix was triggered by a power state change');
}
});
// Later, when done:
subscription.remove();Important: Register the listener before calling
launchSetupto ensure no location events are missed during the setup flow itself.
getStatus
function getStatus(): Promise<VerificationStatus>Reads a cheap snapshot of the current verification state — no network, just native UserDefaults / SharedPreferences plus in-memory tracker state. Safe to call on every screen mount. Use it to gate host UI between a "start verification" CTA, an in-progress view, and a completion view, and to answer "is the SDK tracking right now, and if not, why?".
New in 1.1.0: the bridge attaches its delegates and runs boot-restore at module init, so a reboot-interrupted verification keeps running and getStatus() reflects it even on a cold launch before any JS subscribes.
Example:
import { getStatus } from '@uselira/core';
const status = await getStatus();
if (status.isActive) {
// Verifying — night {status.nightsCompleted} of {status.duration}
if (!status.inWindow) console.log('Outside tonight\'s tracking window');
if (!status.batteryOk) console.log('Paused — battery below threshold');
if (status.permissionState === 'whenInUse')
console.log('Needs "Always" location to track in the background');
} else if (status.terminalReason) {
console.log(`Verification ended: ${status.terminalReason}`);
}See VerificationStatus for the full field list.
addAuditListener
function addAuditListener(
callback: (event: AuditEvent) => void
): EmitterSubscriptionSubscribes to the SDK's structured audit log — typed lifecycle events
(tracking_started, tracking_paused, job_completed, boot_resumed,
upload_failed, and more) emitted on the Uselira_auditEvent channel.
Returns an EmitterSubscription; call .remove() when done.
Because the native bridge attaches its audit delegate eagerly at module
init, events fired during a cold-boot resume are persisted and can be
recovered via getEventLog even if they
fired before this listener was registered.
Example:
import { addAuditListener, type AuditEvent } from '@uselira/core';
const sub = addAuditListener((event: AuditEvent) => {
console.log(`[audit] ${event.kind} seq=${event.sequence}`, event.payload);
});
// Later:
sub.remove();getEventLog / clearEventLog
function getEventLog(opts?: {
sinceMs?: number; // only events at/after this epoch-ms
limit?: number; // default 200, max 1000
jobId?: string; // scope to one job
}): Promise<AuditEvent[]>
function clearEventLog(opts?: { beforeMs?: number }): Promise<void> // omit beforeMs to clear allgetEventLog reads the persisted audit log — use it after a cold launch
to reconstruct what the SDK did while the JS layer was dead.
clearEventLog prunes old rows once you've consumed them.
Example:
import { getEventLog, clearEventLog } from '@uselira/core';
// On app start, replay everything since we last checked:
const events = await getEventLog({ sinceMs: lastSeenMs });
events.forEach((e) => console.log(e.kind, e.timestampMs, e.payload));
// Then prune what we've processed:
await clearEventLog({ beforeMs: Date.now() });Types
TrackedLocation
Represents a single recorded location fix.
interface TrackedLocation {
latitude: number; // WGS-84 decimal degrees
longitude: number; // WGS-84 decimal degrees
accuracyMeters: number; // horizontal accuracy radius in metres
sourceType: 'gps' | 'network' | 'passive'; // location provider
timestampMs: number; // UTC epoch timestamp in milliseconds
powerEvent: boolean; // true if triggered by a device power state change (charge/discharge)
}| Field | Description |
|-------|-------------|
| latitude | Decimal degrees, WGS-84. Positive = north, negative = south. |
| longitude | Decimal degrees, WGS-84. Positive = east, negative = west. |
| accuracyMeters | The estimated horizontal accuracy radius. 68% confidence that the true position is within this radius. |
| sourceType | The location provider that generated this fix. 'gps' = satellite, 'network' = cell/Wi-Fi, 'passive' = piggybacked on another app's request. |
| timestampMs | When the fix was acquired, as a Unix timestamp in milliseconds. |
| powerEvent | true when the fix was triggered by the device being plugged in or unplugged (used to detect home presence patterns). |
AuditEvent
A single row in the SDK's structured audit log (new in 1.1.0). The
shape is identical across SDK persistence, the live listener payload, and
the Lira backend.
type AuditKind =
| 'setup_failed' | 'tracking_started' | 'tracking_paused'
| 'boot_resumed' | 'boot_resume_skipped' | 'night_completed'
| 'job_completed' | 'job_cancelled' | 'job_stopped'
| 'upload_failed' | 'audit_log_overflow';
interface AuditEvent {
id: string; // ULID, monotonic, idempotency key
sequence: number; // per-job, gap-free, restart-safe
kind: AuditKind;
jobId: string | null; // null pre-launchSetup
timestampMs: number; // when it happened on-device
payload: Record<string, unknown>; // discriminated by `kind`
}The payload is keyed by kind — e.g. tracking_started carries
{ trigger: 'window_open' | 'battery_recovered' | ... } and
tracking_paused carries { reason: 'window_close' | 'low_battery' | 'permission_revoked' }. The sequence is gap-free per job, so a host can detect dropped events.
VerificationStatus
Returned by getStatus. Cheap to read; no network.
type PermissionState = 'always' | 'whenInUse' | 'denied' | 'unknown';
type TerminalReason = 'duration_elapsed' | 'cancelled' | 'stopped' | 'hard_cap';
interface VerificationStatus {
// Present in every release ≥ 1.1.
isActive: boolean;
jobId: string | null;
nightsCompleted: number;
duration: number;
nightsRemaining: number; // derived: max(0, duration - nightsCompleted)
// New in 1.1.
startedAt: number | null; // ms-since-epoch the verification started
lastSampleAt: number | null; // ms-since-epoch of the freshest fix (heartbeat)
lastUploadAt: number | null; // ms-since-epoch of the last successful upload
terminalReason: TerminalReason | null; // null while active or if no job ran
inWindow: boolean; // computed: current time inside the nightly window
batteryOk: boolean; // computed: battery currently above threshold
permissionState: PermissionState; // computed: current location grant
}| Field | Description |
|-------|-------------|
| isActive | true while a verification is running. |
| terminalReason | Why tracking stopped, or null while active / if none ran. |
| startedAt / lastSampleAt / lastUploadAt | Persisted timestamps — survive a reboot, so you can reconstruct activity after a cold launch. |
| inWindow / batteryOk / permissionState | Computed at read time — answer "why isn't it sampling right now?". |
Configuration Reference
SdkConfig
Controls tracking behaviour and SDK credentials.
interface SdkConfig {
apiKey: string;
customerRef: string;
duration: number; // required — number of nights to observe (1–90)
scheduleStart?: string;
scheduleEnd?: string;
batteryThreshold?: number;
sampleInterval?: number;
syncInterval?: number;
environment: 'sandbox' | 'production'; // required
telemetry?: 'lira' | 'none'; // default 'none'
}| Field | Type | Default | Description |
|-------|------|---------|-------------|
| apiKey | string | required | Your Lira API key. Obtain this from the Lira dashboard. |
| customerRef | string | required | A unique identifier for this customer in your system (e.g. user ID, application number). Used to correlate location data with a specific verification job on the Lira server. Must be non-empty. |
| scheduleStart | string | "20:00" | The time of day (24-hour "HH:MM") at which the SDK begins collecting location samples. Choose a time when the customer is likely to be at home. |
| scheduleEnd | string | "06:00" | The time of day (24-hour "HH:MM") at which the SDK stops collecting samples. Together with scheduleStart, this defines the nightly tracking window. If scheduleEnd is earlier in the clock than scheduleStart, the window spans midnight (e.g. 20:00–06:00 = 10-hour overnight window). |
| batteryThreshold | number | 15 | Battery percentage (1–99) below which the SDK suspends tracking to avoid draining the device. Tracking resumes automatically when the battery level rises. |
| sampleInterval | number | 900000 | How frequently (in milliseconds) the SDK attempts to record a location fix during the tracking window. Lower values give more data points but use more battery. Minimum recommended: 30,000 (30 seconds). Default: 900,000 (15 minutes). |
| syncInterval | number | 900000 | How frequently (in milliseconds) the SDK uploads buffered location data to the Lira server. Buffering is used to reduce network usage. Default: 900,000 (15 minutes). |
| duration | number | required | The total number of nights (1–90) over which the verification runs. After this many overnight tracking windows have elapsed, the SDK automatically stops and the job is finalised on the Lira server. |
| environment | 'sandbox' \| 'production' | required | Which Uselira API the SDK talks to. Forwarded to the native LiraEnvironment on both platforms. No default — the developer must choose, so a sandbox build can't accidentally ship to production. Missing or unrecognised values reject launchSetup with INVALID_CONFIG. |
| telemetry | 'lira' \| 'none' | 'none' | Opt-in upload of the on-device audit log. 'lira' ships persisted audit-event rows to Lira alongside location samples; 'none' keeps them on-device only (still readable via getEventLog). Unknown values fall back to 'none'. |
Example — overnight tracking, 30-second samples:
const config: SdkConfig = {
apiKey: 'lira_live_xxxxxxxxxxxx',
customerRef: 'user-8f3a2c',
environment: 'production',
scheduleStart: '21:00',
scheduleEnd: '05:00',
sampleInterval: 30_000,
syncInterval: 60_000,
duration: 21,
batteryThreshold: 20,
};UiConfig
Controls the appearance of the native setup UI. All fields are optional — omitting theme entirely uses the Lira brand defaults.
interface UiConfig {
theme?: LiraTheme;
/** @deprecated Use `theme.brand.accent` */
primaryColor?: string;
/** @deprecated Use `theme.mapStyleUrl` */
mapStyleUrl?: string;
/** @deprecated Use `theme.copy.confirmButtonText` */
confirmButtonText?: string;
/** @deprecated Use `theme.copy.continueButtonText` */
continueButtonText?: string;
}LiraTheme
interface LiraTheme {
brand?: BrandTokens;
pastels?: Partial<Record<PastelKey, string>>;
copy?: CopyTokens;
mapStyleUrl?: string;
}| Field | Description |
|-------|-------------|
| brand | Role-based colour overrides. See BrandTokens. |
| pastels | Pastel tile accent colours used in the setup cards. See PastelKey. |
| copy | Text overrides for every screen in the setup flow. See CopyTokens. |
| mapStyleUrl | URL of a MapLibre-compatible style JSON for the address confirmation map. |
BrandTokens
interface BrandTokens {
primary?: string;
onPrimary?: string;
surface?: string;
onSurface?: string;
onSurfaceMuted?: string;
onSurfaceHint?: string;
accent?: string;
onAccent?: string;
divider?: string;
}| Token | Role |
|-------|------|
| primary | Main background colour (hero panel, nav bar). Hex string, e.g. "#011329". |
| onPrimary | Text/icons on a primary background. |
| surface | Card and sheet background. |
| onSurface | Primary text on surface. |
| onSurfaceMuted | Secondary / dimmed text. |
| onSurfaceHint | Placeholder and hint text. |
| accent | Conversion colour — primary CTA button fill. |
| onAccent | Text/icons on an accent background. |
| divider | Separator lines. Supports 8-digit hex with alpha, e.g. "#5C5F6619". |
PastelKey
Pastel tiles are small coloured squares used as data-row decorations inside setup cards. Six named slots can be individually overridden:
type PastelKey = 'peach' | 'cyanTint' | 'blueTint' | 'pinkTint' | 'yellowTint' | 'limeTint';theme: {
pastels: {
limeTint: '#D4F1A0',
peach: '#FFD8C0',
},
}CopyTokens
Fine-grained text overrides for every screen and button in the setup flow:
interface CopyTokens {
// Welcome screen
welcomeEyebrow?: string;
welcomeHeadline?: string;
welcomeSubheadline?: string;
continueButtonText?: string;
// Permission prompt screen
permissionEyebrow?: string;
permissionHeadline?: string;
permissionSubheadline?: string;
permissionContinueButton?: string;
// Address picker screen
pickerEyebrow?: string;
pickerSearchPlaceholder?: string;
confirmButtonText?: string;
// Address review / consent screen
reviewEyebrow?: string;
reviewHeadline?: string;
reviewSaveButton?: string; // primary "confirm" CTA
reviewEditPinButton?: string; // secondary "edit pin" CTA
// Setup complete screen
startedEyebrow?: string;
startedHeadline?: string;
startedDoneButton?: string;
}Example — branded theme:
const uiConfig: UiConfig = {
theme: {
brand: {
primary: '#011329',
accent: '#E65100',
onAccent: '#FFFFFF',
},
copy: {
reviewSaveButton: 'Yes, this is my home',
startedDoneButton: 'Got it, start verification',
},
},
};Permissions
The SDK requires location permissions to be granted before launchSetup is called. The Lira setup UI guides the user through the location consent flow, but the initial "when in use" permission must already be granted by your app before opening the setup.
Android Permissions
| Permission | Required | Purpose |
|------------|----------|---------|
| ACCESS_FINE_LOCATION | Yes | GPS-accuracy location fixes |
| ACCESS_COARSE_LOCATION | Yes | Network-based location fixes |
| ACCESS_BACKGROUND_LOCATION | Yes | Tracking during the overnight window when the app is not in the foreground |
| INTERNET | Yes | Syncing location data to the Lira server |
| RECEIVE_BOOT_COMPLETED | Recommended | Restarts tracking after a device reboot |
| FOREGROUND_SERVICE | Recommended | Keeps the tracking service alive in low-memory conditions |
| FOREGROUND_SERVICE_LOCATION | Recommended | Required on Android 14+ for foreground services that access location |
Request flow (Android 10+):
- Request
ACCESS_FINE_LOCATION(foreground) — standard runtime dialog. - After the user grants foreground location, the Lira setup UI prompts for
ACCESS_BACKGROUND_LOCATION— Android takes the user to system settings for this.
Never request background location before foreground location — Android will silently ignore or reject it.
iOS Permissions
| Key | Required | Purpose |
|-----|----------|---------|
| NSLocationWhenInUseUsageDescription | Yes | Required to show the initial location prompt |
| NSLocationAlwaysAndWhenInUseUsageDescription | Yes | Required to request "Always" permission for background tracking |
| NSLocationAlwaysUsageDescription | Yes (iOS 13 and below) | Legacy key for "Always" permission |
| Background mode: location | Yes | Allows tracking when the app is suspended or in the background |
Request flow (iOS):
iOS will show two prompts in sequence:
- "Allow location while using the app" / "Allow once" — first prompt uses
NSLocationWhenInUseUsageDescription. - A follow-up prompt or system settings redirect for "Always Allow" — uses
NSLocationAlwaysAndWhenInUseUsageDescription.
The Lira setup UI initiates these prompts automatically. Your Info.plist must contain the usage description strings or the app will crash.
Error Handling
launchSetup rejects with a structured error. The rejection value has code and message fields on Android (via the React Native bridge) and a standard Error on iOS.
Error Codes
| Code | Platform | Cause |
|------|----------|-------|
| INVALID_CONFIG | Android / iOS | apiKey or customerRef is missing or blank. |
| NO_ACTIVITY | Android | No foreground Activity is available (called from a background service or before the app is visible). |
| NO_VIEW_CONTROLLER | iOS | Could not find the root UIViewController to present the setup flow from. |
| SETUP_IN_PROGRESS | iOS | launchSetup was called while a previous setup flow was still active. |
| SETUP_ERROR | Android | An unexpected exception was thrown inside the native bridge. Check message for details. |
Example error handling
try {
const result = await launchSetup({ apiKey: '', customerRef: 'user-123' });
} catch (err: unknown) {
if (err instanceof Error) {
// err.message contains the human-readable description
// On Android, err also has a .code property via the RN bridge
console.error('Setup failed:', err.message);
}
}Checking the error code on Android
try {
await launchSetup(config, uiConfig);
} catch (err: unknown) {
const code = (err as { code?: string }).code;
const message = err instanceof Error ? err.message : String(err);
if (code === 'INVALID_CONFIG') {
// Show user-facing message
} else {
// Log to crash reporting
crashReporter.log(`Lira error [${code}]: ${message}`);
}
}Troubleshooting
Android: IllegalArgumentException at SDK initialisation
Symptom: Native crash mentioning require(isNotBlank()).
Cause: apiKey or customerRef was passed as an empty string. The bridge validates these before passing to the SDK, but if you are calling the native layer directly this guard is bypassed.
Fix: Ensure both fields are non-empty before calling launchSetup.
Android: Build error — Could not resolve com.uselira.sdk:core
Symptom: Gradle sync fails with Could not resolve com.uselira.sdk:core.
Cause: The Lira native SDK is not available via Maven Central or your configured repositories.
Fix: Add a composite build entry in android/settings.gradle pointing to a local checkout of the SDK (see Android Setup), or configure the correct Maven repository URL.
iOS: Pod install fails with Unable to find a specification for 'Uselira'
Symptom: pod install errors with a missing spec.
Cause: The UseliraCore pod path in your Podfile is incorrect or the local SDK directory does not contain a valid .podspec.
Fix: Verify the :path value in your Podfile points to the root of the iOS SDK directory (the folder containing UseliraCore.podspec).
iOS: App crashes on first launch with This app has attempted to access privacy-sensitive data without a usage description
Symptom: Crash immediately when location is first accessed.
Cause: NSLocationWhenInUseUsageDescription or NSLocationAlwaysAndWhenInUseUsageDescription is missing from Info.plist.
Fix: Add both usage description keys to ios/YourApp/Info.plist. See iOS Permissions.
iOS: launchSetup never resolves or rejects after calling stop()
Symptom: The promise from launchSetup remains pending indefinitely when stop() is called concurrently.
Cause: This is handled by the bridge — stop() settles any pending launchSetup promise with null before halting the SDK. If you are seeing this on an older version, update to the latest bridge.
iOS build error: RCTThirdPartyFabricComponentsProvider.mm not found
Symptom: Xcode build fails with a missing file during the React Native Codegen phase.
Cause: This file is generated by RN Codegen and is wiped when running yarn --force or reinstalling modules.
Fix: After reinstalling node modules and before building, regenerate the Codegen artifacts:
node node_modules/uselira/scripts/generate-codegen-artifacts.js \
--path . \
--outputPath ./ios/build/generatedThen build again.
Location events not received
Symptom: addLocationListener callback is never called even after setup completes.
Possible causes and fixes:
- Listener registered after
launchSetup: Register your listener before callinglaunchSetupto avoid missing events emitted during the setup flow. - Background permission denied: Confirm the user granted "Always" location permission. Check device settings.
- Tracking schedule window: Location samples are only collected during the
scheduleStart–scheduleEndwindow. Verify the current time is within that window. - Battery below threshold: If the device battery is below
batteryThreshold, sampling is suspended. Charge the device and try again.
Architecture Notes
Bridge design
The library uses the React Native TurboModule spec (NativeUselira.ts) for New Architecture compatibility, with an RCT_EXTERN_MODULE Objective-C bridge for backward-compatible Old Architecture support on iOS. On Android, the module extends ReactContextBaseJavaModule directly.
Thread safety
- Android: All setup logic runs on the UI queue (
runOnUiQueueThread). The tracking listener callback emits events directly viaDeviceEventManagerModule. - iOS: All setup and stop logic runs on the main queue. The
pendingResolvepromise block is only read and written on the main thread to avoid data races.
iOS ObjC class naming
The iOS Swift class is exposed to Objective-C as UseliraBridge (via @objc(UseliraBridge)) to avoid a name collision with the native iOS SDK's own UseliraCore class. The React Native module name visible to JavaScript remains "Uselira", set by overriding moduleName().
Unit intervals
sampleIntervalandsyncIntervalare specified in milliseconds in the JavaScript API on both platforms.- The Android bridge passes these values directly to the SDK (which also expects milliseconds).
- The iOS bridge divides by 1,000 before passing to the iOS SDK (which expects seconds).
This normalisation is handled transparently — always use milliseconds in your JavaScript code regardless of platform.
