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

@uselira/core

v1.1.0

Published

React Native wrapper for the Uselira SDK

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

| 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/core

Android 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_LOCATION must 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:

  1. Set platform :ios, '16.0'. The default RN scaffold uses min_ios_version_supported (~13.4 on RN 0.74), which fails pod install with a platform-mismatch error against RNUselira.
  2. Use dynamic linking. Either run USE_FRAMEWORKS=dynamic pod install, or add use_frameworks! :linkage => :dynamic near the top of the Podfile. Without dynamic linking, the Swift build fails with No 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}/.."
  )
end

Then install pods:

cd ios && pod install

If the SDK is published to a private CocoaPods spec repo, replace the :path reference with a version specifier and ensure your Podfile sources 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+ CapabilityBackground 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 #import form (not @import UseliraCore;): React Native's AppDelegate.mm is Objective-C++, where Clang module imports are disabled by default, so @import fails 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:

  • UIBackgroundModes includes location (see step 4).
  • An NSLocationAlwaysAndWhenInUseUsageDescription string (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 this jobId on 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(): void

Stops 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
): EmitterSubscription

Subscribes 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 launchSetup to 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
): EmitterSubscription

Subscribes 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 all

getEventLog 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+):

  1. Request ACCESS_FINE_LOCATION (foreground) — standard runtime dialog.
  2. 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:

  1. "Allow location while using the app" / "Allow once" — first prompt uses NSLocationWhenInUseUsageDescription.
  2. 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/generated

Then build again.


Location events not received

Symptom: addLocationListener callback is never called even after setup completes.

Possible causes and fixes:

  1. Listener registered after launchSetup: Register your listener before calling launchSetup to avoid missing events emitted during the setup flow.
  2. Background permission denied: Confirm the user granted "Always" location permission. Check device settings.
  3. Tracking schedule window: Location samples are only collected during the scheduleStartscheduleEnd window. Verify the current time is within that window.
  4. 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 via DeviceEventManagerModule.
  • iOS: All setup and stop logic runs on the main queue. The pendingResolve promise 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

  • sampleInterval and syncInterval are 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.