expo-beacon 0.4.0 → 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.
- package/README.md +852 -376
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/expo/modules/beacon/BeaconConstants.kt +26 -0
- package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +5 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +113 -115
- package/android/src/main/java/expo/modules/beacon/BeaconParsers.kt +33 -0
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +219 -115
- package/build/ExpoBeacon.types.d.ts +12 -2
- package/build/ExpoBeacon.types.d.ts.map +1 -1
- package/build/ExpoBeacon.types.js.map +1 -1
- package/build/ExpoBeaconModule.d.ts +10 -3
- package/build/ExpoBeaconModule.d.ts.map +1 -1
- package/build/ExpoBeaconModule.js.map +1 -1
- package/build/ExpoBeaconModule.web.d.ts +4 -0
- package/build/ExpoBeaconModule.web.d.ts.map +1 -1
- package/build/ExpoBeaconModule.web.js +4 -0
- package/build/ExpoBeaconModule.web.js.map +1 -1
- package/ios/ExpoBeaconModule.swift +371 -304
- package/package.json +1 -1
- package/src/ExpoBeacon.types.ts +12 -2
- package/src/ExpoBeaconModule.ts +11 -3
- package/src/ExpoBeaconModule.web.ts +4 -0
- package/build/ExpoBeaconView.d.ts +0 -2
- package/build/ExpoBeaconView.d.ts.map +0 -1
- package/build/ExpoBeaconView.js +0 -2
- package/build/ExpoBeaconView.js.map +0 -1
- package/build/ExpoBeaconView.web.d.ts +0 -2
- package/build/ExpoBeaconView.web.d.ts.map +0 -1
- package/build/ExpoBeaconView.web.js +0 -2
- package/build/ExpoBeaconView.web.js.map +0 -1
- package/ios/ExpoBeaconView.swift +0 -5
- package/src/ExpoBeaconView.tsx +0 -2
- 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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
93
|
-
ExpoBeacon.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
//
|
|
146
|
+
// 3. Request permissions and start monitoring
|
|
132
147
|
ExpoBeacon.requestPermissionsAsync().then((granted) => {
|
|
133
|
-
if (granted) ExpoBeacon.startMonitoring(10); // enter
|
|
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
|
-
|
|
149
|
-
|
|
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)}
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
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,69 +610,82 @@ if (!granted) {
|
|
|
199
610
|
scanForBeaconsAsync(uuids?: string[], scanDurationMs?: number): Promise<BeaconScanResult[]>
|
|
200
611
|
```
|
|
201
612
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
| Parameter | Type | Default | Description |
|
|
205
|
-
| ---------------- | ---------- | ------- | ---------------------------------------- |
|
|
206
|
-
| `uuids` | `string[]` | `[]` | Proximity UUIDs to filter by. Pass `[]` or omit for a **wildcard scan** that discovers all nearby iBeacons. |
|
|
207
|
-
| `scanDurationMs` | `number` | `5000` | How long to scan in milliseconds (1–60 000 recommended) |
|
|
613
|
+
Performs a **one-shot iBeacon scan**. Waits for the specified duration, then resolves with all discovered beacons.
|
|
208
614
|
|
|
209
|
-
|
|
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). |
|
|
210
619
|
|
|
211
|
-
**
|
|
620
|
+
**Returns**: `BeaconScanResult[]` — deduplicated by UUID + major + minor.
|
|
212
621
|
|
|
213
|
-
| |
|
|
622
|
+
| Behaviour | Android | iOS |
|
|
214
623
|
|---|---|---|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
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 |
|
|
217
626
|
|
|
218
|
-
|
|
627
|
+
**Possible errors**:
|
|
219
628
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 |
|
|
223
637
|
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
[
|
|
638
|
+
```ts
|
|
639
|
+
const beacons = await ExpoBeacon.scanForBeaconsAsync(
|
|
640
|
+
["E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"],
|
|
227
641
|
8000,
|
|
228
642
|
);
|
|
229
|
-
|
|
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
|
-
);
|
|
233
643
|
```
|
|
234
644
|
|
|
235
645
|
---
|
|
236
646
|
|
|
237
|
-
### `
|
|
647
|
+
### `scanForEddystonesAsync(scanDurationMs?)`
|
|
238
648
|
|
|
239
649
|
```ts
|
|
240
|
-
|
|
650
|
+
scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>
|
|
241
651
|
```
|
|
242
652
|
|
|
243
|
-
|
|
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). |
|
|
244
658
|
|
|
245
|
-
|
|
659
|
+
**Returns**: `EddystoneScanResult[]` — deduplicated by namespace:instance (UID) or url (URL).
|
|
246
660
|
|
|
247
|
-
|
|
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 |
|
|
248
668
|
|
|
249
669
|
```ts
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
console.log("Eddystone:", beacon.frameType, beacon.namespace ?? beacon.url);
|
|
255
|
-
});
|
|
670
|
+
const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
256
674
|
|
|
257
|
-
|
|
675
|
+
### `startContinuousScan()`
|
|
258
676
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
sub.remove();
|
|
262
|
-
eddySub.remove();
|
|
677
|
+
```ts
|
|
678
|
+
startContinuousScan(): void
|
|
263
679
|
```
|
|
264
680
|
|
|
681
|
+
Begins a **continuous BLE scan** that streams beacon discoveries via events:
|
|
682
|
+
- `onBeaconFound` — iBeacon advertisements
|
|
683
|
+
- `onEddystoneFound` — Eddystone advertisements
|
|
684
|
+
|
|
685
|
+
Does not return results directly — subscribe to events before calling. Call `stopContinuousScan()` to end.
|
|
686
|
+
|
|
687
|
+
> **iOS**: Only reports iBeacons whose UUID is registered via `pairBeacon()`. Eddystones are reported regardless of pairing.
|
|
688
|
+
|
|
265
689
|
---
|
|
266
690
|
|
|
267
691
|
### `stopContinuousScan()`
|
|
@@ -270,7 +694,17 @@ eddySub.remove();
|
|
|
270
694
|
stopContinuousScan(): void
|
|
271
695
|
```
|
|
272
696
|
|
|
273
|
-
Stops the continuous scan
|
|
697
|
+
Stops the continuous scan. No-op if no scan is running.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
### `cancelScan()`
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
cancelScan(): void
|
|
705
|
+
```
|
|
706
|
+
|
|
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
|
|
717
|
+
Registers an iBeacon for persistent monitoring.
|
|
284
718
|
|
|
285
|
-
| Parameter
|
|
286
|
-
|
|
287
|
-
| `identifier` | `string` |
|
|
288
|
-
| `uuid`
|
|
289
|
-
| `major`
|
|
290
|
-
| `minor`
|
|
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
|
|
740
|
+
Removes a paired iBeacon. If monitoring is active, the region stops being tracked immediately.
|
|
310
741
|
|
|
311
|
-
| Parameter
|
|
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
|
|
758
|
+
Returns all currently paired iBeacons from persistent storage.
|
|
328
759
|
|
|
329
760
|
```ts
|
|
330
761
|
const paired = ExpoBeacon.getPairedBeacons();
|
|
331
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
795
|
+
Removes a paired Eddystone beacon.
|
|
396
796
|
|
|
397
|
-
| Parameter
|
|
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
|
|
813
|
+
Returns all currently paired Eddystone beacons from persistent storage.
|
|
414
814
|
|
|
415
815
|
```ts
|
|
416
816
|
const paired = ExpoBeacon.getPairedEddystones();
|
|
417
|
-
|
|
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 (
|
|
828
|
+
Starts background region monitoring for **all paired beacons** (iBeacon + Eddystone).
|
|
431
829
|
|
|
432
|
-
Accepts
|
|
830
|
+
Accepts a `MonitoringOptions` object, a plain `number` (shorthand for `maxDistance`), or nothing.
|
|
433
831
|
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
844
|
+
**Possible errors**: `PERMISSION_DENIED` (Always authorization required on iOS).
|
|
444
845
|
|
|
445
846
|
```ts
|
|
446
|
-
//
|
|
847
|
+
// Shorthand — just a distance threshold
|
|
447
848
|
await ExpoBeacon.startMonitoring(5);
|
|
448
849
|
|
|
449
|
-
// Full options
|
|
850
|
+
// Full options
|
|
450
851
|
await ExpoBeacon.startMonitoring({
|
|
451
|
-
maxDistance:
|
|
852
|
+
maxDistance: 10,
|
|
452
853
|
notifications: {
|
|
453
854
|
beaconEvents: {
|
|
454
|
-
enterTitle: "
|
|
455
|
-
body: "{identifier} is
|
|
855
|
+
enterTitle: "Welcome!",
|
|
856
|
+
body: "{identifier} is nearby",
|
|
456
857
|
},
|
|
457
858
|
},
|
|
458
859
|
});
|
|
459
860
|
|
|
460
|
-
//
|
|
861
|
+
// No distance filter, silent
|
|
461
862
|
await ExpoBeacon.startMonitoring({
|
|
462
863
|
notifications: { beaconEvents: { enabled: false } },
|
|
463
864
|
});
|
|
464
|
-
```
|
|
465
865
|
|
|
466
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
499
|
-
enterTitle: "
|
|
500
|
-
exitTitle: "
|
|
501
|
-
body: "{identifier} {event}ed",
|
|
502
|
-
sound: true,
|
|
503
|
-
icon: "
|
|
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: "
|
|
509
|
-
text: "
|
|
510
|
-
icon: "ic_service",
|
|
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: "
|
|
516
|
-
description: "
|
|
517
|
-
importance: "default",
|
|
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
|
|
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
|
-
//
|
|
929
|
+
// Later:
|
|
533
930
|
sub.remove();
|
|
534
931
|
```
|
|
535
932
|
|
|
536
|
-
|
|
933
|
+
### Event Summary
|
|
537
934
|
|
|
538
|
-
|
|
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
|
-
|
|
946
|
+
### Event Detail
|
|
541
947
|
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
964
|
+
#### `onBeaconExit`
|
|
551
965
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
Fired when the device leaves the region of a monitored beacon. Always fired regardless of distance filtering.
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(`
|
|
1002
|
+
console.log(`UID: ${b.namespace}/${b.instance} — ${b.distance.toFixed(1)}m`);
|
|
610
1003
|
} else {
|
|
611
|
-
console.log(`
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
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
|
|
1067
|
+
Returned by `scanForBeaconsAsync()` and `onBeaconFound`.
|
|
665
1068
|
|
|
666
1069
|
```ts
|
|
667
1070
|
type BeaconScanResult = {
|
|
668
|
-
uuid: string; //
|
|
669
|
-
major: number; //
|
|
670
|
-
minor: number; //
|
|
671
|
-
rssi: number; // Signal strength in dBm (negative
|
|
672
|
-
distance: number; // Estimated distance in metres
|
|
673
|
-
txPower: number; // Calibrated TX power from the
|
|
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`
|
|
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; //
|
|
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
|
|
1124
|
+
Returned by `scanForEddystonesAsync()` and `onEddystoneFound`.
|
|
737
1125
|
|
|
738
1126
|
```ts
|
|
739
1127
|
type EddystoneScanResult = {
|
|
740
1128
|
frameType: "uid" | "url";
|
|
741
|
-
namespace?: string; //
|
|
742
|
-
instance?: string; //
|
|
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;
|
|
745
|
-
distance: number;
|
|
746
|
-
txPower: number;
|
|
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;
|
|
757
|
-
namespace: string; //
|
|
758
|
-
instance: string; //
|
|
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`
|
|
1152
|
+
Payload for `onEddystoneEnter` / `onEddystoneExit`.
|
|
765
1153
|
|
|
766
1154
|
```ts
|
|
767
1155
|
type EddystoneRegionEvent = {
|
|
768
|
-
identifier: string;
|
|
1156
|
+
identifier: string;
|
|
769
1157
|
namespace: string;
|
|
770
1158
|
instance: string;
|
|
771
1159
|
event: "enter" | "exit";
|
|
772
|
-
distance: number; //
|
|
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;
|
|
1173
|
+
distance: number;
|
|
786
1174
|
};
|
|
787
1175
|
```
|
|
788
1176
|
|
|
789
|
-
---
|
|
790
|
-
|
|
791
1177
|
### `MonitoringOptions`
|
|
792
1178
|
|
|
793
|
-
Passed to `startMonitoring(
|
|
1179
|
+
Passed to `startMonitoring()`.
|
|
794
1180
|
|
|
795
1181
|
```ts
|
|
796
1182
|
type MonitoringOptions = {
|
|
797
|
-
maxDistance?: number;
|
|
798
|
-
notifications?: NotificationConfig;
|
|
1183
|
+
maxDistance?: number;
|
|
1184
|
+
notifications?: NotificationConfig;
|
|
799
1185
|
};
|
|
800
1186
|
```
|
|
801
1187
|
|
|
802
1188
|
### `NotificationConfig`
|
|
803
1189
|
|
|
804
|
-
Top-level
|
|
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; //
|
|
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; //
|
|
824
|
-
//
|
|
825
|
-
sound?: boolean; // iOS only
|
|
826
|
-
icon?: string; // Android only
|
|
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;
|
|
849
|
-
description?: string;
|
|
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
|
|
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
|
-
|
|
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`
|
|
1251
|
+
`startMonitoring()` activates `CLLocationManager` region monitoring for iBeacons and CoreBluetooth BLE scanning for Eddystones.
|
|
867
1252
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|