expo-beacon 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +851 -375
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/AndroidManifest.xml +2 -1
  4. package/android/src/main/java/expo/modules/beacon/BeaconConstants.kt +26 -0
  5. package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +5 -0
  6. package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +113 -115
  7. package/android/src/main/java/expo/modules/beacon/BeaconParsers.kt +33 -0
  8. package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +219 -115
  9. package/build/ExpoBeacon.types.d.ts +12 -2
  10. package/build/ExpoBeacon.types.d.ts.map +1 -1
  11. package/build/ExpoBeacon.types.js.map +1 -1
  12. package/build/ExpoBeaconModule.d.ts +5 -0
  13. package/build/ExpoBeaconModule.d.ts.map +1 -1
  14. package/build/ExpoBeaconModule.js.map +1 -1
  15. package/build/ExpoBeaconModule.web.d.ts +4 -0
  16. package/build/ExpoBeaconModule.web.d.ts.map +1 -1
  17. package/build/ExpoBeaconModule.web.js +4 -0
  18. package/build/ExpoBeaconModule.web.js.map +1 -1
  19. package/ios/ExpoBeaconModule.swift +368 -215
  20. package/package.json +1 -1
  21. package/src/ExpoBeacon.types.ts +12 -2
  22. package/src/ExpoBeaconModule.ts +6 -0
  23. package/src/ExpoBeaconModule.web.ts +4 -0
  24. package/build/ExpoBeaconView.d.ts +0 -2
  25. package/build/ExpoBeaconView.d.ts.map +0 -1
  26. package/build/ExpoBeaconView.js +0 -2
  27. package/build/ExpoBeaconView.js.map +0 -1
  28. package/build/ExpoBeaconView.web.d.ts +0 -2
  29. package/build/ExpoBeaconView.web.d.ts.map +0 -1
  30. package/build/ExpoBeaconView.web.js +0 -2
  31. package/build/ExpoBeaconView.web.js.map +0 -1
  32. package/ios/ExpoBeaconView.swift +0 -5
  33. package/src/ExpoBeaconView.tsx +0 -2
  34. package/src/ExpoBeaconView.web.tsx +0 -2
package/README.md CHANGED
@@ -1,18 +1,63 @@
1
1
  # expo-beacon
2
2
 
3
- An Expo module for scanning, pairing, and monitoring iBeacons and Eddystone beacons in React Native apps.
3
+ 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.
4
+
5
+ | Feature | Description |
6
+ |---|---|
7
+ | **Scan** | Discover nearby iBeacons (one-shot or continuous) and Eddystone-UID / Eddystone-URL beacons via BLE |
8
+ | **Pair** | Register specific beacons for persistent tracking — survives app restarts |
9
+ | **Monitor** | Background enter/exit region detection with distance-based filtering |
10
+ | **Distance** | Real-time distance updates (~1/sec) while monitoring |
11
+ | **Notifications** | Automatic local notifications on region enter/exit, fully customisable |
12
+
13
+ | Platform | Native Implementation |
14
+ |---|---|
15
+ | **Android** | [AltBeacon](https://altbeacon.github.io/android-beacon-library/) library + Foreground Service |
16
+ | **iOS** | CoreLocation (iBeacon ranging & monitoring) + CoreBluetooth (Eddystone & wildcard BLE) |
17
+ | **Web** | Not supported (throws on all calls) |
4
18
 
5
- - **Scan** for nearby iBeacons and Eddystone beacons via one-shot or continuous BLE scans
6
- - **Pair** specific beacons for persistent tracking across app restarts
7
- - **Monitor** paired beacons in the background with enter/exit callbacks
8
- - **Distance events** fired continuously while a monitored beacon is in range
9
- - **Native notifications** shown automatically on region enter/exit
19
+ ---
10
20
 
11
- | Platform | Native implementation |
12
- | -------- | --------------------------------------------------------------------------------------------------------- |
13
- | Android | [AltBeacon](https://altbeacon.github.io/android-beacon-library/) (`org.altbeacon:android-beacon-library`) |
14
- | iOS | CoreLocation (UUID-targeted scans & monitoring) + CoreBluetooth (wildcard scanning & Eddystone) |
15
- | Web | Not supported (throws on all calls) |
21
+ ## Table of Contents
22
+
23
+ - [Installation](#installation)
24
+ - [Platform Setup](#platform-setup)
25
+ - [iOS](#ios)
26
+ - [Android](#android)
27
+ - [Quick Start](#quick-start)
28
+ - [Usage Examples](#usage-examples)
29
+ - [Scanning for iBeacons](#scanning-for-ibeacons)
30
+ - [Scanning for Eddystone Beacons](#scanning-for-eddystone-beacons)
31
+ - [Continuous (Live) Scanning](#continuous-live-scanning)
32
+ - [Pairing & Unpairing Beacons](#pairing--unpairing-beacons)
33
+ - [Background Monitoring](#background-monitoring)
34
+ - [Customizing Notifications](#customizing-notifications)
35
+ - [Cancelling a Scan](#cancelling-a-scan)
36
+ - [Full API Reference](#full-api-reference)
37
+ - [requestPermissionsAsync()](#requestpermissionsasync)
38
+ - [scanForBeaconsAsync()](#scanforbeaconsasyncuuids-scandurationms)
39
+ - [scanForEddystonesAsync()](#scanforeddystonesasyncscanDurationms)
40
+ - [startContinuousScan()](#startcontinuousscan)
41
+ - [stopContinuousScan()](#stopcontinuousscan)
42
+ - [cancelScan()](#cancelscan)
43
+ - [pairBeacon()](#pairbeaconidentifier-uuid-major-minor)
44
+ - [unpairBeacon()](#unpairbeaconidentifier)
45
+ - [getPairedBeacons()](#getpairedbeacons)
46
+ - [pairEddystone()](#paireddystoneidentifier-namespace-instance)
47
+ - [unpairEddystone()](#unpaireddystoneidentifier)
48
+ - [getPairedEddystones()](#getpairededdystones)
49
+ - [startMonitoring()](#startmonitoringoptions)
50
+ - [stopMonitoring()](#stopmonitoring)
51
+ - [setNotificationConfig()](#setnotificationconfigconfig)
52
+ - [Events](#events)
53
+ - [TypeScript Types](#typescript-types)
54
+ - [Background Behaviour](#background-behaviour)
55
+ - [Notifications](#notifications)
56
+ - [Platform-Specific Notes & Gotchas](#platform-specific-notes--gotchas)
57
+ - [Troubleshooting](#troubleshooting)
58
+ - [Error Codes](#error-codes)
59
+ - [Contributing](#contributing)
60
+ - [License](#license)
16
61
 
17
62
  ---
18
63
 
@@ -22,7 +67,7 @@ An Expo module for scanning, pairing, and monitoring iBeacons and Eddystone beac
22
67
  npx expo install expo-beacon
23
68
  ```
24
69
 
25
- > This module contains native code and **cannot** be used with Expo Go. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/).
70
+ > **Important**: This module contains native code and **cannot** be used with Expo Go. You must use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) or a bare workflow.
26
71
 
27
72
  ---
28
73
 
@@ -30,7 +75,9 @@ npx expo install expo-beacon
30
75
 
31
76
  ### iOS
32
77
 
33
- Add the following keys to your `Info.plist`:
78
+ #### 1. Info.plist Keys
79
+
80
+ Add the following keys to your `Info.plist` (or use an Expo config plugin):
34
81
 
35
82
  ```xml
36
83
  <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
@@ -41,47 +88,46 @@ Add the following keys to your `Info.plist`:
41
88
  <string>This app uses Bluetooth to scan for iBeacons.</string>
42
89
  ```
43
90
 
91
+ #### 2. Background Modes
92
+
44
93
  In Xcode under **Signing & Capabilities**, enable:
45
94
 
46
95
  - **Background Modes → Location updates**
47
96
  - **Background Modes → Uses Bluetooth LE accessories**
48
97
 
49
- > iOS limits apps to **20 simultaneously monitored regions** (iBeacon only — Eddystone beacons are monitored via BLE and do not count toward this limit).
50
- >
51
- > **iBeacon scanning on iOS**: Apple strips iBeacon manufacturer data from CoreBluetooth BLE advertisements, so wildcard iBeacon discovery is **not possible** on iOS. You must provide at least one proximity UUID, or pair beacons first (the module will use paired beacon UUIDs automatically). UUID-targeted scans use CoreLocation ranging and work in both foreground and background.
52
- >
53
- > **Eddystone scanning**: Eddystone beacons use standard BLE service data (UUID `0xFEAA`), which iOS does not filter. `scanForEddystonesAsync()` and continuous scanning discover Eddystones on both platforms without restrictions.
98
+ #### Key iOS Constraints
99
+
100
+ - **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.
101
+ - **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).
102
+ - **Eddystone works unrestricted**: Eddystone uses standard BLE service data (`0xFEAA`), which iOS does not strip. Both `scanForEddystonesAsync()` and continuous scanning discover Eddystones without restrictions.
54
103
 
55
104
  ### Android
56
105
 
57
- All required permissions are declared by the module's `AndroidManifest.xml` and merged into your app automatically. You must still request runtime permissions before scanning or monitoring — the easiest way is:
106
+ All required permissions are declared in the module's `AndroidManifest.xml` and merged automatically. You must still request **runtime permissions** before scanning or monitoring:
58
107
 
59
108
  ```ts
60
- await ExpoBeacon.requestPermissionsAsync();
109
+ const granted = await ExpoBeacon.requestPermissionsAsync();
61
110
  ```
62
111
 
112
+ The module requests: `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION`, and `POST_NOTIFICATIONS` (API 33+).
113
+
63
114
  ---
64
115
 
65
- ## Quick-start example
116
+ ## Quick Start
117
+
118
+ A minimal example that pairs one iBeacon and one Eddystone, starts monitoring, and scans for nearby beacons:
66
119
 
67
120
  ```tsx
68
121
  import { useEffect, useState } from "react";
69
122
  import { Button, FlatList, Text, View } from "react-native";
70
123
  import ExpoBeacon from "expo-beacon";
71
- import type {
72
- BeaconScanResult,
73
- BeaconRegionEvent,
74
- BeaconDistanceEvent,
75
- EddystoneScanResult,
76
- EddystoneRegionEvent,
77
- EddystoneDistanceEvent,
78
- } from "expo-beacon";
124
+ import type { BeaconScanResult, BeaconRegionEvent } from "expo-beacon";
79
125
 
80
126
  export default function App() {
81
127
  const [beacons, setBeacons] = useState<BeaconScanResult[]>([]);
82
128
 
83
129
  useEffect(() => {
84
- // 1. Pair a known iBeacon for monitoring
130
+ // 1. Pair beacons you want to monitor
85
131
  ExpoBeacon.pairBeacon(
86
132
  "lobby-entrance",
87
133
  "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
@@ -89,76 +135,221 @@ export default function App() {
89
135
  100,
90
136
  );
91
137
 
92
- // 1b. Pair a known Eddystone-UID beacon for monitoring
93
- ExpoBeacon.pairEddystone(
94
- "meeting-room",
95
- "edd1ebeac04e5defa017", // 10-byte namespace
96
- "0123456789ab", // 6-byte instance
97
- );
98
-
99
- // 2. Subscribe to iBeacon region events
100
- const enterSub = ExpoBeacon.addListener(
101
- "onBeaconEnter",
102
- (e: BeaconRegionEvent) =>
103
- console.log(`Entered ${e.identifier} at ${e.distance.toFixed(1)} m`),
104
- );
105
- const exitSub = ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) =>
106
- console.log(`Exited ${e.identifier}`),
107
- );
108
- const distSub = ExpoBeacon.addListener(
109
- "onBeaconDistance",
110
- (e: BeaconDistanceEvent) =>
111
- console.log(`${e.identifier}: ${e.distance.toFixed(2)} m`),
112
- );
113
-
114
- // 3. Subscribe to Eddystone region events
115
- const eddyEnterSub = ExpoBeacon.addListener(
116
- "onEddystoneEnter",
117
- (e: EddystoneRegionEvent) =>
118
- console.log(`Eddystone entered ${e.identifier} (${e.namespace})`),
119
- );
120
- const eddyExitSub = ExpoBeacon.addListener(
121
- "onEddystoneExit",
122
- (e: EddystoneRegionEvent) =>
123
- console.log(`Eddystone exited ${e.identifier}`),
124
- );
125
- const eddyDistSub = ExpoBeacon.addListener(
126
- "onEddystoneDistance",
127
- (e: EddystoneDistanceEvent) =>
128
- console.log(`Eddystone ${e.identifier}: ${e.distance.toFixed(2)} m`),
129
- );
138
+ // 2. Listen for enter/exit events
139
+ const enterSub = ExpoBeacon.addListener("onBeaconEnter", (e: BeaconRegionEvent) => {
140
+ console.log(`Entered ${e.identifier} at ${e.distance.toFixed(1)} m`);
141
+ });
142
+ const exitSub = ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) => {
143
+ console.log(`Exited ${e.identifier}`);
144
+ });
130
145
 
131
- // 4. Start background monitoring (fires for both paired iBeacons and Eddystones)
146
+ // 3. Request permissions and start monitoring
132
147
  ExpoBeacon.requestPermissionsAsync().then((granted) => {
133
- if (granted) ExpoBeacon.startMonitoring(10); // enter events within 10 m
148
+ if (granted) ExpoBeacon.startMonitoring(10); // enter within 10 m
134
149
  });
135
150
 
136
151
  return () => {
137
152
  enterSub.remove();
138
153
  exitSub.remove();
139
- distSub.remove();
140
- eddyEnterSub.remove();
141
- eddyExitSub.remove();
142
- eddyDistSub.remove();
143
154
  ExpoBeacon.stopMonitoring();
144
155
  };
145
156
  }, []);
146
157
 
147
158
  async function scan() {
148
- // Wildcard scan discovers all nearby iBeacons (no UUID filter)
149
- const results = await ExpoBeacon.scanForBeaconsAsync([], 5000);
159
+ const results = await ExpoBeacon.scanForBeaconsAsync(
160
+ ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
161
+ 5000
162
+ );
150
163
  setBeacons(results);
151
164
  }
152
165
 
153
166
  return (
154
- <View>
167
+ <View style={{ flex: 1, padding: 20, paddingTop: 60 }}>
155
168
  <Button title="Scan 5 s" onPress={scan} />
156
169
  <FlatList
157
170
  data={beacons}
158
171
  keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
172
+ renderItem={({ item: b }) => (
173
+ <Text>{b.uuid} {b.major}/{b.minor} — {b.distance.toFixed(1)} m</Text>
174
+ )}
175
+ />
176
+ </View>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Usage Examples
184
+
185
+ ### Scanning for iBeacons
186
+
187
+ #### One-shot scan with UUID filter (both platforms)
188
+
189
+ ```ts
190
+ import ExpoBeacon from "expo-beacon";
191
+
192
+ // Scan for 8 seconds, filtering by a specific UUID
193
+ const beacons = await ExpoBeacon.scanForBeaconsAsync(
194
+ ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
195
+ 8000,
196
+ );
197
+
198
+ beacons.forEach((b) => {
199
+ console.log(
200
+ `UUID: ${b.uuid} Major: ${b.major} Minor: ${b.minor} ` +
201
+ `Distance: ${b.distance.toFixed(1)}m RSSI: ${b.rssi}dBm`
202
+ );
203
+ });
204
+ ```
205
+
206
+ #### Wildcard scan (Android only)
207
+
208
+ ```ts
209
+ // Pass an empty array to discover ALL nearby iBeacons
210
+ // On iOS, this auto-uses UUIDs from paired beacons
211
+ const beacons = await ExpoBeacon.scanForBeaconsAsync([], 5000);
212
+ ```
213
+
214
+ #### Multiple UUID scan
215
+
216
+ ```ts
217
+ // Scan for beacons from two different manufacturers/deployments
218
+ const beacons = await ExpoBeacon.scanForBeaconsAsync(
219
+ [
220
+ "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
221
+ "FDA50693-A4E2-4FB1-AFCF-C6EB07647825",
222
+ ],
223
+ 10000,
224
+ );
225
+ ```
226
+
227
+ ---
228
+
229
+ ### Scanning for Eddystone Beacons
230
+
231
+ ```ts
232
+ import ExpoBeacon from "expo-beacon";
233
+
234
+ // Discover both Eddystone-UID and Eddystone-URL frames
235
+ const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
236
+
237
+ eddystones.forEach((b) => {
238
+ if (b.frameType === "uid") {
239
+ console.log(`UID: namespace=${b.namespace} instance=${b.instance} dist=${b.distance.toFixed(1)}m`);
240
+ } else if (b.frameType === "url") {
241
+ console.log(`URL: ${b.url} dist=${b.distance.toFixed(1)}m`);
242
+ }
243
+ });
244
+ ```
245
+
246
+ > Eddystone scanning works identically on both iOS and Android — no UUID filter required.
247
+
248
+ ---
249
+
250
+ ### Continuous (Live) Scanning
251
+
252
+ 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.
253
+
254
+ ```tsx
255
+ import { useEffect, useRef, useState } from "react";
256
+ import { FlatList, Text, Button, View } from "react-native";
257
+ import ExpoBeacon from "expo-beacon";
258
+ import type { BeaconScanResult, EddystoneScanResult } from "expo-beacon";
259
+
260
+ export default function LiveScanner() {
261
+ const [ibeacons, setIbeacons] = useState<BeaconScanResult[]>([]);
262
+ const [eddystones, setEddystones] = useState<EddystoneScanResult[]>([]);
263
+ const [scanning, setScanning] = useState(false);
264
+ const subs = useRef<Array<{ remove: () => void }>>([]);
265
+
266
+ const startScan = () => {
267
+ setScanning(true);
268
+
269
+ // iBeacon advertisements
270
+ subs.current.push(
271
+ ExpoBeacon.addListener("onBeaconFound", (beacon) => {
272
+ setIbeacons((prev) => {
273
+ const key = `${beacon.uuid}-${beacon.major}-${beacon.minor}`;
274
+ const idx = prev.findIndex(
275
+ (b) => `${b.uuid}-${b.major}-${b.minor}` === key,
276
+ );
277
+ if (idx >= 0) {
278
+ const copy = [...prev];
279
+ copy[idx] = beacon; // Update distance/RSSI
280
+ return copy;
281
+ }
282
+ return [...prev, beacon];
283
+ });
284
+ }),
285
+ );
286
+
287
+ // Eddystone advertisements
288
+ subs.current.push(
289
+ ExpoBeacon.addListener("onEddystoneFound", (beacon) => {
290
+ setEddystones((prev) => {
291
+ const key = beacon.frameType === "uid"
292
+ ? `${beacon.namespace}-${beacon.instance}`
293
+ : `url-${beacon.url}`;
294
+ const idx = prev.findIndex((b) => {
295
+ const k = b.frameType === "uid"
296
+ ? `${b.namespace}-${b.instance}`
297
+ : `url-${b.url}`;
298
+ return k === key;
299
+ });
300
+ if (idx >= 0) {
301
+ const copy = [...prev];
302
+ copy[idx] = beacon;
303
+ return copy;
304
+ }
305
+ return [...prev, beacon];
306
+ });
307
+ }),
308
+ );
309
+
310
+ ExpoBeacon.startContinuousScan();
311
+ };
312
+
313
+ const stopScan = () => {
314
+ ExpoBeacon.stopContinuousScan();
315
+ subs.current.forEach((s) => s.remove());
316
+ subs.current = [];
317
+ setScanning(false);
318
+ };
319
+
320
+ useEffect(() => {
321
+ return () => stopScan(); // Cleanup on unmount
322
+ }, []);
323
+
324
+ return (
325
+ <View style={{ flex: 1, padding: 20 }}>
326
+ <Button
327
+ title={scanning ? "Stop Scan" : "Start Live Scan"}
328
+ onPress={scanning ? stopScan : startScan}
329
+ />
330
+ <Text style={{ fontWeight: "bold", marginTop: 10 }}>
331
+ iBeacons ({ibeacons.length})
332
+ </Text>
333
+ <FlatList
334
+ data={ibeacons}
335
+ keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
159
336
  renderItem={({ item: b }) => (
160
337
  <Text>
161
- {b.uuid} {b.major}/{b.minor} — {b.distance.toFixed(1)} m
338
+ {b.uuid.slice(0, 8)} {b.major}/{b.minor} — {b.distance.toFixed(1)}m (RSSI: {b.rssi})
339
+ </Text>
340
+ )}
341
+ />
342
+ <Text style={{ fontWeight: "bold", marginTop: 10 }}>
343
+ Eddystones ({eddystones.length})
344
+ </Text>
345
+ <FlatList
346
+ data={eddystones}
347
+ keyExtractor={(b, i) => `eddy-${i}`}
348
+ renderItem={({ item: b }) => (
349
+ <Text>
350
+ {b.frameType === "uid"
351
+ ? `UID: ${b.namespace?.slice(0, 8)}… / ${b.instance}`
352
+ : `URL: ${b.url}`} — {b.distance.toFixed(1)}m
162
353
  </Text>
163
354
  )}
164
355
  />
@@ -167,9 +358,225 @@ export default function App() {
167
358
  }
168
359
  ```
169
360
 
361
+ > **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.
362
+
363
+ ---
364
+
365
+ ### Pairing & Unpairing Beacons
366
+
367
+ Pairing registers a beacon for persistent monitoring. Paired beacons survive app restarts — they are stored in `UserDefaults` (iOS) / `SharedPreferences` (Android).
368
+
369
+ ```ts
370
+ import ExpoBeacon from "expo-beacon";
371
+
372
+ // ── iBeacon ──
373
+
374
+ // Pair an iBeacon (identifier must be unique)
375
+ ExpoBeacon.pairBeacon(
376
+ "lobby-entrance", // your label
377
+ "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", // proximity UUID
378
+ 1, // major (0–65535)
379
+ 100, // minor (0–65535)
380
+ );
381
+
382
+ // Re-pairing with the same identifier replaces the previous entry
383
+ ExpoBeacon.pairBeacon(
384
+ "lobby-entrance",
385
+ "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
386
+ 1,
387
+ 200, // updated minor
388
+ );
389
+
390
+ // List all paired iBeacons
391
+ const paired = ExpoBeacon.getPairedBeacons();
392
+ console.log(paired);
393
+ // → [{ identifier: "lobby-entrance", uuid: "E2C5…", major: 1, minor: 200 }]
394
+
395
+ // Remove a beacon
396
+ ExpoBeacon.unpairBeacon("lobby-entrance");
397
+
398
+ // ── Eddystone-UID ──
399
+
400
+ // Pair an Eddystone-UID beacon
401
+ ExpoBeacon.pairEddystone(
402
+ "meeting-room", // your label
403
+ "edd1ebeac04e5defa017", // 10-byte namespace (20 hex chars)
404
+ "0123456789ab", // 6-byte instance (12 hex chars)
405
+ );
406
+
407
+ // List all paired Eddystones
408
+ const pairedEddy = ExpoBeacon.getPairedEddystones();
409
+ console.log(pairedEddy);
410
+ // → [{ identifier: "meeting-room", namespace: "edd1…", instance: "0123…" }]
411
+
412
+ // Remove an Eddystone
413
+ ExpoBeacon.unpairEddystone("meeting-room");
414
+ ```
415
+
416
+ ---
417
+
418
+ ### Background Monitoring
419
+
420
+ Monitoring watches all paired beacons (iBeacon + Eddystone) in the background and fires events when the device enters or exits a beacon region.
421
+
422
+ ```tsx
423
+ import { useEffect, useRef } from "react";
424
+ import ExpoBeacon from "expo-beacon";
425
+ import type {
426
+ BeaconRegionEvent,
427
+ BeaconDistanceEvent,
428
+ EddystoneRegionEvent,
429
+ EddystoneDistanceEvent,
430
+ } from "expo-beacon";
431
+
432
+ export function useBeaconMonitoring() {
433
+ const subs = useRef<Array<{ remove: () => void }>>([]);
434
+
435
+ useEffect(() => {
436
+ async function start() {
437
+ const granted = await ExpoBeacon.requestPermissionsAsync();
438
+ if (!granted) {
439
+ console.warn("Beacon permissions denied");
440
+ return;
441
+ }
442
+
443
+ // Subscribe to iBeacon events
444
+ subs.current.push(
445
+ ExpoBeacon.addListener("onBeaconEnter", (e: BeaconRegionEvent) => {
446
+ console.log(`[iBeacon] Entered "${e.identifier}" at ~${e.distance.toFixed(1)}m`);
447
+ }),
448
+ ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) => {
449
+ console.log(`[iBeacon] Exited "${e.identifier}"`);
450
+ }),
451
+ ExpoBeacon.addListener("onBeaconDistance", (e: BeaconDistanceEvent) => {
452
+ console.log(`[iBeacon] "${e.identifier}" → ${e.distance.toFixed(2)}m`);
453
+ }),
454
+ );
455
+
456
+ // Subscribe to Eddystone events
457
+ subs.current.push(
458
+ ExpoBeacon.addListener("onEddystoneEnter", (e: EddystoneRegionEvent) => {
459
+ console.log(`[Eddystone] Entered "${e.identifier}"`);
460
+ }),
461
+ ExpoBeacon.addListener("onEddystoneExit", (e: EddystoneRegionEvent) => {
462
+ console.log(`[Eddystone] Exited "${e.identifier}"`);
463
+ }),
464
+ ExpoBeacon.addListener("onEddystoneDistance", (e: EddystoneDistanceEvent) => {
465
+ console.log(`[Eddystone] "${e.identifier}" → ${e.distance.toFixed(2)}m`);
466
+ }),
467
+ );
468
+
469
+ // Start with distance threshold
470
+ await ExpoBeacon.startMonitoring({
471
+ maxDistance: 10, // Only fire "enter" within 10 metres
472
+ notifications: {
473
+ beaconEvents: {
474
+ enterTitle: "You're near a beacon!",
475
+ exitTitle: "Beacon out of range",
476
+ body: "{identifier} {event}ed",
477
+ },
478
+ },
479
+ });
480
+ }
481
+
482
+ start();
483
+
484
+ return () => {
485
+ subs.current.forEach((s) => s.remove());
486
+ subs.current = [];
487
+ ExpoBeacon.stopMonitoring();
488
+ };
489
+ }, []);
490
+ }
491
+ ```
492
+
493
+ #### Simple shorthand (number = maxDistance)
494
+
495
+ ```ts
496
+ // Equivalent to { maxDistance: 5 }
497
+ await ExpoBeacon.startMonitoring(5);
498
+ ```
499
+
500
+ #### Monitor with no distance filter
501
+
502
+ ```ts
503
+ // Monitor without distance limit — enter fires as soon as the region is detected
504
+ await ExpoBeacon.startMonitoring();
505
+ ```
506
+
507
+ ---
508
+
509
+ ### Customizing Notifications
510
+
511
+ #### Persistent configuration (survives app restarts)
512
+
513
+ ```ts
514
+ ExpoBeacon.setNotificationConfig({
515
+ // Enter/exit alert notifications (both platforms)
516
+ beaconEvents: {
517
+ enabled: true, // Set false to suppress notifications entirely
518
+ enterTitle: "Beacon nearby",
519
+ exitTitle: "Beacon out of range",
520
+ body: "{identifier} {event}ed", // Placeholders: {identifier}, {event}
521
+ sound: true, // iOS only
522
+ icon: "ic_beacon_notification", // Android only — drawable resource name
523
+ },
524
+
525
+ // Persistent status-bar notification (Android only)
526
+ foregroundService: {
527
+ title: "My App — Monitoring",
528
+ text: "Watching for nearby beacons",
529
+ icon: "ic_service",
530
+ },
531
+
532
+ // Android notification channel
533
+ channel: {
534
+ name: "Proximity Alerts",
535
+ description: "Alerts when beacons enter or leave range",
536
+ importance: "default", // "low" | "default" | "high"
537
+ },
538
+ });
539
+ ```
540
+
541
+ #### One-off session configuration (inline with startMonitoring)
542
+
543
+ ```ts
544
+ await ExpoBeacon.startMonitoring({
545
+ maxDistance: 5,
546
+ notifications: {
547
+ beaconEvents: { enabled: false }, // Silent monitoring — no user-facing alerts
548
+ },
549
+ });
550
+ ```
551
+
552
+ ---
553
+
554
+ ### Cancelling a Scan
555
+
556
+ Cancel any in-progress one-shot scan (iBeacon or Eddystone). The pending promise will reject with error code `SCAN_CANCELLED`.
557
+
558
+ ```ts
559
+ // Start a long scan
560
+ const scanPromise = ExpoBeacon.scanForBeaconsAsync(
561
+ ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
562
+ 30000,
563
+ );
564
+
565
+ // Cancel it after 2 seconds
566
+ setTimeout(() => ExpoBeacon.cancelScan(), 2000);
567
+
568
+ try {
569
+ const results = await scanPromise;
570
+ } catch (e) {
571
+ if (e.code === "SCAN_CANCELLED") {
572
+ console.log("Scan was cancelled by user");
573
+ }
574
+ }
575
+ ```
576
+
170
577
  ---
171
578
 
172
- ## API Reference
579
+ ## Full API Reference
173
580
 
174
581
  ### `requestPermissionsAsync()`
175
582
 
@@ -177,12 +584,14 @@ export default function App() {
177
584
  requestPermissionsAsync(): Promise<boolean>
178
585
  ```
179
586
 
180
- Requests all permissions required for scanning and monitoring:
587
+ Requests all permissions required for scanning and monitoring.
181
588
 
182
- - **Android**: `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION`, `POST_NOTIFICATIONS` (API 33+)
183
- - **iOS**: `CLLocationManager` "Always" authorization
589
+ | Platform | Permissions Requested |
590
+ |---|---|
591
+ | **Android** | `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION`, `POST_NOTIFICATIONS` (API 33+) |
592
+ | **iOS** | `CLLocationManager` "When In Use" → "Always" authorization (two-step prompt) |
184
593
 
185
- Returns `true` if all permissions were granted, `false` otherwise.
594
+ **Returns**: `true` if all required permissions were granted.
186
595
 
187
596
  ```ts
188
597
  const granted = await ExpoBeacon.requestPermissionsAsync();
@@ -191,6 +600,8 @@ if (!granted) {
191
600
  }
192
601
  ```
193
602
 
603
+ > **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.
604
+
194
605
  ---
195
606
 
196
607
  ### `scanForBeaconsAsync(uuids?, scanDurationMs?)`
@@ -199,37 +610,64 @@ if (!granted) {
199
610
  scanForBeaconsAsync(uuids?: string[], scanDurationMs?: number): Promise<BeaconScanResult[]>
200
611
  ```
201
612
 
202
- Starts a **one-shot BLE scan**, waits for `scanDurationMs` milliseconds, then resolves with all beacons discovered during that window.
613
+ Performs a **one-shot iBeacon scan**. Waits for the specified duration, then resolves with all discovered beacons.
203
614
 
204
- | Parameter | Type | Default | Description |
205
- | ---------------- | ---------- | ------- | ---------------------------------------- |
206
- | `uuids` | `string[]` | `[]` | Proximity UUIDs to filter by. **iOS**: at least one UUID is required (or have paired beacons). **Android**: pass `[]` for a wildcard scan. |
207
- | `scanDurationMs` | `number` | `5000` | How long to scan in milliseconds (1–60 000 recommended) |
615
+ | Parameter | Type | Default | Description |
616
+ |---|---|---|---|
617
+ | `uuids` | `string[]` | `[]` | Proximity UUIDs to filter by. See platform differences below. |
618
+ | `scanDurationMs` | `number` | `5000` | Scan duration in milliseconds (must be > 0). |
208
619
 
209
- Returns an array of [`BeaconScanResult`](#beaconscanresult) objects. Rejects with `SCAN_IN_PROGRESS` if another scan is already running.
620
+ **Returns**: `BeaconScanResult[]` deduplicated by UUID + major + minor.
210
621
 
211
- **Platform differences**
212
-
213
- | | Empty UUIDs (`[]`) | Targeted (`['UUID-1', …]`) |
622
+ | Behaviour | Android | iOS |
214
623
  |---|---|---|
215
- | **Android** | Discovers all iBeacons via AltBeacon | Filters results to matching UUIDs |
216
- | **iOS** | Uses paired beacon UUIDs automatically. Rejects if no UUIDs and no paired beacons. | CoreLocation ranging (works in foreground & background) |
624
+ | Empty `uuids` (`[]`) | Wildcard — discovers all nearby iBeacons | Auto-uses paired beacon UUIDs. Rejects with `WILDCARD_NOT_SUPPORTED` if none are paired. |
625
+ | Targeted (`["UUID-1"]`) | Filters scan results to matching UUIDs | CoreLocation ranging for those UUIDs |
626
+
627
+ **Possible errors**:
217
628
 
218
- > **iOS limitation**: Apple strips iBeacon manufacturer data from CoreBluetooth BLE advertisements. Wildcard iBeacon scanning (no UUID filter) is not possible on iOS. When you pass an empty `uuids` array, the module automatically uses UUIDs from your paired beacons. If no beacons are paired, the call rejects with `WILDCARD_NOT_SUPPORTED`. For Eddystone scanning, use `scanForEddystonesAsync()` instead — it works without restrictions on both platforms.
629
+ | Code | Reason |
630
+ |---|---|
631
+ | `SCAN_IN_PROGRESS` | Another scan is already running |
632
+ | `INVALID_UUID` | One of the UUID strings is malformed |
633
+ | `INVALID_DURATION` | Duration ≤ 0 |
634
+ | `PERMISSION_DENIED` | Location permission not granted |
635
+ | `WILDCARD_NOT_SUPPORTED` | iOS: empty UUIDs with no paired beacons |
636
+ | `SCAN_CANCELLED` | `cancelScan()` was called |
219
637
 
220
638
  ```ts
221
- // Scan by UUID — works on both platforms
222
- const filtered = await ExpoBeacon.scanForBeaconsAsync(
223
- ['E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'],
639
+ const beacons = await ExpoBeacon.scanForBeaconsAsync(
640
+ ["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
224
641
  8000,
225
642
  );
643
+ ```
226
644
 
227
- // Wildcard scan — Android only. On iOS, uses paired beacon UUIDs.
228
- const all = await ExpoBeacon.scanForBeaconsAsync([], 5000);
645
+ ---
229
646
 
230
- filtered.forEach((b) =>
231
- console.log(`${b.uuid} major=${b.major} minor=${b.minor} dist=${b.distance.toFixed(1)}m rssi=${b.rssi}dBm`)
232
- );
647
+ ### `scanForEddystonesAsync(scanDurationMs?)`
648
+
649
+ ```ts
650
+ scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>
651
+ ```
652
+
653
+ Performs a **one-shot Eddystone scan** using BLE. Discovers both Eddystone-UID and Eddystone-URL frames.
654
+
655
+ | Parameter | Type | Default | Description |
656
+ |---|---|---|---|
657
+ | `scanDurationMs` | `number` | `5000` | Scan duration in milliseconds (must be > 0). |
658
+
659
+ **Returns**: `EddystoneScanResult[]` — deduplicated by namespace:instance (UID) or url (URL).
660
+
661
+ **Possible errors**:
662
+
663
+ | Code | Reason |
664
+ |---|---|
665
+ | `SCAN_IN_PROGRESS` | Another Eddystone scan is already running |
666
+ | `INVALID_DURATION` | Duration ≤ 0 |
667
+ | `SCAN_CANCELLED` | `cancelScan()` was called |
668
+
669
+ ```ts
670
+ const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
233
671
  ```
234
672
 
235
673
  ---
@@ -240,37 +678,33 @@ filtered.forEach((b) =>
240
678
  startContinuousScan(): void
241
679
  ```
242
680
 
243
- Begins a **continuous BLE scan** that fires an [`onBeaconFound`](#onbeaconfound) event every time a beacon advertisement is received. Call [`stopContinuousScan()`](#stopcontinuousscan) to end it.
681
+ Begins a **continuous BLE scan** that streams beacon discoveries via events:
682
+ - `onBeaconFound` — iBeacon advertisements
683
+ - `onEddystoneFound` — Eddystone advertisements
244
684
 
245
- Unlike `scanForBeaconsAsync`, this never resolvesit streams results in real time. Eddystone beacons are also reported via the [`onEddystoneFound`](#oneddystonefound) event.
685
+ Does not return results directlysubscribe to events before calling. Call `stopContinuousScan()` to end.
246
686
 
247
- **iOS note**: Due to CoreLocation API constraints, `startContinuousScan()` on iOS only ranges iBeacons that have been previously paired with `pairBeacon()`. On Android, all nearby BLE beacons are reported regardless of pairing status. Eddystone beacons are discovered on both platforms via CoreBluetooth / AltBeacon regardless of pairing.
687
+ > **iOS**: Only reports iBeacons whose UUID is registered via `pairBeacon()`. Eddystones are reported regardless of pairing.
248
688
 
249
- ```ts
250
- const sub = ExpoBeacon.addListener("onBeaconFound", (beacon) => {
251
- console.log("Live:", beacon.uuid, beacon.distance);
252
- });
253
- const eddySub = ExpoBeacon.addListener("onEddystoneFound", (beacon) => {
254
- console.log("Eddystone:", beacon.frameType, beacon.namespace ?? beacon.url);
255
- });
689
+ ---
256
690
 
257
- ExpoBeacon.startContinuousScan();
691
+ ### `stopContinuousScan()`
258
692
 
259
- // later, when done:
260
- ExpoBeacon.stopContinuousScan();
261
- sub.remove();
262
- eddySub.remove();
693
+ ```ts
694
+ stopContinuousScan(): void
263
695
  ```
264
696
 
697
+ Stops the continuous scan. No-op if no scan is running.
698
+
265
699
  ---
266
700
 
267
- ### `stopContinuousScan()`
701
+ ### `cancelScan()`
268
702
 
269
703
  ```ts
270
- stopContinuousScan(): void
704
+ cancelScan(): void
271
705
  ```
272
706
 
273
- Stops the continuous scan started by `startContinuousScan()`. No-op if no scan is running.
707
+ Cancels any in-progress one-shot scan (iBeacon or Eddystone). The pending promise rejects with code `SCAN_CANCELLED`.
274
708
 
275
709
  ---
276
710
 
@@ -280,22 +714,19 @@ Stops the continuous scan started by `startContinuousScan()`. No-op if no scan i
280
714
  pairBeacon(identifier: string, uuid: string, major: number, minor: number): void
281
715
  ```
282
716
 
283
- Registers a beacon for persistent region monitoring. Paired beacons survive app restarts (stored in `SharedPreferences` on Android, `UserDefaults` on iOS). Calling `pairBeacon` with an existing `identifier` replaces that entry.
717
+ Registers an iBeacon for persistent monitoring.
284
718
 
285
- | Parameter | Type | Description |
286
- | ------------ | -------- | ------------------------------------------------------------- |
287
- | `identifier` | `string` | Your unique label for this beacon (e.g. `"lobby-entrance"`) |
288
- | `uuid` | `string` | iBeacon proximity UUID (case-insensitive, standard format) |
289
- | `major` | `number` | iBeacon major value (0 – 65535) |
290
- | `minor` | `number` | iBeacon minor value (0 – 65535) |
719
+ | Parameter | Type | Description |
720
+ |---|---|---|
721
+ | `identifier` | `string` | Unique label (e.g. `"lobby-entrance"`). Re-using an identifier replaces the previous entry. |
722
+ | `uuid` | `string` | iBeacon proximity UUID (case-insensitive, e.g. `"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"`) |
723
+ | `major` | `number` | Major value: `0`–`65535` |
724
+ | `minor` | `number` | Minor value: `0`–`65535` |
725
+
726
+ **Possible errors**: `INVALID_UUID`, `INVALID_MAJOR`, `INVALID_MINOR`.
291
727
 
292
728
  ```ts
293
- ExpoBeacon.pairBeacon(
294
- "main-door",
295
- "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
296
- 1,
297
- 42,
298
- );
729
+ ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42);
299
730
  ```
300
731
 
301
732
  ---
@@ -306,11 +737,11 @@ ExpoBeacon.pairBeacon(
306
737
  unpairBeacon(identifier: string): void
307
738
  ```
308
739
 
309
- Removes a previously paired beacon. If monitoring is active, the region for this beacon stops being monitored immediately.
740
+ Removes a paired iBeacon. If monitoring is active, the region stops being tracked immediately.
310
741
 
311
- | Parameter | Type | Description |
312
- | ------------ | -------- | ---------------------------------- |
313
- | `identifier` | `string` | The label used when pairing |
742
+ | Parameter | Type | Description |
743
+ |---|---|---|
744
+ | `identifier` | `string` | The label used when pairing |
314
745
 
315
746
  ```ts
316
747
  ExpoBeacon.unpairBeacon("main-door");
@@ -324,40 +755,11 @@ ExpoBeacon.unpairBeacon("main-door");
324
755
  getPairedBeacons(): PairedBeacon[]
325
756
  ```
326
757
 
327
- Returns the list of all currently paired beacons from persistent storage.
758
+ Returns all currently paired iBeacons from persistent storage.
328
759
 
329
760
  ```ts
330
761
  const paired = ExpoBeacon.getPairedBeacons();
331
- paired.forEach((b) =>
332
- console.log(b.identifier, b.uuid, b.major, b.minor)
333
- );
334
- ```
335
-
336
- ---
337
-
338
- ### `scanForEddystonesAsync(scanDurationMs?)`
339
-
340
- ```ts
341
- scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>
342
- ```
343
-
344
- Starts a **one-shot BLE scan** for Eddystone beacons, waits for `scanDurationMs` milliseconds, then resolves with all Eddystone beacons discovered.
345
-
346
- | Parameter | Type | Default | Description |
347
- | ---------------- | -------- | ------- | ---------------------------------------------- |
348
- | `scanDurationMs` | `number` | `5000` | How long to scan in milliseconds |
349
-
350
- Returns an array of [`EddystoneScanResult`](#eddystonescanresult) objects. Discovers both Eddystone-UID and Eddystone-URL frames.
351
-
352
- ```ts
353
- const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
354
- eddystones.forEach((b) => {
355
- if (b.frameType === "uid") {
356
- console.log(`UID: ns=${b.namespace} inst=${b.instance} dist=${b.distance.toFixed(1)}m`);
357
- } else {
358
- console.log(`URL: ${b.url} dist=${b.distance.toFixed(1)}m`);
359
- }
360
- });
762
+ // [{ identifier: "main-door", uuid: "E2C5…", major: 1, minor: 42 }]
361
763
  ```
362
764
 
363
765
  ---
@@ -368,20 +770,18 @@ eddystones.forEach((b) => {
368
770
  pairEddystone(identifier: string, namespace: string, instance: string): void
369
771
  ```
370
772
 
371
- Registers an Eddystone-UID beacon for persistent region monitoring. Paired Eddystones survive app restarts (stored in `SharedPreferences` on Android, `UserDefaults` on iOS). Calling `pairEddystone` with an existing `identifier` replaces that entry.
773
+ Registers an Eddystone-UID beacon for persistent monitoring.
774
+
775
+ | Parameter | Type | Description |
776
+ |---|---|---|
777
+ | `identifier` | `string` | Unique label (e.g. `"meeting-room"`) |
778
+ | `namespace` | `string` | 10-byte namespace ID as hex string — must be exactly **20 hex characters** |
779
+ | `instance` | `string` | 6-byte instance ID as hex string — must be exactly **12 hex characters** |
372
780
 
373
- | Parameter | Type | Description |
374
- | ------------ | -------- | ---------------------------------------------------------------- |
375
- | `identifier` | `string` | Your unique label for this beacon (e.g. `"meeting-room"`) |
376
- | `namespace` | `string` | 10-byte namespace ID as a hex string (20 characters) |
377
- | `instance` | `string` | 6-byte instance ID as a hex string (12 characters) |
781
+ **Possible errors**: `INVALID_NAMESPACE`, `INVALID_INSTANCE`.
378
782
 
379
783
  ```ts
380
- ExpoBeacon.pairEddystone(
381
- "meeting-room",
382
- "edd1ebeac04e5defa017",
383
- "0123456789ab",
384
- );
784
+ ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab");
385
785
  ```
386
786
 
387
787
  ---
@@ -392,11 +792,11 @@ ExpoBeacon.pairEddystone(
392
792
  unpairEddystone(identifier: string): void
393
793
  ```
394
794
 
395
- Removes a previously paired Eddystone beacon.
795
+ Removes a paired Eddystone beacon.
396
796
 
397
- | Parameter | Type | Description |
398
- | ------------ | -------- | ---------------------------------- |
399
- | `identifier` | `string` | The label used when pairing |
797
+ | Parameter | Type | Description |
798
+ |---|---|---|
799
+ | `identifier` | `string` | The label used when pairing |
400
800
 
401
801
  ```ts
402
802
  ExpoBeacon.unpairEddystone("meeting-room");
@@ -410,13 +810,11 @@ ExpoBeacon.unpairEddystone("meeting-room");
410
810
  getPairedEddystones(): PairedEddystone[]
411
811
  ```
412
812
 
413
- Returns the list of all currently paired Eddystone beacons from persistent storage.
813
+ Returns all currently paired Eddystone beacons from persistent storage.
414
814
 
415
815
  ```ts
416
816
  const paired = ExpoBeacon.getPairedEddystones();
417
- paired.forEach((e) =>
418
- console.log(e.identifier, e.namespace, e.instance)
419
- );
817
+ // [{ identifier: "meeting-room", namespace: "edd1…", instance: "0123…" }]
420
818
  ```
421
819
 
422
820
  ---
@@ -427,43 +825,47 @@ paired.forEach((e) =>
427
825
  startMonitoring(options?: MonitoringOptions | number): Promise<void>
428
826
  ```
429
827
 
430
- Starts background region monitoring for all paired beacons (both iBeacons and Eddystones).
828
+ Starts background region monitoring for **all paired beacons** (iBeacon + Eddystone).
431
829
 
432
- Accepts either a `MonitoringOptions` object or a plain `number` (backward-compatible shorthand for `maxDistance`).
830
+ Accepts a `MonitoringOptions` object, a plain `number` (shorthand for `maxDistance`), or nothing.
433
831
 
434
- **`MonitoringOptions`**
832
+ | Property | Type | Default | Description |
833
+ |---|---|---|---|
834
+ | `maxDistance` | `number` | `undefined` | Distance threshold in metres. `onBeaconEnter` / `onEddystoneEnter` only fires when measured distance ≤ this value. `onBeaconExit` / `onEddystoneExit` always fires. Omit to disable filtering. |
835
+ | `notifications` | `NotificationConfig` | `undefined` | Notification overrides for this session (persisted). |
435
836
 
436
- | Property | Type | Default | Description |
437
- | --------------- | -------------------- | ----------- | --------------------------------------------------------------------------------------------- |
438
- | `maxDistance` | `number` | `undefined` | Optional distance threshold in metres. `onBeaconEnter` is only emitted when the measured distance is ≤ this value. `onBeaconExit` is always emitted. Omit to disable distance filtering. |
439
- | `notifications` | `NotificationConfig` | `undefined` | Notification config overrides applied for this session. Persisted and takes effect immediately. |
837
+ **What happens on each platform**:
440
838
 
441
- **Android**: Launches `BeaconForegroundService` a persistent foreground service required by Android 8+ for background BLE. Restarts automatically after device reboot.
839
+ | Platform | Mechanism |
840
+ |---|---|
841
+ | **Android** | Starts `BeaconForegroundService` (persistent notification). Survives app backgrounding. Auto-restarts after device reboot via `BootReceiver`. Scan timing: 1.1 s every 5 s. |
842
+ | **iOS** | Activates `CLLocationManager` region monitoring (iBeacon) + CoreBluetooth BLE scanning (Eddystone). iOS can wake/relaunch the app on region boundary crossings, even if force-quit. |
442
843
 
443
- **iOS**: Activates `CLLocationManager` region monitoring. iOS can wake or relaunch the app when a region boundary is crossed, even if the app is terminated.
844
+ **Possible errors**: `PERMISSION_DENIED` (Always authorization required on iOS).
444
845
 
445
846
  ```ts
446
- // Backward-compatible shorthand (number = maxDistance)
847
+ // Shorthand just a distance threshold
447
848
  await ExpoBeacon.startMonitoring(5);
448
849
 
449
- // Full options object
850
+ // Full options
450
851
  await ExpoBeacon.startMonitoring({
451
- maxDistance: 5,
852
+ maxDistance: 10,
452
853
  notifications: {
453
854
  beaconEvents: {
454
- enterTitle: "Beacon nearby!",
455
- body: "{identifier} is within range",
855
+ enterTitle: "Welcome!",
856
+ body: "{identifier} is nearby",
456
857
  },
457
858
  },
458
859
  });
459
860
 
460
- // Monitor with no distance limit and no enter/exit notifications
861
+ // No distance filter, silent
461
862
  await ExpoBeacon.startMonitoring({
462
863
  notifications: { beaconEvents: { enabled: false } },
463
864
  });
464
- ```
465
865
 
466
- > Call `requestPermissionsAsync()` before `startMonitoring()`.
866
+ // No options at all — monitor all paired beacons, no distance filter, default notifications
867
+ await ExpoBeacon.startMonitoring();
868
+ ```
467
869
 
468
870
  ---
469
871
 
@@ -473,7 +875,7 @@ await ExpoBeacon.startMonitoring({
473
875
  stopMonitoring(): Promise<void>
474
876
  ```
475
877
 
476
- Stops background monitoring and removes all active region subscriptions. On Android, stops the foreground service.
878
+ Stops all background monitoring. On Android, stops the foreground service.
477
879
 
478
880
  ```ts
479
881
  await ExpoBeacon.stopMonitoring();
@@ -487,73 +889,81 @@ await ExpoBeacon.stopMonitoring();
487
889
  setNotificationConfig(config: NotificationConfig): void
488
890
  ```
489
891
 
490
- Persists notification configuration that is applied to all subsequent monitoring sessions. Values are stored in `SharedPreferences` (Android) / `UserDefaults` (iOS) and survive app restarts.
892
+ Persists notification configuration applied to **all subsequent monitoring sessions**. Survives app restarts.
893
+
894
+ For one-off overrides, pass `notifications` inside `startMonitoring(options)` instead.
491
895
 
492
- For one-off overrides tied to a single session, pass `notifications` directly in [`startMonitoring(options)`](#startmonitoringoptions) instead it calls this function automatically.
896
+ See [`NotificationConfig`](#notificationconfig) for the full shape.
493
897
 
494
898
  ```ts
495
899
  ExpoBeacon.setNotificationConfig({
496
- // Enter/exit alert notifications
497
900
  beaconEvents: {
498
- enabled: true, // false to suppress all enter/exit notifications
499
- enterTitle: "Beacon nearby",
500
- exitTitle: "Beacon out of range",
501
- body: "{identifier} {event}ed", // {identifier} and {event} are replaced at runtime
502
- sound: true, // iOS only — default true
503
- icon: "ic_beacon_notification", // Android only — drawable resource name
901
+ enabled: true,
902
+ enterTitle: "Nearby",
903
+ exitTitle: "Gone",
904
+ body: "{identifier} {event}ed",
905
+ sound: true,
906
+ icon: "ic_notification",
504
907
  },
505
-
506
- // Persistent status-bar notification while monitoring is active (Android only)
507
908
  foregroundService: {
508
- title: "My App is watching",
509
- text: "Monitoring for nearby beacons",
510
- icon: "ic_service", // Android drawable resource name
909
+ title: "Monitoring Active",
910
+ text: "Scanning for beacons",
911
+ icon: "ic_service",
511
912
  },
512
-
513
- // Android notification channel settings
514
913
  channel: {
515
- name: "Proximity Alerts",
516
- description: "Alerts when beacons enter or leave range",
517
- importance: "default", // "low" | "default" | "high"
914
+ name: "Beacons",
915
+ description: "Beacon proximity alerts",
916
+ importance: "default",
518
917
  },
519
918
  });
520
919
  ```
521
920
 
522
- > **Android channel importance note**: Android prevents decreasing channel importance once a user has been notified. Increasing importance always works; decreasing it will have no effect until the user clears the app's notification settings or reinstalls the app.
523
-
524
921
  ---
525
922
 
526
923
  ## Events
527
924
 
528
- Subscribe to events using `ExpoBeacon.addListener(eventName, handler)`. Always call `.remove()` on the subscription when your component unmounts.
925
+ Subscribe with `ExpoBeacon.addListener(eventName, handler)`. Always call `.remove()` on the returned subscription during cleanup.
529
926
 
530
927
  ```ts
531
928
  const sub = ExpoBeacon.addListener("onBeaconEnter", handler);
532
- // cleanup:
929
+ // Later:
533
930
  sub.remove();
534
931
  ```
535
932
 
536
- ---
933
+ ### Event Summary
537
934
 
538
- ### `onBeaconEnter`
935
+ | Event | Trigger | Payload Type |
936
+ |---|---|---|
937
+ | `onBeaconEnter` | Paired iBeacon enters range (respects `maxDistance`) | `BeaconRegionEvent` |
938
+ | `onBeaconExit` | Paired iBeacon leaves range (always fires) | `BeaconRegionEvent` |
939
+ | `onBeaconDistance` | Periodic distance update during monitoring (~1/sec) | `BeaconDistanceEvent` |
940
+ | `onBeaconFound` | iBeacon detected during continuous scan | `BeaconScanResult` |
941
+ | `onEddystoneFound` | Eddystone detected during continuous scan | `EddystoneScanResult` |
942
+ | `onEddystoneEnter` | Paired Eddystone enters range (respects `maxDistance`) | `EddystoneRegionEvent` |
943
+ | `onEddystoneExit` | Paired Eddystone leaves range (always fires) | `EddystoneRegionEvent` |
944
+ | `onEddystoneDistance` | Periodic Eddystone distance update during monitoring | `EddystoneDistanceEvent` |
539
945
 
540
- Fired when the device enters the region of a paired (monitored) beacon. If `maxDistance` was set in `startMonitoring`, this only fires when the measured distance at the time of entry is within that threshold.
946
+ ### Event Detail
541
947
 
542
- **Payload**: [`BeaconRegionEvent`](#beaconregionevent)
948
+ #### `onBeaconEnter`
949
+
950
+ 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.
543
951
 
544
952
  ```ts
545
953
  ExpoBeacon.addListener("onBeaconEnter", (e) => {
546
- console.log(`Entered "${e.identifier}" (${e.uuid}) at ~${e.distance.toFixed(1)} m`);
954
+ // e.identifier "lobby-entrance"
955
+ // e.uuid — "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
956
+ // e.major — 1
957
+ // e.minor — 100
958
+ // e.event — "enter"
959
+ // e.distance — 3.2 (metres, or –1 if unavailable)
960
+ console.log(`Entered "${e.identifier}" at ~${e.distance.toFixed(1)}m`);
547
961
  });
548
962
  ```
549
963
 
550
- ---
551
-
552
- ### `onBeaconExit`
553
-
554
- Fired when the device leaves the region of a monitored beacon. Always fired regardless of distance filtering.
964
+ #### `onBeaconExit`
555
965
 
556
- **Payload**: [`BeaconRegionEvent`](#beaconregionevent)
966
+ Fired when the device leaves the region. **Always fires** regardless of `maxDistance` setting.
557
967
 
558
968
  ```ts
559
969
  ExpoBeacon.addListener("onBeaconExit", (e) => {
@@ -561,97 +971,68 @@ ExpoBeacon.addListener("onBeaconExit", (e) => {
561
971
  });
562
972
  ```
563
973
 
564
- ---
565
-
566
- ### `onBeaconRanging`
567
-
568
- > **Not currently emitted.** This event is declared in the TypeScript types ([`BeaconRangingEvent`](#beaconrangingevent)) but is not fired by either the iOS or Android native implementation. Use [`onBeaconDistance`](#onbeacondistance) for periodic distance updates during monitoring.
569
-
570
- ---
571
-
572
- ### `onBeaconDistance`
573
-
574
- Fired continuously during monitoring whenever a distance update is received for a paired beacon. Useful for real-time proximity UI.
974
+ #### `onBeaconDistance`
575
975
 
576
- **Payload**: [`BeaconDistanceEvent`](#beacondistanceevent)
976
+ Fired continuously during monitoring with the latest distance reading. Useful for proximity-based UI.
577
977
 
578
978
  ```ts
579
979
  ExpoBeacon.addListener("onBeaconDistance", (e) => {
580
- setProximity(e.distance);
980
+ // e.identifier, e.uuid, e.major, e.minor, e.distance
981
+ updateProximityBar(e.identifier, e.distance);
581
982
  });
582
983
  ```
583
984
 
584
- ---
585
-
586
- ### `onBeaconFound`
587
-
588
- Fired during a **continuous scan** (started with `startContinuousScan()`) each time an iBeacon advertisement is received.
985
+ #### `onBeaconFound`
589
986
 
590
- **Payload**: [`BeaconScanResult`](#beaconscanresult)
987
+ Fired during `startContinuousScan()` each time an iBeacon advertisement is received.
591
988
 
592
989
  ```ts
593
990
  ExpoBeacon.addListener("onBeaconFound", (b) => {
594
- console.log(`Found ${b.uuid} ${b.major}/${b.minor} at ${b.distance.toFixed(1)} m`);
991
+ console.log(`${b.uuid} ${b.major}/${b.minor} ${b.distance.toFixed(1)}m RSSI: ${b.rssi}`);
595
992
  });
596
993
  ```
597
994
 
598
- ---
599
-
600
- ### `onEddystoneFound`
601
-
602
- Fired during a **continuous scan** (started with `startContinuousScan()`) each time an Eddystone advertisement is received.
995
+ #### `onEddystoneFound`
603
996
 
604
- **Payload**: [`EddystoneScanResult`](#eddystonescanresult)
997
+ Fired during `startContinuousScan()` each time an Eddystone advertisement is received.
605
998
 
606
999
  ```ts
607
1000
  ExpoBeacon.addListener("onEddystoneFound", (b) => {
608
1001
  if (b.frameType === "uid") {
609
- console.log(`Eddystone-UID: ${b.namespace}/${b.instance} at ${b.distance.toFixed(1)} m`);
1002
+ console.log(`UID: ${b.namespace}/${b.instance} ${b.distance.toFixed(1)}m`);
610
1003
  } else {
611
- console.log(`Eddystone-URL: ${b.url}`);
1004
+ console.log(`URL: ${b.url} — ${b.distance.toFixed(1)}m`);
612
1005
  }
613
1006
  });
614
1007
  ```
615
1008
 
616
- ---
617
-
618
- ### `onEddystoneEnter`
619
-
620
- Fired when a paired Eddystone-UID beacon enters range during monitoring. If `maxDistance` was set in `startMonitoring`, this only fires when the measured distance is within that threshold.
1009
+ #### `onEddystoneEnter`
621
1010
 
622
- **Payload**: [`EddystoneRegionEvent`](#eddystoneregionevent)
1011
+ Fired when a paired Eddystone-UID beacon enters range during monitoring.
623
1012
 
624
1013
  ```ts
625
1014
  ExpoBeacon.addListener("onEddystoneEnter", (e) => {
626
- console.log(`Eddystone entered "${e.identifier}" (ns: ${e.namespace}) at ~${e.distance.toFixed(1)} m`);
1015
+ console.log(`Eddystone "${e.identifier}" entered (ns: ${e.namespace})`);
627
1016
  });
628
1017
  ```
629
1018
 
630
- ---
631
-
632
- ### `onEddystoneExit`
633
-
634
- Fired when a paired Eddystone-UID beacon leaves range during monitoring.
1019
+ #### `onEddystoneExit`
635
1020
 
636
- **Payload**: [`EddystoneRegionEvent`](#eddystoneregionevent)
1021
+ Fired when a paired Eddystone-UID beacon leaves range.
637
1022
 
638
1023
  ```ts
639
1024
  ExpoBeacon.addListener("onEddystoneExit", (e) => {
640
- console.log(`Eddystone left "${e.identifier}"`);
1025
+ console.log(`Eddystone "${e.identifier}" exited`);
641
1026
  });
642
1027
  ```
643
1028
 
644
- ---
645
-
646
- ### `onEddystoneDistance`
647
-
648
- Fired continuously during monitoring whenever a distance update is received for a paired Eddystone beacon (~1 update/sec).
1029
+ #### `onEddystoneDistance`
649
1030
 
650
- **Payload**: [`EddystoneDistanceEvent`](#eddystonedistanceevent)
1031
+ Fired continuously during monitoring with the latest Eddystone distance reading.
651
1032
 
652
1033
  ```ts
653
1034
  ExpoBeacon.addListener("onEddystoneDistance", (e) => {
654
- console.log(`Eddystone ${e.identifier}: ${e.distance.toFixed(2)} m`);
1035
+ console.log(`Eddystone "${e.identifier}" ${e.distance.toFixed(2)}m`);
655
1036
  });
656
1037
  ```
657
1038
 
@@ -659,24 +1040,46 @@ ExpoBeacon.addListener("onEddystoneDistance", (e) => {
659
1040
 
660
1041
  ## TypeScript Types
661
1042
 
1043
+ All types are exported from the package:
1044
+
1045
+ ```ts
1046
+ import type {
1047
+ BeaconScanResult,
1048
+ PairedBeacon,
1049
+ BeaconRegionEvent,
1050
+ BeaconDistanceEvent,
1051
+ EddystoneFrameType,
1052
+ EddystoneScanResult,
1053
+ PairedEddystone,
1054
+ EddystoneRegionEvent,
1055
+ EddystoneDistanceEvent,
1056
+ ExpoBeaconModuleEvents,
1057
+ MonitoringOptions,
1058
+ NotificationConfig,
1059
+ BeaconNotificationConfig,
1060
+ ForegroundServiceConfig,
1061
+ NotificationChannelConfig,
1062
+ } from "expo-beacon";
1063
+ ```
1064
+
662
1065
  ### `BeaconScanResult`
663
1066
 
664
- Returned by `scanForBeaconsAsync` and used in `onBeaconFound` events.
1067
+ Returned by `scanForBeaconsAsync()` and `onBeaconFound`.
665
1068
 
666
1069
  ```ts
667
1070
  type BeaconScanResult = {
668
- uuid: string; // iBeacon proximity UUID (uppercase, formatted)
669
- major: number; // iBeacon major value (0–65535)
670
- minor: number; // iBeacon minor value (0–65535)
671
- rssi: number; // Signal strength in dBm (negative integer, e.g. –65)
672
- distance: number; // Estimated distance in metres (calculated from RSSI + txPower)
673
- txPower: number; // Calibrated TX power from the beacon advertisement
1071
+ uuid: string; // Proximity UUID, uppercase (e.g. "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0")
1072
+ major: number; // 0–65535
1073
+ minor: number; // 0–65535
1074
+ rssi: number; // Signal strength in dBm (negative, e.g. –65)
1075
+ distance: number; // Estimated distance in metres
1076
+ txPower: number; // Calibrated TX power from the advertisement
674
1077
  };
675
1078
  ```
676
1079
 
677
1080
  ### `PairedBeacon`
678
1081
 
679
- Returned by `getPairedBeacons`.
1082
+ Returned by `getPairedBeacons()`.
680
1083
 
681
1084
  ```ts
682
1085
  type PairedBeacon = {
@@ -689,7 +1092,7 @@ type PairedBeacon = {
689
1092
 
690
1093
  ### `BeaconRegionEvent`
691
1094
 
692
- Payload for `onBeaconEnter` and `onBeaconExit`.
1095
+ Payload for `onBeaconEnter` / `onBeaconExit`.
693
1096
 
694
1097
  ```ts
695
1098
  type BeaconRegionEvent = {
@@ -698,22 +1101,7 @@ type BeaconRegionEvent = {
698
1101
  major: number;
699
1102
  minor: number;
700
1103
  event: "enter" | "exit";
701
- distance: number; // Measured distance in metres at event time; –1 if unavailable
702
- };
703
- ```
704
-
705
- ### `BeaconRangingEvent`
706
-
707
- Payload type for the `onBeaconRanging` event. **Declared for future use — this event is not currently emitted by either platform.** Use [`BeaconDistanceEvent`](#beacondistanceevent) and [`onBeaconDistance`](#onbeacondistance) for real-time distance updates.
708
-
709
- ```ts
710
- type BeaconRangingEvent = {
711
- identifier: string;
712
- uuid: string;
713
- major: number;
714
- minor: number;
715
- rssi: number; // Signal strength in dBm
716
- distance: number; // Estimated distance in metres
1104
+ distance: number; // Metres at event time; –1 if unavailable
717
1105
  };
718
1106
  ```
719
1107
 
@@ -733,43 +1121,43 @@ type BeaconDistanceEvent = {
733
1121
 
734
1122
  ### `EddystoneScanResult`
735
1123
 
736
- Returned by `scanForEddystonesAsync` and used in `onEddystoneFound` events.
1124
+ Returned by `scanForEddystonesAsync()` and `onEddystoneFound`.
737
1125
 
738
1126
  ```ts
739
1127
  type EddystoneScanResult = {
740
1128
  frameType: "uid" | "url";
741
- namespace?: string; // 10-byte hex string (20 chars). Present for UID frames.
742
- instance?: string; // 6-byte hex string (12 chars). Present for UID frames.
1129
+ namespace?: string; // 20 hex chars. Present for UID frames.
1130
+ instance?: string; // 12 hex chars. Present for UID frames.
743
1131
  url?: string; // Decoded URL. Present for URL frames.
744
- rssi: number; // Signal strength in dBm
745
- distance: number; // Estimated distance in metres
746
- txPower: number; // Calibrated TX power
1132
+ rssi: number;
1133
+ distance: number;
1134
+ txPower: number;
747
1135
  };
748
1136
  ```
749
1137
 
750
1138
  ### `PairedEddystone`
751
1139
 
752
- Returned by `getPairedEddystones`.
1140
+ Returned by `getPairedEddystones()`.
753
1141
 
754
1142
  ```ts
755
1143
  type PairedEddystone = {
756
- identifier: string; // Your label
757
- namespace: string; // 10-byte hex string (20 chars)
758
- instance: string; // 6-byte hex string (12 chars)
1144
+ identifier: string;
1145
+ namespace: string; // 20 hex chars
1146
+ instance: string; // 12 hex chars
759
1147
  };
760
1148
  ```
761
1149
 
762
1150
  ### `EddystoneRegionEvent`
763
1151
 
764
- Payload for `onEddystoneEnter` and `onEddystoneExit`.
1152
+ Payload for `onEddystoneEnter` / `onEddystoneExit`.
765
1153
 
766
1154
  ```ts
767
1155
  type EddystoneRegionEvent = {
768
- identifier: string; // Matches PairedEddystone.identifier
1156
+ identifier: string;
769
1157
  namespace: string;
770
1158
  instance: string;
771
1159
  event: "enter" | "exit";
772
- distance: number; // Measured distance in metres at event time; –1 if unavailable
1160
+ distance: number; // Metres; –1 if unavailable
773
1161
  };
774
1162
  ```
775
1163
 
@@ -782,55 +1170,49 @@ type EddystoneDistanceEvent = {
782
1170
  identifier: string;
783
1171
  namespace: string;
784
1172
  instance: string;
785
- distance: number; // Estimated distance in metres
1173
+ distance: number;
786
1174
  };
787
1175
  ```
788
1176
 
789
- ---
790
-
791
1177
  ### `MonitoringOptions`
792
1178
 
793
- Passed to `startMonitoring(options)`.
1179
+ Passed to `startMonitoring()`.
794
1180
 
795
1181
  ```ts
796
1182
  type MonitoringOptions = {
797
- maxDistance?: number; // Distance threshold in metres for enter events
798
- notifications?: NotificationConfig; // Notification overrides for this session
1183
+ maxDistance?: number;
1184
+ notifications?: NotificationConfig;
799
1185
  };
800
1186
  ```
801
1187
 
802
1188
  ### `NotificationConfig`
803
1189
 
804
- Top-level config object accepted by `setNotificationConfig()` and `startMonitoring({ notifications })`.
1190
+ Top-level notification configuration.
805
1191
 
806
1192
  ```ts
807
1193
  type NotificationConfig = {
808
- beaconEvents?: BeaconNotificationConfig;
809
- foregroundService?: ForegroundServiceConfig; // Android only
810
- channel?: NotificationChannelConfig; // Android only
1194
+ beaconEvents?: BeaconNotificationConfig; // Enter/exit alerts
1195
+ foregroundService?: ForegroundServiceConfig; // Android only — persistent status bar
1196
+ channel?: NotificationChannelConfig; // Android only — channel settings
811
1197
  };
812
1198
  ```
813
1199
 
814
1200
  ### `BeaconNotificationConfig`
815
1201
 
816
- Controls the enter/exit alert notifications.
817
-
818
1202
  ```ts
819
1203
  type BeaconNotificationConfig = {
820
- enabled?: boolean; // false to disable all enter/exit notifications. Default: true
1204
+ enabled?: boolean; // Default: true. Set false to suppress.
821
1205
  enterTitle?: string; // Default: "Beacon Entered"
822
1206
  exitTitle?: string; // Default: "Beacon Exited"
823
- body?: string; // Template {identifier} and {event} are replaced at runtime
824
- // Default: "{identifier} region {event}ed"
825
- sound?: boolean; // iOS only — play notification sound. Default: true
826
- icon?: string; // Android only drawable resource name (e.g. "ic_notification")
1207
+ body?: string; // Default: "{identifier} region {event}ed"
1208
+ // Supports {identifier} and {event} placeholders.
1209
+ sound?: boolean; // iOS only. Default: true
1210
+ icon?: string; // Android only. Drawable resource name.
827
1211
  };
828
1212
  ```
829
1213
 
830
1214
  ### `ForegroundServiceConfig`
831
1215
 
832
- Controls the persistent Android status-bar notification while monitoring is active.
833
-
834
1216
  ```ts
835
1217
  type ForegroundServiceConfig = {
836
1218
  title?: string; // Default: "Beacon Monitoring Active"
@@ -841,12 +1223,10 @@ type ForegroundServiceConfig = {
841
1223
 
842
1224
  ### `NotificationChannelConfig`
843
1225
 
844
- Controls the Android notification channel shown in system settings.
845
-
846
1226
  ```ts
847
1227
  type NotificationChannelConfig = {
848
- name?: string; // Default: "Beacon Monitoring"
849
- description?: string; // Default: "Used for background iBeacon region monitoring"
1228
+ name?: string; // Default: "Beacon Monitoring"
1229
+ description?: string; // Default: "Used for background iBeacon region monitoring"
850
1230
  importance?: "low" | "default" | "high"; // Default: "low"
851
1231
  };
852
1232
  ```
@@ -857,46 +1237,142 @@ type NotificationChannelConfig = {
857
1237
 
858
1238
  ### Android
859
1239
 
860
- `startMonitoring()` launches a **foreground service** (`BeaconForegroundService`) with a persistent notification titled _"Beacon Monitoring Active"_. This is required by Android 8+ (Oreo) to keep BLE scanning alive when the app is backgrounded. The service is automatically restarted after device reboot if monitoring was active at shutdown (via `BootReceiver`).
1240
+ `startMonitoring()` launches a **foreground service** (`BeaconForegroundService`) with a persistent notification. This is required by Android 8+ (Oreo) to keep BLE scanning alive in the background.
861
1241
 
862
- Default scan timing: 1.1 s scan window every 5 s.
1242
+ | Behaviour | Detail |
1243
+ |---|---|
1244
+ | Foreground service | Required for background BLE on Android 8+. Shows persistent notification. |
1245
+ | Reboot survival | `BootReceiver` auto-restarts monitoring after device reboot. |
1246
+ | Scan timing | 1.1 s scan window every 5 s (AltBeacon default). |
1247
+ | Battery | Low impact due to duty-cycled scanning. |
863
1248
 
864
1249
  ### iOS
865
1250
 
866
- `startMonitoring()` activates `CLLocationManager` **region monitoring** for paired iBeacons. iOS can wake or relaunch the app when the device crosses a region boundary, even if the app has been force-quit. `allowsBackgroundLocationUpdates` is `true` and `pausesLocationUpdatesAutomatically` is `false`.
1251
+ `startMonitoring()` activates `CLLocationManager` region monitoring for iBeacons and CoreBluetooth BLE scanning for Eddystones.
867
1252
 
868
- For paired Eddystone beacons, iOS uses **CoreBluetooth BLE scanning** during monitoring. BLE scanning works reliably in the foreground and while the app is backgrounded with `Uses Bluetooth LE accessories` background mode enabled. However, BLE scanning may be throttled or stopped by iOS when the app is suspended.
869
-
870
- > iOS limits apps to **20 simultaneously monitored regions** (iBeacon onlyEddystone monitoring does not count toward this limit).
1253
+ | Behaviour | Detail |
1254
+ |---|---|
1255
+ | Region monitoring | iOS wakes/relaunches the app on region boundary crossings even if force-quit. |
1256
+ | BLE scanning | Eddystones are monitored via CoreBluetooth. Works reliably in foreground; may be throttled when the app is suspended. |
1257
+ | Background modes | `allowsBackgroundLocationUpdates = true`, `pausesLocationUpdatesAutomatically = false` |
1258
+ | Region limit | 20 simultaneous `CLBeaconRegion` registrations max. Eddystones don't count. |
871
1259
 
872
1260
  ---
873
1261
 
874
1262
  ## Notifications
875
1263
 
876
- A local notification is posted for every beacon enter and exit event (both iBeacon and Eddystone). All notification settings can be customised via [`setNotificationConfig()`](#setnotificationconfigconfig) or inline in [`startMonitoring(options)`](#startmonitoringoptions).
1264
+ A local notification is posted automatically for every beacon enter/exit event (both iBeacon and Eddystone) during monitoring.
1265
+
1266
+ ### Default Values
1267
+
1268
+ | Property | Default |
1269
+ |---|---|
1270
+ | Enter title | `"Beacon Entered"` |
1271
+ | Exit title | `"Beacon Exited"` |
1272
+ | Body | `"{identifier} region {event}ed"` |
1273
+ | Sound (iOS) | `true` |
1274
+ | Icon (Android) | System `ic_dialog_info` |
1275
+ | Foreground service title | `"Beacon Monitoring Active"` |
1276
+ | Foreground service text | `"Monitoring for iBeacons in the background"` |
1277
+ | Channel name (Android) | `"Beacon Monitoring"` |
1278
+ | Channel importance (Android) | `"low"` |
1279
+
1280
+ ### Android Channel
1281
+
1282
+ Both the foreground service and enter/exit alerts share the channel ID `expo_beacon_channel`. The channel is recreated on each `onStartCommand`, so config changes take effect on the next monitoring start.
1283
+
1284
+ > **Android channel importance note**: Android prevents decreasing channel importance after the first notification. Increasing works; decreasing has no effect until the user clears notification settings or reinstalls the app.
1285
+
1286
+ ---
1287
+
1288
+ ## Platform-Specific Notes & Gotchas
1289
+
1290
+ ### iOS Native Insights (CoreLocation + CoreBluetooth)
1291
+
1292
+ 1. **iBeacon scanning requires UUIDs**: Apple's CoreBluetooth strips iBeacon manufacturer data from BLE advertisements. The module uses `CLLocationManager` ranging with `CLBeaconIdentityConstraint`, which requires known UUIDs. Wildcard iBeacon discovery is architecturally impossible on iOS.
1293
+
1294
+ 2. **Two-step location permission**: iOS requires requesting "When In Use" first, then upgrading to "Always". The module handles this automatically via a two-step flow in `requestPermissionsAsync()`.
1295
+
1296
+ 3. **20 region limit**: `CLLocationManager` enforces a hard limit of 20 monitored `CLBeaconRegion` regions across all apps. If your app pairs more than 20 iBeacons, only the first 20 will be actively monitored. Plan your beacon deployment accordingly.
1297
+
1298
+ 4. **Region monitoring vs. ranging**: Region monitoring (enter/exit) works indefinitely in the background. Ranging (distance updates) requires the app to be in the foreground or have an active background task. The module keeps ranging alive when background location mode is enabled.
1299
+
1300
+ 5. **Eddystone background limitations**: Eddystone monitoring uses CoreBluetooth, which iOS throttles in the background (longer scan intervals, delayed discovery). For critical Eddystone use cases, consider using significant location changes to periodically wake the app.
877
1301
 
878
- ### Defaults
1302
+ 6. **Hysteresis**: The module requires 3 consecutive readings inside/outside the distance threshold before emitting enter/exit events. This prevents jitter from RSSI fluctuations.
879
1303
 
880
- | Property | Default value |
881
- | ------------------------------ | ------------------------------------------------ |
882
- | Enter title | `"Beacon Entered"` |
883
- | Exit title | `"Beacon Exited"` |
884
- | Body | `"{identifier} region {event}ed"` |
885
- | Sound (iOS) | `true` |
886
- | Icon (Android) | System `ic_dialog_info` |
887
- | Foreground service title | `"Beacon Monitoring Active"` |
888
- | Foreground service text | `"Monitoring for iBeacons in the background"` |
889
- | Channel name (Android) | `"Beacon Monitoring"` |
890
- | Channel importance (Android) | `"low"` |
1304
+ ### Android Native Insights (AltBeacon + Foreground Service)
891
1305
 
892
- ### Channel IDs (Android)
1306
+ 1. **Foreground service is mandatory**: Android 8+ kills background BLE scans. The module uses `BeaconForegroundService` with a persistent notification. Users will see this notification while monitoring is active.
893
1307
 
894
- | Channel / type | Importance |
895
- | ------------------------------- | ------------------- |
896
- | Foreground service (Android) | configurable (default `low`) |
897
- | Enter / exit alerts | configurable (default `default`) |
1308
+ 2. **Doze mode**: Android Doze can delay BLE scan callbacks. The foreground service mitigates this, but very aggressive OEM battery optimization (Xiaomi, Huawei, Samsung) may still interfere. Direct users to disable battery optimization for your app.
898
1309
 
899
- Both notifications share the channel id `expo_beacon_channel`. The channel is recreated on each `onStartCommand` so config changes take effect on the next monitoring start.
1310
+ 3. **Boot receiver**: Monitoring auto-restarts after reboot via `BootReceiver` reading the `is_monitoring` flag from `SharedPreferences`.
1311
+
1312
+ 4. **Runtime permissions**: Android 12+ requires `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` in addition to location. Android 13+ requires `POST_NOTIFICATIONS` for the foreground service notification. `requestPermissionsAsync()` handles all of these.
1313
+
1314
+ 5. **Notification channel immutability**: Once Android creates a notification channel with a given importance level, decreasing the importance has no effect. The only workaround is uninstalling and reinstalling the app.
1315
+
1316
+ ---
1317
+
1318
+ ## Troubleshooting
1319
+
1320
+ ### "WILDCARD_NOT_SUPPORTED" error on iOS
1321
+
1322
+ You called `scanForBeaconsAsync([])` with no paired beacons. Either:
1323
+ - Pass at least one UUID: `scanForBeaconsAsync(["YOUR-UUID"])`
1324
+ - Or pair beacons first with `pairBeacon()` — the module will auto-use their UUIDs
1325
+
1326
+ ### Scanning returns empty results
1327
+
1328
+ 1. Verify Bluetooth is enabled on the device
1329
+ 2. Ensure you called `requestPermissionsAsync()` and got `true`
1330
+ 3. On iOS, confirm you passed a valid UUID or have paired beacons
1331
+ 4. The beacon must be powered on, advertising, and within BLE range (~30–70 m typical)
1332
+ 5. Try a longer scan duration (10000 ms)
1333
+
1334
+ ### Monitoring events not firing
1335
+
1336
+ 1. Ensure beacons are paired **before** calling `startMonitoring()`
1337
+ 2. Check that permissions returned `true` (iOS needs "Always" authorization for background monitoring)
1338
+ 3. On iOS, verify Background Modes are enabled in Xcode
1339
+ 4. On Android, check that battery optimization is disabled for your app
1340
+ 5. If using `maxDistance`, the beacon may be too far — try removing the distance filter
1341
+
1342
+ ### Distance values are inaccurate
1343
+
1344
+ BLE distance estimation is inherently imprecise. RSSI fluctuates due to:
1345
+ - Physical obstacles (walls, furniture, the user's body)
1346
+ - Multipath interference
1347
+ - Device orientation
1348
+ - Other 2.4 GHz interference (Wi-Fi, microwaves)
1349
+
1350
+ Use distance values as approximate zones (immediate/near/far) rather than precise measurements. For best accuracy, calibrate `txPower` on your beacons at 1 metre.
1351
+
1352
+ ### Android foreground notification won't go away
1353
+
1354
+ The persistent notification is required by Android 8+ for background BLE scanning. It disappears when you call `stopMonitoring()`. You can customize its appearance via `setNotificationConfig()`.
1355
+
1356
+ ### `onBeaconEnter` fires repeatedly
1357
+
1358
+ The module uses hysteresis (3 consecutive readings) to prevent jitter. If you're still seeing repeated events, it may be because the beacon is at the boundary of `maxDistance`. Consider adding a margin to your distance threshold.
1359
+
1360
+ ---
1361
+
1362
+ ## Error Codes
1363
+
1364
+ | Code | Method | Description |
1365
+ |---|---|---|
1366
+ | `SCAN_IN_PROGRESS` | `scanForBeaconsAsync`, `scanForEddystonesAsync` | A scan is already running. Wait for it to complete or call `cancelScan()`. |
1367
+ | `SCAN_CANCELLED` | `scanForBeaconsAsync`, `scanForEddystonesAsync` | The scan was cancelled via `cancelScan()`. |
1368
+ | `INVALID_UUID` | `scanForBeaconsAsync`, `pairBeacon` | Malformed UUID string. |
1369
+ | `INVALID_DURATION` | `scanForBeaconsAsync`, `scanForEddystonesAsync` | Scan duration must be > 0. |
1370
+ | `INVALID_MAJOR` | `pairBeacon` | Major value not in range 0–65535. |
1371
+ | `INVALID_MINOR` | `pairBeacon` | Minor value not in range 0–65535. |
1372
+ | `INVALID_NAMESPACE` | `pairEddystone` | Namespace must be exactly 20 hex characters. |
1373
+ | `INVALID_INSTANCE` | `pairEddystone` | Instance must be exactly 12 hex characters. |
1374
+ | `PERMISSION_DENIED` | `scanForBeaconsAsync`, `startMonitoring` | Required permissions were not granted. |
1375
+ | `WILDCARD_NOT_SUPPORTED` | `scanForBeaconsAsync` | iOS only: no UUIDs provided and no paired beacons exist. |
900
1376
 
901
1377
  ---
902
1378