expo-beacon 0.1.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 +514 -0
- package/android/build.gradle +23 -0
- package/android/src/main/AndroidManifest.xml +57 -0
- package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +41 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +300 -0
- package/android/src/main/java/expo/modules/beacon/BootReceiver.kt +18 -0
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +329 -0
- package/build/ExpoBeacon.types.d.ts +53 -0
- package/build/ExpoBeacon.types.d.ts.map +1 -0
- package/build/ExpoBeacon.types.js +2 -0
- package/build/ExpoBeacon.types.js.map +1 -0
- package/build/ExpoBeaconModule.d.ts +46 -0
- package/build/ExpoBeaconModule.d.ts.map +1 -0
- package/build/ExpoBeaconModule.js +3 -0
- package/build/ExpoBeaconModule.js.map +1 -0
- package/build/ExpoBeaconModule.web.d.ts +16 -0
- package/build/ExpoBeaconModule.web.d.ts.map +1 -0
- package/build/ExpoBeaconModule.web.js +18 -0
- package/build/ExpoBeaconModule.web.js.map +1 -0
- package/build/ExpoBeaconView.d.ts +2 -0
- package/build/ExpoBeaconView.d.ts.map +1 -0
- package/build/ExpoBeaconView.js +2 -0
- package/build/ExpoBeaconView.js.map +1 -0
- package/build/ExpoBeaconView.web.d.ts +2 -0
- package/build/ExpoBeaconView.web.d.ts.map +1 -0
- package/build/ExpoBeaconView.web.js +2 -0
- package/build/ExpoBeaconView.web.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoBeacon.podspec +32 -0
- package/ios/ExpoBeaconModule.swift +432 -0
- package/ios/ExpoBeaconView.swift +5 -0
- package/package.json +67 -0
- package/src/ExpoBeacon.types.ts +57 -0
- package/src/ExpoBeaconModule.ts +64 -0
- package/src/ExpoBeaconModule.web.ts +31 -0
- package/src/ExpoBeaconView.tsx +2 -0
- package/src/ExpoBeaconView.web.tsx +2 -0
- package/src/index.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# expo-beacon
|
|
2
|
+
|
|
3
|
+
An Expo module for scanning, pairing, and monitoring iBeacons in React Native apps.
|
|
4
|
+
|
|
5
|
+
- **Scan** for nearby iBeacons via a one-shot or continuous BLE scan
|
|
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
|
|
10
|
+
|
|
11
|
+
| Platform | Native implementation |
|
|
12
|
+
| -------- | --------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| Android | [AltBeacon](https://altbeacon.github.io/android-beacon-library/) (`org.altbeacon:android-beacon-library`) |
|
|
14
|
+
| iOS | CoreLocation / `CLLocationManager` (iBeacon native API) |
|
|
15
|
+
| Web | Not supported (throws on all calls) |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx expo install expo-beacon
|
|
23
|
+
```
|
|
24
|
+
|
|
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/).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Platform Setup
|
|
30
|
+
|
|
31
|
+
### iOS
|
|
32
|
+
|
|
33
|
+
Add the following keys to your `Info.plist`:
|
|
34
|
+
|
|
35
|
+
```xml
|
|
36
|
+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
37
|
+
<string>This app monitors iBeacons in the background.</string>
|
|
38
|
+
<key>NSLocationWhenInUseUsageDescription</key>
|
|
39
|
+
<string>This app uses location to detect nearby beacons.</string>
|
|
40
|
+
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
41
|
+
<string>This app uses Bluetooth to scan for iBeacons.</string>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
In Xcode under **Signing & Capabilities**, enable:
|
|
45
|
+
|
|
46
|
+
- **Background Modes → Location updates**
|
|
47
|
+
- **Background Modes → Uses Bluetooth LE accessories**
|
|
48
|
+
|
|
49
|
+
> iOS limits apps to **20 simultaneously monitored regions**.
|
|
50
|
+
|
|
51
|
+
### Android
|
|
52
|
+
|
|
53
|
+
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:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await ExpoBeacon.requestPermissionsAsync();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick-start example
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { useEffect, useState } from "react";
|
|
65
|
+
import { Button, FlatList, Text, View } from "react-native";
|
|
66
|
+
import ExpoBeacon from "expo-beacon";
|
|
67
|
+
import type {
|
|
68
|
+
BeaconScanResult,
|
|
69
|
+
BeaconRegionEvent,
|
|
70
|
+
BeaconDistanceEvent,
|
|
71
|
+
} from "expo-beacon";
|
|
72
|
+
|
|
73
|
+
export default function App() {
|
|
74
|
+
const [beacons, setBeacons] = useState<BeaconScanResult[]>([]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
// 1. Pair a known beacon for monitoring
|
|
78
|
+
ExpoBeacon.pairBeacon(
|
|
79
|
+
"lobby-entrance",
|
|
80
|
+
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
|
|
81
|
+
1,
|
|
82
|
+
100,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 2. Subscribe to region events
|
|
86
|
+
const enterSub = ExpoBeacon.addListener(
|
|
87
|
+
"onBeaconEnter",
|
|
88
|
+
(e: BeaconRegionEvent) =>
|
|
89
|
+
console.log(`Entered ${e.identifier} at ${e.distance.toFixed(1)} m`),
|
|
90
|
+
);
|
|
91
|
+
const exitSub = ExpoBeacon.addListener("onBeaconExit", (e: BeaconRegionEvent) =>
|
|
92
|
+
console.log(`Exited ${e.identifier}`),
|
|
93
|
+
);
|
|
94
|
+
const distSub = ExpoBeacon.addListener(
|
|
95
|
+
"onBeaconDistance",
|
|
96
|
+
(e: BeaconDistanceEvent) =>
|
|
97
|
+
console.log(`${e.identifier}: ${e.distance.toFixed(2)} m`),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// 3. Start background monitoring (only fires for paired beacons)
|
|
101
|
+
ExpoBeacon.requestPermissionsAsync().then((granted) => {
|
|
102
|
+
if (granted) ExpoBeacon.startMonitoring(10); // enter events within 10 m
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
enterSub.remove();
|
|
107
|
+
exitSub.remove();
|
|
108
|
+
distSub.remove();
|
|
109
|
+
ExpoBeacon.stopMonitoring();
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
async function scan() {
|
|
114
|
+
const results = await ExpoBeacon.scanForBeaconsAsync(5000);
|
|
115
|
+
setBeacons(results);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View>
|
|
120
|
+
<Button title="Scan 5 s" onPress={scan} />
|
|
121
|
+
<FlatList
|
|
122
|
+
data={beacons}
|
|
123
|
+
keyExtractor={(b) => `${b.uuid}-${b.major}-${b.minor}`}
|
|
124
|
+
renderItem={({ item: b }) => (
|
|
125
|
+
<Text>
|
|
126
|
+
{b.uuid} {b.major}/{b.minor} — {b.distance.toFixed(1)} m
|
|
127
|
+
</Text>
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
</View>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## API Reference
|
|
138
|
+
|
|
139
|
+
### `requestPermissionsAsync()`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
requestPermissionsAsync(): Promise<boolean>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Requests all permissions required for scanning and monitoring:
|
|
146
|
+
|
|
147
|
+
- **Android**: `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION`, `POST_NOTIFICATIONS` (API 33+)
|
|
148
|
+
- **iOS**: `CLLocationManager` "Always" authorization
|
|
149
|
+
|
|
150
|
+
Returns `true` if all permissions were granted, `false` otherwise.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const granted = await ExpoBeacon.requestPermissionsAsync();
|
|
154
|
+
if (!granted) {
|
|
155
|
+
console.warn("Permissions not granted — scanning and monitoring will fail.");
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `scanForBeaconsAsync(scanDurationMs?)`
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
scanForBeaconsAsync(scanDurationMs?: number): Promise<BeaconScanResult[]>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Starts a **one-shot BLE scan**, waits for `scanDurationMs` milliseconds, then resolves with all beacons discovered during that window.
|
|
168
|
+
|
|
169
|
+
| Parameter | Type | Default | Description |
|
|
170
|
+
| --------------- | -------- | ------- | ---------------------------------------- |
|
|
171
|
+
| `scanDurationMs`| `number` | `5000` | How long to scan in milliseconds (1–60 000 recommended) |
|
|
172
|
+
|
|
173
|
+
Returns an array of [`BeaconScanResult`](#beaconscanresult) objects. Rejects with `SCAN_IN_PROGRESS` if another scan is already running.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const beacons = await ExpoBeacon.scanForBeaconsAsync(8000); // 8-second scan
|
|
177
|
+
beacons.forEach((b) =>
|
|
178
|
+
console.log(`${b.uuid} major=${b.major} minor=${b.minor} dist=${b.distance.toFixed(1)}m rssi=${b.rssi}dBm`)
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `startContinuousScan()`
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
startContinuousScan(): void
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Begins a **continuous BLE scan** that fires an [`onBeaconFound`](#onbeaconfound) event every time a beacon advertisement is received. Call [`stopContinuousScan()`](#stopcontinuousscan) to end it.
|
|
191
|
+
|
|
192
|
+
Unlike `scanForBeaconsAsync`, this never resolves — it streams results in real time.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const sub = ExpoBeacon.addListener("onBeaconFound", (beacon) => {
|
|
196
|
+
console.log("Live:", beacon.uuid, beacon.distance);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
ExpoBeacon.startContinuousScan();
|
|
200
|
+
|
|
201
|
+
// later, when done:
|
|
202
|
+
ExpoBeacon.stopContinuousScan();
|
|
203
|
+
sub.remove();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### `stopContinuousScan()`
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
stopContinuousScan(): void
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Stops the continuous scan started by `startContinuousScan()`. No-op if no scan is running.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### `pairBeacon(identifier, uuid, major, minor)`
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
pairBeacon(identifier: string, uuid: string, major: number, minor: number): void
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
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.
|
|
225
|
+
|
|
226
|
+
| Parameter | Type | Description |
|
|
227
|
+
| ------------ | -------- | ------------------------------------------------------------- |
|
|
228
|
+
| `identifier` | `string` | Your unique label for this beacon (e.g. `"lobby-entrance"`) |
|
|
229
|
+
| `uuid` | `string` | iBeacon proximity UUID (case-insensitive, standard format) |
|
|
230
|
+
| `major` | `number` | iBeacon major value (0 – 65535) |
|
|
231
|
+
| `minor` | `number` | iBeacon minor value (0 – 65535) |
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
ExpoBeacon.pairBeacon(
|
|
235
|
+
"main-door",
|
|
236
|
+
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
|
|
237
|
+
1,
|
|
238
|
+
42,
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### `unpairBeacon(identifier)`
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
unpairBeacon(identifier: string): void
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Removes a previously paired beacon. If monitoring is active, the region for this beacon stops being monitored immediately.
|
|
251
|
+
|
|
252
|
+
| Parameter | Type | Description |
|
|
253
|
+
| ------------ | -------- | ---------------------------------- |
|
|
254
|
+
| `identifier` | `string` | The label used when pairing |
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
ExpoBeacon.unpairBeacon("main-door");
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### `getPairedBeacons()`
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
getPairedBeacons(): PairedBeacon[]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Returns the list of all currently paired beacons from persistent storage.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const paired = ExpoBeacon.getPairedBeacons();
|
|
272
|
+
paired.forEach((b) =>
|
|
273
|
+
console.log(b.identifier, b.uuid, b.major, b.minor)
|
|
274
|
+
);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### `startMonitoring(maxDistance?)`
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
startMonitoring(maxDistance?: number): Promise<void>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Starts background region monitoring for all paired beacons.
|
|
286
|
+
|
|
287
|
+
| Parameter | Type | Default | Description |
|
|
288
|
+
| ------------- | -------- | ------------ | --------------------------------------------------------------------------------------------- |
|
|
289
|
+
| `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. |
|
|
290
|
+
|
|
291
|
+
**Android**: Launches `BeaconForegroundService` — a persistent foreground service required by Android 8+ for background BLE. Restarts automatically after device reboot.
|
|
292
|
+
|
|
293
|
+
**iOS**: Activates `CLLocationManager` region monitoring. iOS can wake or relaunch the app when a region boundary is crossed, even if the app is terminated.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// Monitor with no distance limit
|
|
297
|
+
await ExpoBeacon.startMonitoring();
|
|
298
|
+
|
|
299
|
+
// Only fire enter events when within 5 metres
|
|
300
|
+
await ExpoBeacon.startMonitoring(5);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
> Call `requestPermissionsAsync()` before `startMonitoring()`.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### `stopMonitoring()`
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
stopMonitoring(): Promise<void>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Stops background monitoring and removes all active region subscriptions. On Android, stops the foreground service.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
await ExpoBeacon.stopMonitoring();
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Events
|
|
322
|
+
|
|
323
|
+
Subscribe to events using `ExpoBeacon.addListener(eventName, handler)`. Always call `.remove()` on the subscription when your component unmounts.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const sub = ExpoBeacon.addListener("onBeaconEnter", handler);
|
|
327
|
+
// cleanup:
|
|
328
|
+
sub.remove();
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### `onBeaconEnter`
|
|
334
|
+
|
|
335
|
+
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.
|
|
336
|
+
|
|
337
|
+
**Payload**: [`BeaconRegionEvent`](#beaconregionevent)
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
ExpoBeacon.addListener("onBeaconEnter", (e) => {
|
|
341
|
+
console.log(`Entered "${e.identifier}" (${e.uuid}) at ~${e.distance.toFixed(1)} m`);
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
### `onBeaconExit`
|
|
348
|
+
|
|
349
|
+
Fired when the device leaves the region of a monitored beacon. Always fired regardless of distance filtering.
|
|
350
|
+
|
|
351
|
+
**Payload**: [`BeaconRegionEvent`](#beaconregionevent)
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
ExpoBeacon.addListener("onBeaconExit", (e) => {
|
|
355
|
+
console.log(`Left "${e.identifier}"`);
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
### `onBeaconRanging`
|
|
362
|
+
|
|
363
|
+
Fired periodically during active monitoring with the latest ranging measurement for a paired beacon.
|
|
364
|
+
|
|
365
|
+
**Payload**: [`BeaconRangingEvent`](#beaconrangingevent)
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
ExpoBeacon.addListener("onBeaconRanging", (e) => {
|
|
369
|
+
console.log(`Ranging ${e.identifier}: ${e.distance.toFixed(2)} m rssi=${e.rssi}`);
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
### `onBeaconDistance`
|
|
376
|
+
|
|
377
|
+
Fired continuously during monitoring whenever a distance update is received for a paired beacon. Useful for real-time proximity UI.
|
|
378
|
+
|
|
379
|
+
**Payload**: [`BeaconDistanceEvent`](#beacondistanceevent)
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
ExpoBeacon.addListener("onBeaconDistance", (e) => {
|
|
383
|
+
setProximity(e.distance);
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### `onBeaconFound`
|
|
390
|
+
|
|
391
|
+
Fired during a **continuous scan** (started with `startContinuousScan()`) each time a beacon advertisement is received.
|
|
392
|
+
|
|
393
|
+
**Payload**: [`BeaconScanResult`](#beaconscanresult)
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
ExpoBeacon.addListener("onBeaconFound", (b) => {
|
|
397
|
+
console.log(`Found ${b.uuid} ${b.major}/${b.minor} at ${b.distance.toFixed(1)} m`);
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## TypeScript Types
|
|
404
|
+
|
|
405
|
+
### `BeaconScanResult`
|
|
406
|
+
|
|
407
|
+
Returned by `scanForBeaconsAsync` and used in `onBeaconFound` events.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
type BeaconScanResult = {
|
|
411
|
+
uuid: string; // iBeacon proximity UUID (uppercase, formatted)
|
|
412
|
+
major: number; // iBeacon major value (0–65535)
|
|
413
|
+
minor: number; // iBeacon minor value (0–65535)
|
|
414
|
+
rssi: number; // Signal strength in dBm (negative integer, e.g. –65)
|
|
415
|
+
distance: number; // Estimated distance in metres (calculated from RSSI + txPower)
|
|
416
|
+
txPower: number; // Calibrated TX power from the beacon advertisement
|
|
417
|
+
};
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### `PairedBeacon`
|
|
421
|
+
|
|
422
|
+
Returned by `getPairedBeacons`.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
type PairedBeacon = {
|
|
426
|
+
identifier: string; // Your label
|
|
427
|
+
uuid: string;
|
|
428
|
+
major: number;
|
|
429
|
+
minor: number;
|
|
430
|
+
};
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### `BeaconRegionEvent`
|
|
434
|
+
|
|
435
|
+
Payload for `onBeaconEnter` and `onBeaconExit`.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
type BeaconRegionEvent = {
|
|
439
|
+
identifier: string; // Matches PairedBeacon.identifier
|
|
440
|
+
uuid: string;
|
|
441
|
+
major: number;
|
|
442
|
+
minor: number;
|
|
443
|
+
event: "enter" | "exit";
|
|
444
|
+
distance: number; // Measured distance in metres at event time; –1 if unavailable
|
|
445
|
+
};
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### `BeaconRangingEvent`
|
|
449
|
+
|
|
450
|
+
Payload for `onBeaconRanging`.
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
type BeaconRangingEvent = {
|
|
454
|
+
identifier: string;
|
|
455
|
+
uuid: string;
|
|
456
|
+
major: number;
|
|
457
|
+
minor: number;
|
|
458
|
+
rssi: number; // Signal strength in dBm
|
|
459
|
+
distance: number; // Estimated distance in metres
|
|
460
|
+
};
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### `BeaconDistanceEvent`
|
|
464
|
+
|
|
465
|
+
Payload for `onBeaconDistance`.
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
type BeaconDistanceEvent = {
|
|
469
|
+
identifier: string;
|
|
470
|
+
uuid: string;
|
|
471
|
+
major: number;
|
|
472
|
+
minor: number;
|
|
473
|
+
distance: number; // Estimated distance in metres
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Background Behaviour
|
|
480
|
+
|
|
481
|
+
### Android
|
|
482
|
+
|
|
483
|
+
`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`).
|
|
484
|
+
|
|
485
|
+
Default scan timing: 1.1 s scan window every 5 s.
|
|
486
|
+
|
|
487
|
+
### iOS
|
|
488
|
+
|
|
489
|
+
`startMonitoring()` activates `CLLocationManager` **region monitoring**. 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`.
|
|
490
|
+
|
|
491
|
+
> iOS limits apps to **20 simultaneously monitored regions**.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Notifications
|
|
496
|
+
|
|
497
|
+
A local notification is posted for every `onBeaconEnter` and `onBeaconExit` event.
|
|
498
|
+
|
|
499
|
+
| Channel / type | Importance |
|
|
500
|
+
| ------------------------------- | ------------------- |
|
|
501
|
+
| Foreground service (Android) | `IMPORTANCE_LOW` |
|
|
502
|
+
| Enter / exit alerts | `IMPORTANCE_DEFAULT`|
|
|
503
|
+
|
|
504
|
+
Both channels share the id `expo_beacon_channel`.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Contributing
|
|
509
|
+
|
|
510
|
+
Contributions are welcome! Please refer to the [contributing guide](https://github.com/expo/expo#contributing).
|
|
511
|
+
|
|
512
|
+
## License
|
|
513
|
+
|
|
514
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'expo.modules.beacon'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "expo.modules.beacon"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName "0.1.0"
|
|
14
|
+
minSdkVersion 23
|
|
15
|
+
}
|
|
16
|
+
lintOptions {
|
|
17
|
+
abortOnError false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dependencies {
|
|
22
|
+
implementation 'org.altbeacon:android-beacon-library:2.21.2'
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
3
|
+
|
|
4
|
+
<!-- Bluetooth permissions (Android 12+ requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT) -->
|
|
5
|
+
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
|
6
|
+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
|
7
|
+
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
|
8
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
9
|
+
|
|
10
|
+
<!-- Location permissions required for BLE scanning -->
|
|
11
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
12
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
13
|
+
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
14
|
+
|
|
15
|
+
<!-- Foreground service for background BLE scanning -->
|
|
16
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
17
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
|
18
|
+
|
|
19
|
+
<!-- Notifications permission (Android 13+) -->
|
|
20
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
21
|
+
|
|
22
|
+
<!-- Receive boot broadcast to restart monitoring after device reboot -->
|
|
23
|
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
24
|
+
|
|
25
|
+
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
|
26
|
+
|
|
27
|
+
<application>
|
|
28
|
+
<!-- AltBeacon background scanning service -->
|
|
29
|
+
<service
|
|
30
|
+
android:name="org.altbeacon.beacon.service.BeaconService"
|
|
31
|
+
android:enabled="true"
|
|
32
|
+
android:exported="false"
|
|
33
|
+
android:isolatedProcess="false"
|
|
34
|
+
android:label="iBeacon Service"
|
|
35
|
+
tools:replace="android:label" />
|
|
36
|
+
|
|
37
|
+
<service
|
|
38
|
+
android:name="org.altbeacon.beacon.BeaconIntentProcessor"
|
|
39
|
+
android:exported="false" />
|
|
40
|
+
|
|
41
|
+
<!-- Foreground service for sustained background scanning -->
|
|
42
|
+
<service
|
|
43
|
+
android:name="expo.modules.beacon.BeaconForegroundService"
|
|
44
|
+
android:foregroundServiceType="connectedDevice"
|
|
45
|
+
android:exported="false" />
|
|
46
|
+
|
|
47
|
+
<!-- Restart monitoring after boot -->
|
|
48
|
+
<receiver
|
|
49
|
+
android:name="expo.modules.beacon.BootReceiver"
|
|
50
|
+
android:exported="true">
|
|
51
|
+
<intent-filter>
|
|
52
|
+
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
53
|
+
</intent-filter>
|
|
54
|
+
</receiver>
|
|
55
|
+
</application>
|
|
56
|
+
|
|
57
|
+
</manifest>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package expo.modules.beacon
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Receives ACTION_BEACON_EVENT broadcasts from BeaconForegroundService
|
|
9
|
+
* and forwards them to the Expo module event system.
|
|
10
|
+
*/
|
|
11
|
+
class BeaconEventReceiver(
|
|
12
|
+
private val onEvent: (eventName: String, params: Map<String, Any>) -> Unit
|
|
13
|
+
) : BroadcastReceiver() {
|
|
14
|
+
|
|
15
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
16
|
+
if (intent.action != ACTION_BEACON_EVENT) return
|
|
17
|
+
|
|
18
|
+
val identifier = intent.getStringExtra("identifier") ?: return
|
|
19
|
+
val uuid = intent.getStringExtra("uuid") ?: ""
|
|
20
|
+
val major = intent.getIntExtra("major", 0)
|
|
21
|
+
val minor = intent.getIntExtra("minor", 0)
|
|
22
|
+
val eventType = intent.getStringExtra("event") ?: return
|
|
23
|
+
|
|
24
|
+
val params = mapOf(
|
|
25
|
+
"identifier" to identifier,
|
|
26
|
+
"uuid" to uuid,
|
|
27
|
+
"major" to major,
|
|
28
|
+
"minor" to minor,
|
|
29
|
+
"event" to eventType,
|
|
30
|
+
"distance" to intent.getDoubleExtra("distance", -1.0)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
val eventName = when (eventType) {
|
|
34
|
+
"enter" -> "onBeaconEnter"
|
|
35
|
+
"exit" -> "onBeaconExit"
|
|
36
|
+
"distance" -> "onBeaconDistance"
|
|
37
|
+
else -> return
|
|
38
|
+
}
|
|
39
|
+
onEvent(eventName, params)
|
|
40
|
+
}
|
|
41
|
+
}
|