npm.io
0.10.7 • Published 3d ago

expo-beacon

Licence
MIT
Version
0.10.7
Deps
0
Size
662 kB
Vulns
0
Weekly
1.2K

expo-beacon

An Expo module for scanning, pairing, and monitoring iBeacons and Eddystone beacons in React Native apps — with full background support on both iOS and Android.

Feature Description
Scan Discover nearby iBeacons (one-shot or continuous) and Eddystone-UID / Eddystone-URL beacons via BLE
Pair Register specific beacons for persistent tracking — survives app restarts
Monitor Background enter/exit region detection with distance-based filtering
Distance Real-time distance updates (~1/sec) while monitoring
Timeout Fire a one-shot event after a beacon has been out of range for a configured duration
Event Logging Persist every beacon event to a local SQLite database for diagnostics & replay
Notifications Automatic local notifications on region enter/exit, fully customisable
Platform Native Implementation
Android AltBeacon library + Foreground Service
iOS CoreLocation (iBeacon ranging & monitoring) + CoreBluetooth (Eddystone & wildcard BLE)
Web Not supported (async methods reject, sync getters return inert defaults, everything else throws)

Table of Contents


Installation

npx expo install expo-beacon

Important: This module contains native code and cannot be used with Expo Go. You must use a development build or a bare workflow.


Platform Setup

iOS
1. Info.plist Keys

Add the following keys to your Info.plist (or use an Expo config plugin):

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app monitors iBeacons in the background.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses location to detect nearby beacons.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to scan for iBeacons.</string>
2. Background Modes

In Xcode under Signing & Capabilities, enable:

  • Background Modes → Location updates
  • Background Modes → Uses Bluetooth LE accessories

When the bundled config plugin is installed ("plugins": ["expo-beacon"]), location is merged into UIBackgroundModes automatically on expo prebuild — the native module only enables background ranging when this mode is present.

Key iOS Constraints
  • 20 monitored regions max: iOS limits CLLocationManager to 20 simultaneously monitored beacon regions. If you pair more than 20 iBeacons, only the first 20 are monitored. Eddystone beacons use BLE scanning and do not count toward this limit.
  • No wildcard iBeacon scanning: Apple strips iBeacon manufacturer data from CoreBluetooth advertisements. You must supply at least one proximity UUID when scanning, or have paired beacons (the module auto-uses their UUIDs).
  • Eddystone works unrestricted: Eddystone uses standard BLE service data (0xFEAA), which iOS does not strip. Both scanForEddystonesAsync() and continuous scanning discover Eddystones without restrictions.
Android

All required permissions are declared in the module's AndroidManifest.xml and merged automatically. You must still request runtime permissions before scanning or monitoring:

const granted = await ExpoBeacon.requestPermissionsAsync();

The module requests: BLUETOOTH_SCAN, BLUETOOTH_CONNECT, ACCESS_FINE_LOCATION, and POST_NOTIFICATIONS (API 33+).


Quick Start

A minimal example that pairs one iBeacon and one Eddystone, starts monitoring, and scans for nearby beacons:

import { useEffect, useState } from "react";
import { Button, FlatList, Text, View } from "react-native";
import ExpoBeacon from "expo-beacon";
import type { BeaconScanResult, BeaconRegionEvent } from "expo-beacon";

export default function App() {
  const [beacons, setBeacons] = useState<BeaconScanResult[]>([]);

  useEffect(() => {
    // 1. Pair beacons you want to monitor
    ExpoBeacon.pairBeacon(
      "lobby-entrance",
      "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
      1,
      100,
    );

    // 2. Listen for enter/exit events
    const enterSub = ExpoBeacon.addListener("onBeaconEnter", (e: BeaconRegionEvent) => {
      console.log(`Entered ${e.identifier} at ${e.distance.toFixed(1)} m`);
    });
    const exitSub = ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) => {
      console.log(`Exited ${e.identifier}`);
    });

    // 3. Request permissions and start monitoring
    ExpoBeacon.requestPermissionsAsync().then((granted) => {
      if (granted) ExpoBeacon.startMonitoring(10); // enter within 10 m
    });

    return () => {
      enterSub.remove();
      exitSub.remove();
      ExpoBeacon.stopMonitoring();
    };
  }, []);

  async function scan() {
    const results = await ExpoBeacon.scanForBeaconsAsync(
      ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
      5000
    );
    setBeacons(results);
  }

  return (
    <View style={{ flex: 1, padding: 20, paddingTop: 60 }}>
      <Button title="Scan 5 s" onPress={scan} />
      <FlatList
        data={beacons}
        keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
        renderItem={({ item: b }) => (
          <Text>{b.uuid} {b.major}/{b.minor} — {b.distance.toFixed(1)} m</Text>
        )}
      />
    </View>
  );
}

React Hooks

For React / React Native apps the package ships two hooks that wrap the imperative API, manage event subscriptions (with automatic cleanup), and expose the relevant state reactively. Import them directly from the package:

import { useBeacon, useCarPlay } from "expo-beacon";

Both hooks accept optional event callbacks. Callbacks are read from a ref, so passing fresh inline functions on every render does not re-subscribe the underlying native listeners.

useBeacon()

Manages scanning and background monitoring. It keeps the paired-beacon lists, the set of beacons currently in range, and the monitoring flag in sync, and returns stable action wrappers.

import { useBeacon } from "expo-beacon";

function BeaconScreen() {
  const {
    inRange,
    isMonitoring,
    pairedBeacons,
    requestPermissions,
    pairBeacon,
    startMonitoring,
    stopMonitoring,
  } = useBeacon({
    onBeaconEnter: (e) => console.log("entered", e.identifier, e.distance),
    onBeaconExit: (e) => console.log("exited", e.identifier),
    onError: (e) => console.warn(`[${e.code}] ${e.message}`),
  });

  return (
    <View>
      <Button title="Grant permissions" onPress={requestPermissions} />
      <Button
        title={isMonitoring ? "Stop monitoring" : "Start monitoring"}
        onPress={() => (isMonitoring ? stopMonitoring() : startMonitoring())}
      />
      {inRange.map((b) => (
        <Text key={b.identifier}>
          {b.identifier} — {b.distance >= 0 ? `${b.distance.toFixed(1)}m` : "n/a"}
        </Text>
      ))}
    </View>
  );
}
Returned value Description
pairedBeacons / pairedEddystones Reactive lists of paired devices.
inRange Paired beacons currently in range, derived live from enter/exit/distance/timeout events (InRangeBeacon[]).
isMonitoring Whether background monitoring is active.
isEventLoggingEnabled Whether SQLite event logging is enabled (kept in sync by the logging actions).
refreshPaired() Re-read the paired lists from native.
pairBeacon() / unpairBeacon() Pair / unpair an iBeacon, then refresh.
pairEddystone() / unpairEddystone() Pair / unpair an Eddystone, then refresh.
scanForBeacons() / scanForEddystones() One-shot scans returning a promise.
startContinuousScan() / stopContinuousScan() Live scan; results arrive via onBeaconFound / onEddystoneFound.
cancelScan() Cancel an in-progress one-shot scan.
startMonitoring() / stopMonitoring() Start / stop background monitoring.
getMonitoringConfig() Read the current monitoring config + active-state snapshot.
getMonitoredDeviceState() / getMonitoredDeviceStates() Native state snapshot for one / all paired devices.
setNotificationConfig() Persist notification configuration for monitoring sessions.
setBeaconNotificationConfig() Persist only beacon notification settings.
setCarPlayNotificationConfig() Persist only CarPlay / Android Auto notification settings.
enableEventLogging() / disableEventLogging() Toggle SQLite logging (updates isEventLoggingEnabled).
getEventLogs() / clearEventLogs() / destroyEventLogs() Read / clear / drop the persisted event log.
setApiEndpoint() / getApiEndpoint() Configure / read the native event-forwarding endpoint.
isBatteryOptimizationExempt() / requestBatteryOptimizationExemption() Check / request Android battery-optimization exemption.
requestPermissions() Request the permissions needed for scanning / monitoring.

inRange reflects monitored (paired) beacons only. Continuous-scan results are delivered through the onBeaconFound / onEddystoneFound callbacks because raw scan hits carry no paired identifier. Pass track: false to skip inRange bookkeeping when you only need the callbacks.

useCarPlay()

Observes CarPlay / Android Auto connection state. It initializes from the persisted native state on mount and tracks live connect / disconnect events.

import { useCarPlay } from "expo-beacon";

function CarPlayBadge() {
  const { connected, transport, isMonitoring, startMonitoring, stopMonitoring } =
    useCarPlay({
      onConnected: (e) => console.log("car connected via", e.transport),
      onDisconnected: () => console.log("car disconnected"),
    });

  return (
    <View>
      <Text>{connected ? `Connected (${transport})` : "Not connected"}</Text>
      <Button
        title={isMonitoring ? "Stop" : "Start"}
        onPress={() => (isMonitoring ? stopMonitoring() : startMonitoring())}
      />
    </View>
  );
}
Returned value Description
connected Whether a CarPlay / Android Auto session is active.
transport Transport of the active session (CarPlayTransport), or null.
isMonitoring Whether persistent monitoring is enabled.
lastConnectedAt / lastDisconnectedAt Epoch-ms timestamps of the last transitions, or null.
startMonitoring() / stopMonitoring() Enable / disable monitoring.
refresh() Re-read the connection + monitoring state from native.
getDiagnostics() Fetch detection diagnostics for troubleshooting.

Pass autoStart: true to call startCarPlayMonitoring() on mount when it is not already enabled.

Both hooks are safe to call on web: the underlying module is a no-op stub there, so the hooks simply report empty / disconnected state.


Usage Examples

Scanning for iBeacons
One-shot scan with UUID filter (both platforms)
import ExpoBeacon from "expo-beacon";

// Scan for 8 seconds, filtering by a specific UUID
const beacons = await ExpoBeacon.scanForBeaconsAsync(
  ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
  8000,
);

beacons.forEach((b) => {
  console.log(
    `UUID: ${b.uuid}  Major: ${b.major}  Minor: ${b.minor}  ` +
    `Distance: ${b.distance.toFixed(1)}m  RSSI: ${b.rssi}dBm`
  );
});
Wildcard scan (Android only)
// Pass an empty array (or omit the arguments — defaults are uuids = [],
// scanDuration = 5000) to discover ALL nearby iBeacons.
// On iOS, this auto-uses UUIDs from paired beacons
const beacons = await ExpoBeacon.scanForBeaconsAsync([], 5000);
Multiple UUID scan
// Scan for beacons from two different manufacturers/deployments
const beacons = await ExpoBeacon.scanForBeaconsAsync(
  [
    "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
    "FDA50693-A4E2-4FB1-AFCF-C6EB07647825",
  ],
  10000,
);

Scanning for Eddystone Beacons
import ExpoBeacon from "expo-beacon";

// Discover both Eddystone-UID and Eddystone-URL frames
const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);

eddystones.forEach((b) => {
  if (b.frameType === "uid") {
    console.log(`UID: namespace=${b.namespace} instance=${b.instance} dist=${b.distance.toFixed(1)}m`);
  } else if (b.frameType === "url") {
    console.log(`URL: ${b.url} dist=${b.distance.toFixed(1)}m`);
  }
});

Eddystone scanning works identically on both iOS and Android — no UUID filter required.


Continuous (Live) Scanning

Use continuous scanning when you need real-time beacon updates (e.g., a live radar UI). This fires events continuously rather than resolving a single promise.

import { useEffect, useRef, useState } from "react";
import { FlatList, Text, Button, View } from "react-native";
import ExpoBeacon from "expo-beacon";
import type { BeaconScanResult, EddystoneScanResult } from "expo-beacon";

export default function LiveScanner() {
  const [ibeacons, setIbeacons] = useState<BeaconScanResult[]>([]);
  const [eddystones, setEddystones] = useState<EddystoneScanResult[]>([]);
  const [scanning, setScanning] = useState(false);
  const subs = useRef<Array<{ remove: () => void }>>([]);

  const startScan = () => {
    setScanning(true);

    // iBeacon advertisements
    subs.current.push(
      ExpoBeacon.addListener("onBeaconFound", (beacon) => {
        setIbeacons((prev) => {
          const key = `${beacon.uuid}-${beacon.major}-${beacon.minor}`;
          const idx = prev.findIndex(
            (b) => `${b.uuid}-${b.major}-${b.minor}` === key,
          );
          if (idx >= 0) {
            const copy = [...prev];
            copy[idx] = beacon; // Update distance/RSSI
            return copy;
          }
          return [...prev, beacon];
        });
      }),
    );

    // Eddystone advertisements
    subs.current.push(
      ExpoBeacon.addListener("onEddystoneFound", (beacon) => {
        setEddystones((prev) => {
          const key = beacon.frameType === "uid"
            ? `${beacon.namespace}-${beacon.instance}`
            : `url-${beacon.url}`;
          const idx = prev.findIndex((b) => {
            const k = b.frameType === "uid"
              ? `${b.namespace}-${b.instance}`
              : `url-${b.url}`;
            return k === key;
          });
          if (idx >= 0) {
            const copy = [...prev];
            copy[idx] = beacon;
            return copy;
          }
          return [...prev, beacon];
        });
      }),
    );

    ExpoBeacon.startContinuousScan();
  };

  const stopScan = () => {
    ExpoBeacon.stopContinuousScan();
    subs.current.forEach((s) => s.remove());
    subs.current = [];
    setScanning(false);
  };

  useEffect(() => {
    return () => stopScan(); // Cleanup on unmount
  }, []);

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Button
        title={scanning ? "Stop Scan" : "Start Live Scan"}
        onPress={scanning ? stopScan : startScan}
      />
      <Text style={{ fontWeight: "bold", marginTop: 10 }}>
        iBeacons ({ibeacons.length})
      </Text>
      <FlatList
        data={ibeacons}
        keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
        renderItem={({ item: b }) => (
          <Text>
            {b.uuid.slice(0, 8)}… {b.major}/{b.minor} — {b.distance.toFixed(1)}m (RSSI: {b.rssi})
          </Text>
        )}
      />
      <Text style={{ fontWeight: "bold", marginTop: 10 }}>
        Eddystones ({eddystones.length})
      </Text>
      <FlatList
        data={eddystones}
        keyExtractor={(b, i) => `eddy-${i}`}
        renderItem={({ item: b }) => (
          <Text>
            {b.frameType === "uid"
              ? `UID: ${b.namespace?.slice(0, 8)}… / ${b.instance}`
              : `URL: ${b.url}`} — {b.distance.toFixed(1)}m
          </Text>
        )}
      />
    </View>
  );
}

iOS note: Continuous iBeacon scanning on iOS only discovers beacons whose UUID has been registered via pairBeacon(). On Android, all nearby BLE beacons are reported. Eddystone discovery works on both platforms regardless of pairing.


Pairing & Unpairing Beacons

Pairing registers a beacon for persistent monitoring. Paired beacons survive app restarts — they are stored in UserDefaults (iOS) / SharedPreferences (Android).

import ExpoBeacon from "expo-beacon";

// ── iBeacon ──

// Pair an iBeacon (identifier must be unique)
ExpoBeacon.pairBeacon(
  "lobby-entrance",                           // your label
  "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",    // proximity UUID
  1,                                          // major (065535)
  100,                                        // minor (065535)
);

// Re-pairing with the same identifier replaces the previous entry
ExpoBeacon.pairBeacon(
  "lobby-entrance",
  "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
  1,
  200, // updated minor
);

// List all paired iBeacons
const paired = ExpoBeacon.getPairedBeacons();
console.log(paired);
// → [{ identifier: "lobby-entrance", uuid: "E2C5…", major: 1, minor: 200 }]

// Remove a beacon
ExpoBeacon.unpairBeacon("lobby-entrance");

// ── Eddystone-UID ──

// Pair an Eddystone-UID beacon
ExpoBeacon.pairEddystone(
  "meeting-room",                              // your label
  "edd1ebeac04e5defa017",                      // 10-byte namespace (20 hex chars)
  "0123456789ab",                              // 6-byte instance  (12 hex chars)
);

// List all paired Eddystones
const pairedEddy = ExpoBeacon.getPairedEddystones();
console.log(pairedEddy);
// → [{ identifier: "meeting-room", namespace: "edd1…", instance: "0123…" }]

// Remove an Eddystone
ExpoBeacon.unpairEddystone("meeting-room");

Background Monitoring

Monitoring watches all paired beacons (iBeacon + Eddystone) in the background and fires events when the device enters or exits a beacon region.

import { useEffect, useRef } from "react";
import ExpoBeacon from "expo-beacon";
import type {
  BeaconRegionEvent,
  BeaconDistanceEvent,
  EddystoneRegionEvent,
  EddystoneDistanceEvent,
} from "expo-beacon";

export function useBeaconMonitoring() {
  const subs = useRef<Array<{ remove: () => void }>>([]);

  useEffect(() => {
    async function start() {
      const granted = await ExpoBeacon.requestPermissionsAsync();
      if (!granted) {
        console.warn("Beacon permissions denied");
        return;
      }

      // Subscribe to iBeacon events
      subs.current.push(
        ExpoBeacon.addListener("onBeaconEnter", (e: BeaconRegionEvent) => {
          console.log(`[iBeacon] Entered "${e.identifier}" at ~${e.distance.toFixed(1)}m`);
        }),
        ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) => {
          console.log(`[iBeacon] Exited "${e.identifier}"`);
        }),
        ExpoBeacon.addListener("onBeaconDistance", (e: BeaconDistanceEvent) => {
          console.log(`[iBeacon] "${e.identifier}" → ${e.distance.toFixed(2)}m`);
        }),
      );

      // Subscribe to Eddystone events
      subs.current.push(
        ExpoBeacon.addListener("onEddystoneEnter", (e: EddystoneRegionEvent) => {
          console.log(`[Eddystone] Entered "${e.identifier}"`);
        }),
        ExpoBeacon.addListener("onEddystoneExit", (e: EddystoneRegionEvent) => {
          console.log(`[Eddystone] Exited "${e.identifier}"`);
        }),
        ExpoBeacon.addListener("onEddystoneDistance", (e: EddystoneDistanceEvent) => {
          console.log(`[Eddystone] "${e.identifier}" → ${e.distance.toFixed(2)}m`);
        }),
      );

      // Start with distance threshold
      await ExpoBeacon.startMonitoring({
        maxDistance: 10, // Only fire "enter" within 10 metres
        notifications: {
          beaconEvents: {
            enterTitle: "You're near a beacon!",
            exitTitle: "Beacon out of range",
            body: "{identifier} {event}ed",
          },
        },
      });
    }

    start();

    return () => {
      subs.current.forEach((s) => s.remove());
      subs.current = [];
      ExpoBeacon.stopMonitoring();
    };
  }, []);
}
Simple shorthand (number = maxDistance)
// Equivalent to { maxDistance: 5 }
await ExpoBeacon.startMonitoring(5);
Monitor with no distance filter
// Monitor without distance limit — enter fires as soon as the region is detected
await ExpoBeacon.startMonitoring();

Customizing Notifications
Persistent configuration (survives app restarts)
ExpoBeacon.setNotificationConfig({
  beacons: {
    // Enter/exit/timeout alert notifications (both platforms)
    events: {
      enabled: true,                      // Set false to suppress beacon alerts
      enterTitle: "Beacon nearby",
      exitTitle: "Beacon out of range",
      timeoutTitle: "Beacon timed out",
      body: "{identifier} {event}ed",     // Placeholders: {identifier}, {event}
      sound: true,                        // iOS only
      icon: "ic_beacon_notification",     // Android only — drawable resource name
    },

    // Persistent status-bar notification for beacon monitoring (Android only)
    foregroundService: {
      title: "My App — Beacon monitoring",
      text: "Watching for nearby beacons",
      icon: "ic_service",
    },

    // Android notification channel for beacon notifications
    channel: {
      name: "Proximity Alerts",
      description: "Alerts when beacons enter or leave range",
      importance: "default",              // "low" | "default" | "high"
    },
  },

  carPlay: {
    // Connect/disconnect notifications (both platforms)
    events: {
      enabled: true,                      // Set false to suppress CarPlay alerts
      connectedTitle: "CarPlay connected",
      disconnectedTitle: "CarPlay disconnected",
      body: "CarPlay {event} {transport}", // Placeholders: {event}, {transport}
      sound: true,                        // iOS only
      icon: "ic_carplay_notification",    // Android only — drawable resource name
    },

    // Persistent status-bar notification in CarPlay-only mode (Android only)
    foregroundService: {
      title: "My App — Vehicle monitoring",
      text: "Monitoring CarPlay / Android Auto",
      icon: "ic_service",
    },

    // Android notification channel for CarPlay notifications
    channel: {
      name: "CarPlay Alerts",
      description: "CarPlay and Android Auto connection notifications",
      importance: "default",
    },
  },
});
One-off session configuration (inline with startMonitoring)
await ExpoBeacon.startMonitoring({
  maxDistance: 5,
  notifications: {
    beaconEvents: { enabled: false }, // Silent monitoring — no user-facing alerts
  },
});

Beacon Timeout

Pair a beacon with timeoutSeconds to fire a one-shot event after the beacon has been out of range for that duration. The countdown is armed when the beacon exits range (or when no BLE readings arrive for 60 seconds, e.g. due to Doze mode or background throttling) and is cancelled if the beacon is seen again before it fires.

import { useEffect } from "react";
import ExpoBeacon from "expo-beacon";
import type { BeaconTimeoutEvent, EddystoneTimeoutEvent } from "expo-beacon";

// Pair with a 30-second timeout
ExpoBeacon.pairBeacon(
  "lobby-entrance",
  "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
  1,
  100,
  undefined,      // name (optional)
  30,             // timeoutSeconds — fires 30 s after the beacon leaves range
);

// Pair Eddystone with a 60-second timeout
ExpoBeacon.pairEddystone(
  "meeting-room",
  "edd1ebeac04e5defa017",
  "0123456789ab",
  undefined,      // name (optional)
  60,             // timeoutSeconds — fires 60 s after the beacon leaves range
);

// Listen for the timeout events
useEffect(() => {
  const beaconTimeout = ExpoBeacon.addListener(
    "onBeaconTimeout",
    (e: BeaconTimeoutEvent) => {
      console.log(`Beacon "${e.identifier}" out of range for configured duration!`);
    },
  );
  const eddystoneTimeout = ExpoBeacon.addListener(
    "onEddystoneTimeout",
    (e: EddystoneTimeoutEvent) => {
      console.log(`Eddystone "${e.identifier}" out of range for configured duration!`);
    },
  );

  return () => {
    beaconTimeout.remove();
    eddystoneTimeout.remove();
  };
}, []);

Note: The timeout fires once per exit. If the beacon re-enters range before the countdown completes, the pending timer is cancelled and re-armed on the next exit.


Event Logging

Enable SQLite-backed event logging to persist every beacon event locally. Useful for diagnostics, debugging, and replaying event history.

import ExpoBeacon from "expo-beacon";
import type { EventLogEntry, EventLogQueryOptions } from "expo-beacon";

// Enable logging — creates/opens the SQLite database
ExpoBeacon.enableEventLogging();

// ... scanning, monitoring, etc. — all events are now persisted automatically ...

// Query all recent events
const logs: EventLogEntry[] = ExpoBeacon.getEventLogs();
console.log(logs);
// [
//   { id: 42, timestamp: 1712345678000, eventType: "onBeaconEnter",
//     identifier: "lobby", data: { uuid: "E2C5…", major: 1, minor: 100, ... } },
//   ...
// ]

// Filter by event type and time range
const enterLogs = ExpoBeacon.getEventLogs({
  eventType: "onBeaconEnter",
  sinceTimestamp: Date.now() - 3600_000, // last hour
  limit: 100,
});

// Disable logging (retains existing data)
ExpoBeacon.disableEventLogging();

// Clear all logged events (keeps the database)
ExpoBeacon.clearEventLogs();

// Destroy the database entirely (also disables logging)
ExpoBeacon.destroyEventLogs();

Storage: Events are stored in a local SQLite database (expo_beacon_events.db). No external dependencies are required — Android uses the built-in SQLite, iOS uses the system libsqlite3.


Cancelling a Scan

Cancel any in-progress one-shot scan (iBeacon or Eddystone). The pending promise will reject with error code SCAN_CANCELLED.

// Start a long scan
const scanPromise = ExpoBeacon.scanForBeaconsAsync(
  ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
  30000,
);

// Cancel it after 2 seconds
setTimeout(() => ExpoBeacon.cancelScan(), 2000);

try {
  const results = await scanPromise;
} catch (e) {
  if (e.code === "SCAN_CANCELLED") {
    console.log("Scan was cancelled by user");
  }
}

CarPlay / Android Auto Detection

Detect when the device connects to a car infotainment system and react in JS — or, when the bundled config plugin is installed, automatically start react-native-background-geolocation tracking on connect and stop it on disconnect.

Detection covers both wired and wireless CarPlay on iOS and Android Auto projection / Android Automotive OS on Android. No special CarPlay entitlement or Android Auto certification is required.

import ExpoBeacon, {
  CarPlayConnectedEvent,
  CarPlayDisconnectedEvent,
} from "expo-beacon";

// Start observing
await ExpoBeacon.startCarPlayMonitoring();

const connectSub = ExpoBeacon.addListener(
  "onCarPlayConnected",
  (event: CarPlayConnectedEvent) => {
    // event.transport: "wired" | "wireless" | "projection" | "native" | "unknown"
    console.log(`Car connected via ${event.transport}`);
  },
);
const disconnectSub = ExpoBeacon.addListener(
  "onCarPlayDisconnected",
  (_event: CarPlayDisconnectedEvent) => {
    console.log("Car disconnected");
  },
);

// Stop later (e.g. when feature is disabled)
await ExpoBeacon.stopCarPlayMonitoring();
connectSub.remove();
disconnectSub.remove();

How it works

  • iOS: observes AVAudioSession.routeChangeNotification for output ports of type .carAudio. Wired-vs-wireless is reported on a best-effort basis (looking for a coexisting Bluetooth output port).
  • Android: observes androidx.car.app.connection.CarConnection LiveData. transport is "projection" for phones casting to a head unit, "native" for Android Automotive OS.
  • Connect/disconnect events flow through the same SQLite event log and remote API forwarder as beacon events.
  • When the config plugin is installed, the auto-generated BeaconGeoPlugin also calls BackgroundGeolocation.start() on connect and .stop() on disconnect — no extra wiring required.

Background detection

CarPlay observation is persistent — the enabled flag is stored in native preferences and the observer is automatically re-attached after app kill or device reboot. startMonitoring() also enables CarPlay observation by default; calling startCarPlayMonitoring() explicitly is only required if you want CarPlay events without beacon monitoring.

  • Android: the foreground service hosts the CarConnection observer. As long as the service runs (which it does whenever beacon monitoring or CarPlay monitoring is enabled, and is restarted on boot by BootReceiver), CarPlay events are captured even after the app process is killed. Guaranteed background detection.
  • iOS: the observer auto-restarts in the module's OnCreate, including background-launches triggered by beacon region monitoring. iOS cannot wake a terminated app on CarPlay alone — for guaranteed wake-from-suspension, also call startMonitoring() with at least one paired beacon (e.g. a beacon left in the vehicle). Region-wake events trigger a CarPlay state resync to reconcile any route changes that happened while the app was suspended.

Notes

  • startCarPlayMonitoring() is idempotent. Calling it twice does not register a duplicate observer.
  • stopCarPlayMonitoring() clears the persisted flag, so the observer will not auto-restart on next launch.
  • The iOS detector does not require the CarPlay entitlement because it only reads the active audio route; you do not need to ship a CarPlay app.
  • On iOS, if the JS bundle is suspended in the background, the JS event delivery is deferred until the app resumes, but the native lifecycle delegate (used by the geolocation plugin) fires immediately on connect.
  • On Android, when CarPlay monitoring is enabled without beacon monitoring, the foreground service shows a generic "Connected device monitoring active" notification.

Android Auto registration (automatic via config plugin)

On Android 14+ — and especially with wireless Android AutoCarConnection.getType() silently returns NOT_CONNECTED for any app that has not declared itself Android-Auto-aware. The bundled config plugin handles this for you: on the next expo prebuild it injects a com.google.android.gms.car.application meta-data entry into AndroidManifest.xml and writes res/xml/automotive_app_desc.xml with <uses name="template"/>. No manual native edits are required.

If you need to disable this (e.g. you already ship your own Android Auto template app and don't want a duplicate registration) or you need to declare a different capability than template, configure the plugin in your app config:

{
  "expo": {
    "plugins": [
      ["expo-beacon", {
        "android": {
          "androidAuto": {
            "register": true,
            "usesName": "template"
          }
        }
      }]
    ]
  }
}

usesName accepts any value supported by Android's automotiveApp schema (template, media, notification, …). Setting register: false skips both the meta-data and the resource file.

Diagnostics

If onCarPlayConnected never fires on Android, call getCarPlayDiagnostics() to inspect the native state:

const d = ExpoBeacon.getCarPlayDiagnostics();
// {
//   isCarAppMetadataPresent: true,   // false → config plugin didn't run; prebuild again
//   isCarProviderQueryable: true,    // false → Android Auto app not installed on device
//   lastRawConnectionType: 2,        // 0=NOT_CONNECTED 1=NATIVE (AAOS) 2=PROJECTION; null = no value yet
//   observerActive: true,
//   serviceAlive: true,
// }

A response with isCarAppMetadataPresent: false indicates the AA registration didn't make it into the built APK — re-run expo prebuild --clean to apply the plugin changes.


Full API Reference

requestPermissionsAsync()
requestPermissionsAsync(): Promise<boolean>

Requests all permissions required for scanning and monitoring.

Platform Permissions Requested
Android ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, BLUETOOTH_SCAN + BLUETOOTH_CONNECT (API 31+), POST_NOTIFICATIONS (API 33+), then ACCESS_BACKGROUND_LOCATION (API 29+) in a second prompt. Resolves true only when background location is granted.
iOS CLLocationManager "When In Use" authorization — resolves true once granted. The "Always" upgrade is requested later by startMonitoring(), and Bluetooth permission is not prompted here.

Returns: true if all required permissions were granted.

const granted = await ExpoBeacon.requestPermissionsAsync();
if (!granted) {
  console.warn("Permissions not granted — scanning and monitoring will fail.");
}

Tip: Call this before scanForBeaconsAsync() or startMonitoring(). If you call startMonitoring() without prior authorization, it requests "Always" permission automatically, but explicit control gives a better UX.


scanForBeaconsAsync(uuids?, scanDurationMs?)
scanForBeaconsAsync(uuids?: string[], scanDurationMs?: number): Promise<BeaconScanResult[]>

Performs a one-shot iBeacon scan. Waits for the specified duration, then resolves with all discovered beacons.

Both parameters are optional — the defaults are applied on the JS side before the native call.

Parameter Type Default Description
uuids string[] [] Proximity UUIDs to filter by. See platform differences below.
scanDurationMs number 5000 Scan duration in milliseconds (must be > 0).

Returns: BeaconScanResult[] — deduplicated by UUID + major + minor.

Behaviour Android iOS
Empty uuids ([]) Wildcard — discovers all nearby iBeacons Auto-uses paired beacon UUIDs. Rejects with WILDCARD_NOT_SUPPORTED if none are paired.
Targeted (["UUID-1"]) Filters scan results to matching UUIDs CoreLocation ranging for those UUIDs

Possible errors:

Code Reason
SCAN_IN_PROGRESS Another scan is already running
INVALID_UUID One of the UUID strings is malformed
INVALID_DURATION Duration ≤ 0
PERMISSION_DENIED Location permission not granted
WILDCARD_NOT_SUPPORTED iOS: empty UUIDs with no paired beacons
SCAN_CANCELLED cancelScan() was called
const beacons = await ExpoBeacon.scanForBeaconsAsync(
  ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
  8000,
);

scanForEddystonesAsync(scanDurationMs?)
scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>

Performs a one-shot Eddystone scan using BLE. Discovers both Eddystone-UID and Eddystone-URL frames.

The parameter is optional — the default is applied on the JS side before the native call.

Parameter Type Default Description
scanDurationMs number 5000 Scan duration in milliseconds (must be > 0).

Returns: EddystoneScanResult[] — deduplicated by namespace:instance (UID) or url (URL).

Possible errors:

Code Reason
SCAN_IN_PROGRESS Another Eddystone scan is already running
INVALID_DURATION Duration ≤ 0
SCAN_CANCELLED cancelScan() was called
const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);

startContinuousScan()
startContinuousScan(): void

Begins a continuous BLE scan that streams beacon discoveries via events:

  • onBeaconFound — iBeacon advertisements
  • onEddystoneFound — Eddystone advertisements

Does not return results directly — subscribe to events before calling. Call stopContinuousScan() to end.

iOS: Only reports iBeacons whose UUID is registered via pairBeacon(). Eddystones are reported regardless of pairing.


stopContinuousScan()
stopContinuousScan(): void

Stops the continuous scan. No-op if no scan is running.


cancelScan()
cancelScan(): void

Cancels any in-progress one-shot scan (iBeacon or Eddystone). The pending promise rejects with code SCAN_CANCELLED.


pairBeacon(identifier, uuid, major, minor, name?, timeoutSeconds?)
pairBeacon(identifier: string, uuid: string, major: number, minor: number, name?: string, timeoutSeconds?: number): void

Registers an iBeacon for persistent monitoring.

Parameter Type Description
identifier string Unique label (e.g. "lobby-entrance"). Re-using an iBeacon identifier replaces the previous entry.
uuid string iBeacon proximity UUID (case-insensitive, e.g. "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
major number Major value: 065535
minor number Minor value: 065535
name string? Optional BLE device name for display purposes
timeoutSeconds number? Fire onBeaconTimeout once, this many seconds after the beacon exits range. Cancelled if the beacon is seen again first.

Possible errors: INVALID_UUID, INVALID_MAJOR, INVALID_MINOR, DUPLICATE_IDENTIFIER (identifier already used by a paired Eddystone).

ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42);

// With timeout — fires onBeaconTimeout 30 s after the beacon leaves range
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42, undefined, 30);

unpairBeacon(identifier)
unpairBeacon(identifier: string): void

Removes a paired iBeacon. If monitoring is active, the region stops being tracked immediately.

Parameter Type Description
identifier string The label used when pairing
ExpoBeacon.unpairBeacon("main-door");

getPairedBeacons()
getPairedBeacons(): PairedBeacon[]

Returns all currently paired iBeacons from persistent storage.

const paired = ExpoBeacon.getPairedBeacons();
// [{ identifier: "main-door", uuid: "E2C5…", major: 1, minor: 42 }]

pairEddystone(identifier, namespace, instance, name?, timeoutSeconds?)
pairEddystone(identifier: string, namespace: string, instance: string, name?: string, timeoutSeconds?: number): void

Registers an Eddystone-UID beacon for persistent monitoring. The namespace and instance are normalized to lowercase before storage.

Parameter Type Description
identifier string Unique label (e.g. "meeting-room"). Re-using an Eddystone identifier replaces the previous entry.
namespace string 10-byte namespace ID as hex string — must be exactly 20 hex characters
instance string 6-byte instance ID as hex string — must be exactly 12 hex characters
name string? Optional BLE device name for display purposes
timeoutSeconds number? Fire onEddystoneTimeout once, this many seconds after the beacon exits range. Cancelled if the beacon is seen again first.

Possible errors: INVALID_NAMESPACE, INVALID_INSTANCE, DUPLICATE_IDENTIFIER (identifier already used by a paired iBeacon).

ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab");

// With timeout — fires onEddystoneTimeout 60 s after the beacon leaves range
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab", undefined, 60);

unpairEddystone(identifier)
unpairEddystone(identifier: string): void

Removes a paired Eddystone beacon.

Parameter Type Description
identifier string The label used when pairing
ExpoBeacon.unpairEddystone("meeting-room");

getPairedEddystones()
getPairedEddystones(): PairedEddystone[]

Returns all currently paired Eddystone beacons from persistent storage.

const paired = ExpoBeacon.getPairedEddystones();
// [{ identifier: "meeting-room", namespace: "edd1…", instance: "0123…" }]

startMonitoring(options?)
startMonitoring(options?: MonitoringOptions | number): Promise<void>

Starts background region monitoring for all paired beacons (iBeacon + Eddystone).

Accepts a MonitoringOptions object, a plain number (shorthand for maxDistance), or nothing.

Property Type Default Description
maxDistance number undefined Distance threshold in metres. onBeaconEnter / onEddystoneEnter only fires when measured distance ≤ this value. onBeaconExit / onEddystoneExit always fires. Omit to disable filtering.
exitDistance number maxDistance + min(maxDistance × 0.5, 2.5) Distance in metres at which exit events fire. Must be ≥ maxDistance. Creates a hysteresis band between enter and exit thresholds to prevent rapid toggling near the boundary. Only used when maxDistance is set.
notifications NotificationConfig undefined Notification overrides for this session (persisted).

What happens on each platform:

Platform Mechanism
Android Starts BeaconForegroundService (persistent notification). Survives app backgrounding. Auto-restarts after device reboot via BootReceiver. Scan timing: 1.1 s every 5 s.
iOS Activates CLLocationManager region monitoring (iBeacon) + CoreBluetooth BLE scanning (Eddystone). iOS can wake/relaunch the app on region boundary crossings, even if force-quit.

Possible errors: PERMISSION_DENIED (Always authorization required on iOS).

// Shorthand — just a distance threshold
await ExpoBeacon.startMonitoring(5);

// Full options with custom exit threshold
await ExpoBeacon.startMonitoring({
  maxDistance: 10,
  exitDistance: 15, // Exit fires when distance exceeds 15m
  notifications: {
    beaconEvents: {
      enterTitle: "Welcome!",
      body: "{identifier} is nearby",
    },
  },
});

// No distance filter, silent
await ExpoBeacon.startMonitoring({
  notifications: { beaconEvents: { enabled: false } },
});

// No options at all — monitor all paired beacons, no distance filter, default notifications
await ExpoBeacon.startMonitoring();

stopMonitoring()
stopMonitoring(): Promise<void>

Stops all background monitoring. On Android, stops the foreground service. Persisted monitoring options (maxDistance, exitDistance, level, exitTimeoutSeconds, …) are cleared on both platforms.

await ExpoBeacon.stopMonitoring();

getMonitoringConfig()
getMonitoringConfig(): MonitoringConfig

Returns the current monitoring configuration snapshot, including whether background monitoring is active.

This reads the native monitoring settings currently persisted by the module. Option fields are omitted when they have not been explicitly set.

const config = ExpoBeacon.getMonitoringConfig();
// {
//   isMonitoring: true,
//   maxDistance: 10,
//   exitDistance: 15,
//   minRssi: -85,
//   level: "all"
// }

getMonitoredDeviceState(identifier)
getMonitoredDeviceState(identifier: string): MonitoredDeviceState | null

Returns the current monitoring-state snapshot for a paired iBeacon or Eddystone with the matching identifier.

  • state is "entered" or "exited".
  • distance is null when the device is currently exited or there is no live reading yet.
  • Returns null when no paired device matches the identifier.

Identifiers should be unique across all paired monitored devices.

const lobby = ExpoBeacon.getMonitoredDeviceState("lobby-entrance");
// {
//   kind: "ibeacon",
//   identifier: "lobby-entrance",
//   uuid: "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
//   major: 1,
//   minor: 100,
//   state: "entered",
//   distance: 2.4
// }

getMonitoredDeviceStates()
getMonitoredDeviceStates(): MonitoredDeviceState[]

Returns the current monitoring-state snapshots for all paired monitored devices across iBeacon and Eddystone.

const states = ExpoBeacon.getMonitoredDeviceStates();
// [
//   { kind: "ibeacon", identifier: "lobby-entrance", state: "entered", distance: 2.4, ... },
//   { kind: "eddystone", identifier: "meeting-room", state: "exited", distance: null, ... }
// ]

setNotificationConfig(config)
setNotificationConfig(config: NotificationConfig): void

Persists notification configuration applied to all subsequent monitoring sessions. Survives app restarts.

For one-off overrides, pass notifications inside startMonitoring(options) instead. Monitoring-time notification overrides are merged into the persisted notification config, so omitted beacon or CarPlay sections are preserved.

Use the top-level beacons and carPlay sections to configure those notification streams independently. Legacy keys (beaconEvents, carPlayEvents, foregroundService, channel, carPlayChannel) are still accepted for existing apps.

See NotificationConfig for the full shape.

setBeaconNotificationConfig(config)
setBeaconNotificationConfig(config: BeaconNotificationSettings | BeaconNotificationConfig): void

Persists only beacon notification settings without replacing CarPlay settings. Passing a plain BeaconNotificationConfig is treated as beacons.events.

setCarPlayNotificationConfig(config)
setCarPlayNotificationConfig(config: CarPlayNotificationSettings | CarPlayNotificationConfig): void

Persists only CarPlay / Android Auto notification settings without replacing beacon settings. Passing a plain CarPlayNotificationConfig is treated as carPlay.events.


enableEventLogging()
enableEventLogging(): void

Creates/opens the local SQLite database and starts persisting every beacon event (onBeaconEnter, onBeaconExit, onBeaconDistance, onBeaconTimeout, onBeaconFound, onEddystoneEnter, etc.). Call before startMonitoring() or startContinuousScan().

ExpoBeacon.enableEventLogging();

disableEventLogging()
disableEventLogging(): void

Stops persisting events. Previously logged data is retained — call clearEventLogs() or destroyEventLogs() to remove it.

ExpoBeacon.disableEventLogging();

getEventLogs(options?)
getEventLogs(options?: EventLogQueryOptions): EventLogEntry[]

Retrieves logged events from the SQLite database, newest first.

Property Type Default Description
limit number 1000 Max rows to return (capped at 10 000)
eventType string undefined Filter by event name (e.g. "onBeaconEnter")
sinceTimestamp number undefined Only events with timestamp >= value (ms since epoch)

Returns: EventLogEntry[]

const logs = ExpoBeacon.getEventLogs({ eventType: "onBeaconEnter", limit: 50 });

clearEventLogs()
clearEventLogs(): void

Deletes all rows from the event log table. The database file remains.

ExpoBeacon.clearEventLogs();

destroyEventLogs()
destroyEventLogs(): void

Disables logging and deletes the entire SQLite database file.

ExpoBeacon.destroyEventLogs();

setApiEndpoint(url, apiKey?, id?)
setApiEndpoint(url: string, apiKey?: string, id?: string): void

Configures a remote endpoint to which native code POSTs every beacon event — delivery works even when the JS bridge is not active (app backgrounded). The configuration persists until changed.

Parameter Type Description
url string The API endpoint URL to POST events to.
apiKey string? Sent as the X-CSFR-Token header (sic — the header is literally X-CSFR-Token, not X-CSRF-Token).
id string? Identifier appended to every forwarded event payload.

Use getApiEndpoint() to read back the current configuration (each field is null if unset).


Events

Subscribe with ExpoBeacon.addListener(eventName, handler). Always call .remove() on the returned subscription during cleanup.

const sub = ExpoBeacon.addListener("onBeaconEnter", handler);
// Later:
sub.remove();
Event Summary
Event Trigger Payload Type
onBeaconEnter Paired iBeacon enters range (respects maxDistance) BeaconRegionEvent
onBeaconExit Paired iBeacon leaves range (always fires) BeaconRegionEvent
onBeaconDistance Periodic distance update during monitoring (~1/sec) BeaconDistanceEvent
onBeaconFound iBeacon detected during continuous scan BeaconScanResult
onEddystoneFound Eddystone detected during continuous scan EddystoneScanResult
onEddystoneEnter Paired Eddystone enters range (respects maxDistance) EddystoneRegionEvent
onEddystoneExit Paired Eddystone leaves range (always fires) EddystoneRegionEvent
onEddystoneDistance Periodic Eddystone distance update during monitoring EddystoneDistanceEvent
onBeaconTimeout Paired iBeacon out of range for configured timeoutSeconds BeaconTimeoutEvent
onEddystoneTimeout Paired Eddystone out of range for configured timeoutSeconds EddystoneTimeoutEvent
Event Detail
onBeaconEnter

Fired when the device enters the region of a paired iBeacon. If maxDistance was set, only fires when the measured distance is within the threshold.

ExpoBeacon.addListener("onBeaconEnter", (e) => {
  // e.identifier — "lobby-entrance"
  // e.uuid       — "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
  // e.major      — 1
  // e.minor      — 100
  // e.event      — "enter"
  // e.distance   — 3.2 (metres, or –1 if unavailable)
  console.log(`Entered "${e.identifier}" at ~${e.distance.toFixed(1)}m`);
});
onBeaconExit

Fired when the device leaves the region. Always fires regardless of maxDistance setting.

ExpoBeacon.addListener("onBeaconExit", (e) => {
  console.log(`Left "${e.identifier}"`);
});
onBeaconDistance

Fired continuously during monitoring with the latest distance reading. Useful for proximity-based UI.

ExpoBeacon.addListener("onBeaconDistance", (e) => {
  // e.identifier, e.uuid, e.major, e.minor, e.distance
  updateProximityBar(e.identifier, e.distance);
});
onBeaconFound

Fired during startContinuousScan() each time an iBeacon advertisement is received.

ExpoBeacon.addListener("onBeaconFound", (b) => {
  console.log(`${b.uuid} ${b.major}/${b.minor}${b.distance.toFixed(1)}m RSSI: ${b.rssi}`);
});
onEddystoneFound

Fired during startContinuousScan() each time an Eddystone advertisement is received.

ExpoBeacon.addListener("onEddystoneFound", (b) => {
  if (b.frameType === "uid") {
    console.log(`UID: ${b.namespace}/${b.instance}${b.distance.toFixed(1)}m`);
  } else {
    console.log(`URL: ${b.url}${b.distance.toFixed(1)}m`);
  }
});
onEddystoneEnter

Fired when a paired Eddystone-UID beacon enters range during monitoring.

ExpoBeacon.addListener("onEddystoneEnter", (e) => {
  console.log(`Eddystone "${e.identifier}" entered (ns: ${e.namespace})`);
});
onEddystoneExit

Fired when a paired Eddystone-UID beacon leaves range.

ExpoBeacon.addListener("onEddystoneExit", (e) => {
  console.log(`Eddystone "${e.identifier}" exited`);
});
onEddystoneDistance

Fired continuously during monitoring with the latest Eddystone distance reading.

ExpoBeacon.addListener("onEddystoneDistance", (e) => {
  console.log(`Eddystone "${e.identifier}" → ${e.distance.toFixed(2)}m`);
});
onBeaconTimeout

Fired once, timeoutSeconds after a paired iBeacon exits range (or after BLE readings stop for 60 s). Re-detection cancels the pending timer.

ExpoBeacon.addListener("onBeaconTimeout", (e) => {
  // e.identifier — "lobby-entrance"
  // e.uuid, e.major, e.minor — beacon identity
  // e.distance — usually –1 (the beacon is out of range when this fires)
  console.log(`Beacon "${e.identifier}" timeout — out of range for configured duration`);
});
onEddystoneTimeout

Fired once, timeoutSeconds after a paired Eddystone exits range (or after BLE readings stop for 60 s). Re-detection cancels the pending timer.

ExpoBeacon.addListener("onEddystoneTimeout", (e) => {
  // e.identifier, e.namespace, e.instance — Eddystone identity
  // e.distance — usually –1 (the beacon is out of range when this fires)
  console.log(`Eddystone "${e.identifier}" timeout`);
});

TypeScript Types

All types are exported from the package:

import type {
  BeaconScanResult,
  PairedBeacon,
  BeaconRegionEvent,
  BeaconDistanceEvent,
  BeaconTimeoutEvent,
  EddystoneFrameType,
  EddystoneScanResult,
  PairedEddystone,
  EddystoneRegionEvent,
  EddystoneDistanceEvent,
  EddystoneTimeoutEvent,
  BeaconErrorEvent,
  ExpoBeaconModuleEvents,
  MonitoringOptions,
  NotificationConfig,
  BeaconNotificationSettings,
  BeaconNotificationConfig,
  CarPlayNotificationSettings,
  CarPlayNotificationConfig,
  ForegroundServiceConfig,
  NotificationChannelConfig,
  CarPlayChannelConfig,
  EventLogQueryOptions,
  EventLogEntry,
} from "expo-beacon";
BeaconScanResult

Returned by scanForBeaconsAsync() and onBeaconFound.

type BeaconScanResult = {
  uuid: string;      // Proximity UUID, uppercase (e.g. "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
  major: number;     // 065535
  minor: number;     // 065535
  rssi: number;      // Signal strength in dBm (negative, e.g. –65)
  distance: number;  // Estimated distance in metres (–1 when unavailable)
  txPower: number;   // Calibrated TX power. Android onlyalways 0 on iOS (CoreLocation does not expose it)
};
PairedBeacon

Returned by getPairedBeacons().

type PairedBeacon = {
  identifier: string; // Your label
  uuid: string;
  major: number;
  minor: number;
  name?: string;           // Optional BLE device name
  timeoutSeconds?: number; // Fires onBeaconTimeout this many seconds after the beacon exits range
};
BeaconRegionEvent

Payload for onBeaconEnter / onBeaconExit.

type BeaconRegionEvent = {
  identifier: string;        // Matches PairedBeacon.identifier
  uuid: string;
  major: number;
  minor: number;
  event: "enter" | "exit";
  distance: number;          // Metres at event time; –1 if unavailable
};
BeaconDistanceEvent

Payload for onBeaconDistance.

type BeaconDistanceEvent = {
  identifier: string;
  uuid: string;
  major: number;
  minor: number;
  distance: number;  // Estimated distance in metres
};
EddystoneScanResult

Returned by scanForEddystonesAsync() and onEddystoneFound.

type EddystoneScanResult = {
  frameType: "uid" | "url";
  namespace?: string;  // 20 hex chars. Present for UID frames.
  instance?: string;   // 12 hex chars. Present for UID frames.
  url?: string;        // Decoded URL. Present for URL frames.
  rssi: number;
  distance: number;
  txPower: number;
};
PairedEddystone

Returned by getPairedEddystones().

type PairedEddystone = {
  identifier: string;
  namespace: string;   // 20 hex chars
  instance: string;    // 12 hex chars
  name?: string;           // Optional BLE device name
  timeoutSeconds?: number; // Fires onEddystoneTimeout this many seconds after the beacon exits range
};
EddystoneRegionEvent

Payload for onEddystoneEnter / onEddystoneExit.

type EddystoneRegionEvent = {
  identifier: string;
  namespace: string;
  instance: string;
  event: "enter" | "exit";
  distance: number;           // Metres; –1 if unavailable
};
EddystoneDistanceEvent

Payload for onEddystoneDistance.

type EddystoneDistanceEvent = {
  identifier: string;
  namespace: string;
  instance: string;
  distance: number;
};
MonitoringOptions

Passed to startMonitoring().

type MonitoringOptions = {
  maxDistance?: number;
  exitDistance?: number;
  minRssi?: number;
  level?: "all" | "events";
};
MonitoringConfig

Returned by getMonitoringConfig().

type MonitoringConfig = {
  isMonitoring: boolean;
  maxDistance?: number;
  exitDistance?: number;
  minRssi?: number;
  level?: "all" | "events";
  notifications?: NotificationConfig;
};
MonitoredDeviceState

Returned by getMonitoredDeviceState() and getMonitoredDeviceStates().

type MonitoredDeviceState =
  | {
      kind: "ibeacon";
      identifier: string;
      uuid: string;
      major: number;
      minor: number;
      state: "entered" | "exited";
      distance: number | null;
    }
  | {
      kind: "eddystone";
      identifier: string;
      namespace: string;
      instance: string;
      state: "entered" | "exited";
      distance: number | null;
    };
NotificationConfig

Top-level notification configuration.

type NotificationConfig = {
  beacons?: BeaconNotificationSettings;
  carPlay?: CarPlayNotificationSettings;

  // Legacy aliases, still accepted:
  beaconEvents?: BeaconNotificationConfig;
  carPlayEvents?: CarPlayNotificationConfig;
  foregroundService?: ForegroundServiceConfig;
  channel?: NotificationChannelConfig;
  carPlayChannel?: CarPlayChannelConfig;
};
BeaconNotificationSettings
type BeaconNotificationSettings = {
  events?: BeaconNotificationConfig;
  foregroundService?: ForegroundServiceConfig; // Android only, title/text/icon only
  channel?: NotificationChannelConfig;         // Android only, beacon alert channel
};
CarPlayNotificationSettings
type CarPlayNotificationSettings = {
  events?: CarPlayNotificationConfig;
  foregroundService?: ForegroundServiceConfig; // Android only, CarPlay-only title/text/icon
  channel?: CarPlayChannelConfig;              // Android only, CarPlay alert channel
};
BeaconNotificationConfig
type BeaconNotificationConfig = {
  enabled?: boolean;     // Default: true. Set false to suppress.
  enterTitle?: string;   // Default: "Beacon Entered"
  exitTitle?: string;    // Default: "Beacon Exited"
  timeoutTitle?: string; // Default: "Beacon Timeout"
  body?: string;         // Default: "{identifier} region {event}ed"
                         // Supports {identifier} and {event} placeholders.
  sound?: boolean;       // iOS only. Default: true
  icon?: string;         // Android only. Drawable resource name.
};
ForegroundServiceConfig
type ForegroundServiceConfig = {
  title?: string;  // Default: "Beacon Monitoring Active"
  text?: string;   // Default: "Monitoring for iBeacons in the background"
  icon?: string;   // Android drawable resource name
};

Foreground service notifications always use the dedicated Android channel expo_beacon_foreground_channel, created with low importance and no sound/vibration. This config only changes the notification content.

NotificationChannelConfig
type NotificationChannelConfig = {
  name?: string;                           // Default: "Beacon Monitoring"
  description?: string;                    // Default: "Used for background iBeacon region monitoring"
  importance?: "low" | "default" | "high"; // Default: "low"
};
CarPlayNotificationConfig
type CarPlayNotificationConfig = {
  enabled?: boolean;          // Default: true. Set false to suppress.
  connectedTitle?: string;    // Default: "CarPlay Connected"
  disconnectedTitle?: string; // Default: "CarPlay Disconnected"
  body?: string;              // Default: "CarPlay session {event}"
                              // Supports {event} and {transport} placeholders.
  sound?: boolean;            // iOS only. Default: true
  icon?: string;              // Android only. Drawable resource name.
};
CarPlayChannelConfig
type CarPlayChannelConfig = {
  name?: string;                            // Default: "CarPlay / Android Auto"
  description?: string;                     // Default: "CarPlay and Android Auto connect/disconnect notifications"
  importance?: "low" | "default" | "high"; // Default: "default"
};
BeaconTimeoutEvent

Payload for onBeaconTimeout.

type BeaconTimeoutEvent = {
  identifier: string;
  uuid: string;
  major: number;
  minor: number;
  distance: number; // Usually1 (the beacon is out of range when the timeout fires)
};
EddystoneTimeoutEvent

Payload for onEddystoneTimeout.

type EddystoneTimeoutEvent = {
  identifier: string;
  namespace: string;
  instance: string;
  distance: number; // Usually1 (the beacon is out of range when the timeout fires)
};
EventLogQueryOptions

Passed to `getEve

Keywords