expo-beacon 0.9.3 → 0.10.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 +268 -59
- package/android/src/main/AndroidManifest.xml +0 -2
- package/android/src/main/java/expo/modules/beacon/BeaconApiForwarder.kt +44 -31
- package/android/src/main/java/expo/modules/beacon/BeaconConstants.kt +20 -0
- package/android/src/main/java/expo/modules/beacon/BeaconEventLogger.kt +2 -1
- package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +34 -88
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +257 -264
- package/android/src/main/java/expo/modules/beacon/BootReceiver.kt +20 -8
- package/android/src/main/java/expo/modules/beacon/CarPlayMonitor.kt +17 -9
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +222 -223
- package/build/ExpoBeacon.types.d.ts +19 -18
- package/build/ExpoBeacon.types.d.ts.map +1 -1
- package/build/ExpoBeacon.types.js.map +1 -1
- package/build/ExpoBeaconModule.d.ts +35 -8
- package/build/ExpoBeaconModule.d.ts.map +1 -1
- package/build/ExpoBeaconModule.js +4 -0
- package/build/ExpoBeaconModule.js.map +1 -1
- package/build/ExpoBeaconModule.web.d.ts +2 -41
- package/build/ExpoBeaconModule.web.d.ts.map +1 -1
- package/build/ExpoBeaconModule.web.js +18 -5
- package/build/ExpoBeaconModule.web.js.map +1 -1
- package/build/hooks/useBeacon.d.ts +100 -0
- package/build/hooks/useBeacon.d.ts.map +1 -0
- package/build/hooks/useBeacon.js +225 -0
- package/build/hooks/useBeacon.js.map +1 -0
- package/build/hooks/useCarPlay.d.ts +46 -0
- package/build/hooks/useCarPlay.d.ts.map +1 -0
- package/build/hooks/useCarPlay.js +88 -0
- package/build/hooks/useCarPlay.js.map +1 -0
- package/build/index.d.ts +5 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BeaconApiForwarder.swift +6 -9
- package/ios/BluetoothDelegate.swift +2 -4
- package/ios/CarPlayMonitor.swift +26 -22
- package/ios/ExpoBeaconConstants.swift +25 -0
- package/ios/ExpoBeaconModule+Eddystone.swift +30 -14
- package/ios/ExpoBeaconModule+EventLogging.swift +7 -0
- package/ios/ExpoBeaconModule+Monitoring.swift +117 -6
- package/ios/ExpoBeaconModule+Permissions.swift +13 -33
- package/ios/ExpoBeaconModule+Scanning.swift +80 -9
- package/ios/ExpoBeaconModule+Storage.swift +66 -3
- package/ios/ExpoBeaconModule+Timers.swift +135 -110
- package/ios/ExpoBeaconModule.swift +242 -362
- package/package.json +1 -1
- package/plugin/build/index.d.ts.map +1 -1
- package/plugin/build/index.js +5 -13
- package/plugin/build/withBeaconAndroid.d.ts +8 -0
- package/plugin/build/withBeaconAndroid.d.ts.map +1 -1
- package/plugin/build/withBeaconAndroid.js +26 -6
- package/plugin/build/withBeaconIOS.d.ts +8 -0
- package/plugin/build/withBeaconIOS.d.ts.map +1 -1
- package/plugin/build/withBeaconIOS.js +32 -5
- package/src/ExpoBeacon.types.ts +21 -20
- package/src/ExpoBeaconModule.ts +39 -8
- package/src/ExpoBeaconModule.web.ts +86 -67
- package/src/hooks/useBeacon.ts +399 -0
- package/src/hooks/useCarPlay.ts +146 -0
- package/src/index.ts +14 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
8
8
|
| **Pair** | Register specific beacons for persistent tracking — survives app restarts |
|
|
9
9
|
| **Monitor** | Background enter/exit region detection with distance-based filtering |
|
|
10
10
|
| **Distance** | Real-time distance updates (~1/sec) while monitoring |
|
|
11
|
-
| **Timeout** | Fire a one-shot event after a beacon
|
|
11
|
+
| **Timeout** | Fire a one-shot event after a beacon has been out of range for a configured duration |
|
|
12
12
|
| **Event Logging** | Persist every beacon event to a local SQLite database for diagnostics & replay |
|
|
13
13
|
| **Notifications** | Automatic local notifications on region enter/exit, fully customisable |
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| **Android** | [AltBeacon](https://altbeacon.github.io/android-beacon-library/) library + Foreground Service |
|
|
18
18
|
| **iOS** | CoreLocation (iBeacon ranging & monitoring) + CoreBluetooth (Eddystone & wildcard BLE) |
|
|
19
|
-
| **Web** | Not supported (
|
|
19
|
+
| **Web** | Not supported (async methods reject, sync getters return inert defaults, everything else throws) |
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -27,6 +27,9 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
27
27
|
- [iOS](#ios)
|
|
28
28
|
- [Android](#android)
|
|
29
29
|
- [Quick Start](#quick-start)
|
|
30
|
+
- [React Hooks](#react-hooks)
|
|
31
|
+
- [useBeacon()](#usebeacon)
|
|
32
|
+
- [useCarPlay()](#usecarplay)
|
|
30
33
|
- [Usage Examples](#usage-examples)
|
|
31
34
|
- [Scanning for iBeacons](#scanning-for-ibeacons)
|
|
32
35
|
- [Scanning for Eddystone Beacons](#scanning-for-eddystone-beacons)
|
|
@@ -61,6 +64,7 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
61
64
|
- [getEventLogs()](#geteventlogsoptions)
|
|
62
65
|
- [clearEventLogs()](#cleareventlogs)
|
|
63
66
|
- [destroyEventLogs()](#destroyeventlogs)
|
|
67
|
+
- [setApiEndpoint()](#setapiendpointurl-apikey-id)
|
|
64
68
|
- [Events](#events)
|
|
65
69
|
- [TypeScript Types](#typescript-types)
|
|
66
70
|
- [Native Integrations](#native-integrations)
|
|
@@ -109,6 +113,8 @@ In Xcode under **Signing & Capabilities**, enable:
|
|
|
109
113
|
- **Background Modes → Location updates**
|
|
110
114
|
- **Background Modes → Uses Bluetooth LE accessories**
|
|
111
115
|
|
|
116
|
+
> When the bundled config plugin is installed (`"plugins": ["expo-beacon"]`), `location` is merged into `UIBackgroundModes` automatically on `expo prebuild` — the native module only enables background ranging when this mode is present.
|
|
117
|
+
|
|
112
118
|
#### Key iOS Constraints
|
|
113
119
|
|
|
114
120
|
- **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.
|
|
@@ -194,6 +200,125 @@ export default function App() {
|
|
|
194
200
|
|
|
195
201
|
---
|
|
196
202
|
|
|
203
|
+
## React Hooks
|
|
204
|
+
|
|
205
|
+
For React / React Native apps the package ships two hooks that wrap the
|
|
206
|
+
imperative API, manage event subscriptions (with automatic cleanup), and expose
|
|
207
|
+
the relevant state reactively. Import them directly from the package:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { useBeacon, useCarPlay } from "expo-beacon";
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Both hooks accept optional event callbacks. Callbacks are read from a ref, so
|
|
214
|
+
passing fresh inline functions on every render does **not** re-subscribe the
|
|
215
|
+
underlying native listeners.
|
|
216
|
+
|
|
217
|
+
### useBeacon()
|
|
218
|
+
|
|
219
|
+
Manages scanning and background monitoring. It keeps the paired-beacon lists,
|
|
220
|
+
the set of beacons currently in range, and the monitoring flag in sync, and
|
|
221
|
+
returns stable action wrappers.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { useBeacon } from "expo-beacon";
|
|
225
|
+
|
|
226
|
+
function BeaconScreen() {
|
|
227
|
+
const {
|
|
228
|
+
inRange,
|
|
229
|
+
isMonitoring,
|
|
230
|
+
pairedBeacons,
|
|
231
|
+
requestPermissions,
|
|
232
|
+
pairBeacon,
|
|
233
|
+
startMonitoring,
|
|
234
|
+
stopMonitoring,
|
|
235
|
+
} = useBeacon({
|
|
236
|
+
onBeaconEnter: (e) => console.log("entered", e.identifier, e.distance),
|
|
237
|
+
onBeaconExit: (e) => console.log("exited", e.identifier),
|
|
238
|
+
onError: (e) => console.warn(`[${e.code}] ${e.message}`),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<View>
|
|
243
|
+
<Button title="Grant permissions" onPress={requestPermissions} />
|
|
244
|
+
<Button
|
|
245
|
+
title={isMonitoring ? "Stop monitoring" : "Start monitoring"}
|
|
246
|
+
onPress={() => (isMonitoring ? stopMonitoring() : startMonitoring())}
|
|
247
|
+
/>
|
|
248
|
+
{inRange.map((b) => (
|
|
249
|
+
<Text key={b.identifier}>
|
|
250
|
+
{b.identifier} — {b.distance >= 0 ? `${b.distance.toFixed(1)}m` : "n/a"}
|
|
251
|
+
</Text>
|
|
252
|
+
))}
|
|
253
|
+
</View>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Returned value | Description |
|
|
259
|
+
| --- | --- |
|
|
260
|
+
| `pairedBeacons` / `pairedEddystones` | Reactive lists of paired devices. |
|
|
261
|
+
| `inRange` | Paired beacons currently in range, derived live from enter/exit/distance/timeout events (`InRangeBeacon[]`). |
|
|
262
|
+
| `isMonitoring` | Whether background monitoring is active. |
|
|
263
|
+
| `refreshPaired()` | Re-read the paired lists from native. |
|
|
264
|
+
| `pairBeacon()` / `unpairBeacon()` | Pair / unpair an iBeacon, then refresh. |
|
|
265
|
+
| `pairEddystone()` / `unpairEddystone()` | Pair / unpair an Eddystone, then refresh. |
|
|
266
|
+
| `scanForBeacons()` / `scanForEddystones()` | One-shot scans returning a promise. |
|
|
267
|
+
| `startContinuousScan()` / `stopContinuousScan()` | Live scan; results arrive via `onBeaconFound` / `onEddystoneFound`. |
|
|
268
|
+
| `cancelScan()` | Cancel an in-progress one-shot scan. |
|
|
269
|
+
| `startMonitoring()` / `stopMonitoring()` | Start / stop background monitoring. |
|
|
270
|
+
| `requestPermissions()` | Request the permissions needed for scanning / monitoring. |
|
|
271
|
+
|
|
272
|
+
`inRange` reflects **monitored (paired)** beacons only. Continuous-scan results
|
|
273
|
+
are delivered through the `onBeaconFound` / `onEddystoneFound` callbacks because
|
|
274
|
+
raw scan hits carry no paired identifier. Pass `track: false` to skip `inRange`
|
|
275
|
+
bookkeeping when you only need the callbacks.
|
|
276
|
+
|
|
277
|
+
### useCarPlay()
|
|
278
|
+
|
|
279
|
+
Observes CarPlay / Android Auto connection state. It initializes from the
|
|
280
|
+
persisted native state on mount and tracks live connect / disconnect events.
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import { useCarPlay } from "expo-beacon";
|
|
284
|
+
|
|
285
|
+
function CarPlayBadge() {
|
|
286
|
+
const { connected, transport, isMonitoring, startMonitoring, stopMonitoring } =
|
|
287
|
+
useCarPlay({
|
|
288
|
+
onConnected: (e) => console.log("car connected via", e.transport),
|
|
289
|
+
onDisconnected: () => console.log("car disconnected"),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<View>
|
|
294
|
+
<Text>{connected ? `Connected (${transport})` : "Not connected"}</Text>
|
|
295
|
+
<Button
|
|
296
|
+
title={isMonitoring ? "Stop" : "Start"}
|
|
297
|
+
onPress={() => (isMonitoring ? stopMonitoring() : startMonitoring())}
|
|
298
|
+
/>
|
|
299
|
+
</View>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Returned value | Description |
|
|
305
|
+
| --- | --- |
|
|
306
|
+
| `connected` | Whether a CarPlay / Android Auto session is active. |
|
|
307
|
+
| `transport` | Transport of the active session (`CarPlayTransport`), or `null`. |
|
|
308
|
+
| `isMonitoring` | Whether persistent monitoring is enabled. |
|
|
309
|
+
| `lastConnectedAt` / `lastDisconnectedAt` | Epoch-ms timestamps of the last transitions, or `null`. |
|
|
310
|
+
| `startMonitoring()` / `stopMonitoring()` | Enable / disable monitoring. |
|
|
311
|
+
| `refresh()` | Re-read the connection + monitoring state from native. |
|
|
312
|
+
| `getDiagnostics()` | Fetch detection diagnostics for troubleshooting. |
|
|
313
|
+
|
|
314
|
+
Pass `autoStart: true` to call `startCarPlayMonitoring()` on mount when it is
|
|
315
|
+
not already enabled.
|
|
316
|
+
|
|
317
|
+
> Both hooks are safe to call on web: the underlying module is a no-op stub
|
|
318
|
+
> there, so the hooks simply report empty / disconnected state.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
197
322
|
## Usage Examples
|
|
198
323
|
|
|
199
324
|
### Scanning for iBeacons
|
|
@@ -220,7 +345,8 @@ beacons.forEach((b) => {
|
|
|
220
345
|
#### Wildcard scan (Android only)
|
|
221
346
|
|
|
222
347
|
```ts
|
|
223
|
-
// Pass an empty array
|
|
348
|
+
// Pass an empty array (or omit the arguments — defaults are uuids = [],
|
|
349
|
+
// scanDuration = 5000) to discover ALL nearby iBeacons.
|
|
224
350
|
// On iOS, this auto-uses UUIDs from paired beacons
|
|
225
351
|
const beacons = await ExpoBeacon.scanForBeaconsAsync([], 5000);
|
|
226
352
|
```
|
|
@@ -567,7 +693,7 @@ await ExpoBeacon.startMonitoring({
|
|
|
567
693
|
|
|
568
694
|
### Beacon Timeout
|
|
569
695
|
|
|
570
|
-
Pair a beacon with `timeoutSeconds` to fire a one-shot event after the beacon has been
|
|
696
|
+
Pair a beacon with `timeoutSeconds` to fire a one-shot event after the beacon has been out of range for that duration. The countdown is armed when the beacon exits range (or when no BLE readings arrive for 60 seconds, e.g. due to Doze mode or background throttling) and is cancelled if the beacon is seen again before it fires.
|
|
571
697
|
|
|
572
698
|
```tsx
|
|
573
699
|
import { useEffect } from "react";
|
|
@@ -581,7 +707,7 @@ ExpoBeacon.pairBeacon(
|
|
|
581
707
|
1,
|
|
582
708
|
100,
|
|
583
709
|
undefined, // name (optional)
|
|
584
|
-
30, // timeoutSeconds — fires
|
|
710
|
+
30, // timeoutSeconds — fires 30 s after the beacon leaves range
|
|
585
711
|
);
|
|
586
712
|
|
|
587
713
|
// Pair Eddystone with a 60-second timeout
|
|
@@ -590,7 +716,7 @@ ExpoBeacon.pairEddystone(
|
|
|
590
716
|
"edd1ebeac04e5defa017",
|
|
591
717
|
"0123456789ab",
|
|
592
718
|
undefined, // name (optional)
|
|
593
|
-
60, // timeoutSeconds — fires
|
|
719
|
+
60, // timeoutSeconds — fires 60 s after the beacon leaves range
|
|
594
720
|
);
|
|
595
721
|
|
|
596
722
|
// Listen for the timeout events
|
|
@@ -598,13 +724,13 @@ useEffect(() => {
|
|
|
598
724
|
const beaconTimeout = ExpoBeacon.addListener(
|
|
599
725
|
"onBeaconTimeout",
|
|
600
726
|
(e: BeaconTimeoutEvent) => {
|
|
601
|
-
console.log(`Beacon "${e.identifier}"
|
|
727
|
+
console.log(`Beacon "${e.identifier}" out of range for configured duration!`);
|
|
602
728
|
},
|
|
603
729
|
);
|
|
604
730
|
const eddystoneTimeout = ExpoBeacon.addListener(
|
|
605
731
|
"onEddystoneTimeout",
|
|
606
732
|
(e: EddystoneTimeoutEvent) => {
|
|
607
|
-
console.log(`Eddystone "${e.identifier}"
|
|
733
|
+
console.log(`Eddystone "${e.identifier}" out of range for configured duration!`);
|
|
608
734
|
},
|
|
609
735
|
);
|
|
610
736
|
|
|
@@ -615,7 +741,7 @@ useEffect(() => {
|
|
|
615
741
|
}, []);
|
|
616
742
|
```
|
|
617
743
|
|
|
618
|
-
> **Note**: The timeout fires once per
|
|
744
|
+
> **Note**: The timeout fires once per exit. If the beacon re-enters range before the countdown completes, the pending timer is cancelled and re-armed on the next exit.
|
|
619
745
|
|
|
620
746
|
---
|
|
621
747
|
|
|
@@ -774,13 +900,11 @@ If you need to disable this (e.g. you already ship your own Android Auto templat
|
|
|
774
900
|
If `onCarPlayConnected` never fires on Android, call `getCarPlayDiagnostics()` to inspect the native state:
|
|
775
901
|
|
|
776
902
|
```ts
|
|
777
|
-
const d =
|
|
903
|
+
const d = ExpoBeacon.getCarPlayDiagnostics();
|
|
778
904
|
// {
|
|
779
|
-
// platform: "android",
|
|
780
|
-
// carPlayEnabled: true,
|
|
781
905
|
// isCarAppMetadataPresent: true, // false → config plugin didn't run; prebuild again
|
|
782
906
|
// isCarProviderQueryable: true, // false → Android Auto app not installed on device
|
|
783
|
-
// lastRawConnectionType:
|
|
907
|
+
// lastRawConnectionType: 2, // 0=NOT_CONNECTED 1=NATIVE (AAOS) 2=PROJECTION; null = no value yet
|
|
784
908
|
// observerActive: true,
|
|
785
909
|
// serviceAlive: true,
|
|
786
910
|
// }
|
|
@@ -802,8 +926,8 @@ Requests all permissions required for scanning and monitoring.
|
|
|
802
926
|
|
|
803
927
|
| Platform | Permissions Requested |
|
|
804
928
|
|---|---|
|
|
805
|
-
| **Android** | `
|
|
806
|
-
| **iOS** | `CLLocationManager` "When In Use"
|
|
929
|
+
| **Android** | `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`, `BLUETOOTH_SCAN` + `BLUETOOTH_CONNECT` (API 31+), `POST_NOTIFICATIONS` (API 33+), then `ACCESS_BACKGROUND_LOCATION` (API 29+) in a second prompt. Resolves `true` only when background location is granted. |
|
|
930
|
+
| **iOS** | `CLLocationManager` "When In Use" authorization — resolves `true` once granted. The "Always" upgrade is requested later by `startMonitoring()`, and Bluetooth permission is not prompted here. |
|
|
807
931
|
|
|
808
932
|
**Returns**: `true` if all required permissions were granted.
|
|
809
933
|
|
|
@@ -826,6 +950,8 @@ scanForBeaconsAsync(uuids?: string[], scanDurationMs?: number): Promise<BeaconSc
|
|
|
826
950
|
|
|
827
951
|
Performs a **one-shot iBeacon scan**. Waits for the specified duration, then resolves with all discovered beacons.
|
|
828
952
|
|
|
953
|
+
Both parameters are optional — the defaults are applied on the JS side before the native call.
|
|
954
|
+
|
|
829
955
|
| Parameter | Type | Default | Description |
|
|
830
956
|
|---|---|---|---|
|
|
831
957
|
| `uuids` | `string[]` | `[]` | Proximity UUIDs to filter by. See platform differences below. |
|
|
@@ -866,6 +992,8 @@ scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>
|
|
|
866
992
|
|
|
867
993
|
Performs a **one-shot Eddystone scan** using BLE. Discovers both Eddystone-UID and Eddystone-URL frames.
|
|
868
994
|
|
|
995
|
+
The parameter is optional — the default is applied on the JS side before the native call.
|
|
996
|
+
|
|
869
997
|
| Parameter | Type | Default | Description |
|
|
870
998
|
|---|---|---|---|
|
|
871
999
|
| `scanDurationMs` | `number` | `5000` | Scan duration in milliseconds (must be > 0). |
|
|
@@ -932,19 +1060,19 @@ Registers an iBeacon for persistent monitoring.
|
|
|
932
1060
|
|
|
933
1061
|
| Parameter | Type | Description |
|
|
934
1062
|
|---|---|---|
|
|
935
|
-
| `identifier` | `string` | Unique label (e.g. `"lobby-entrance"`). Re-using an identifier replaces the previous entry. |
|
|
1063
|
+
| `identifier` | `string` | Unique label (e.g. `"lobby-entrance"`). Re-using an iBeacon identifier replaces the previous entry. |
|
|
936
1064
|
| `uuid` | `string` | iBeacon proximity UUID (case-insensitive, e.g. `"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"`) |
|
|
937
1065
|
| `major` | `number` | Major value: `0`–`65535` |
|
|
938
1066
|
| `minor` | `number` | Minor value: `0`–`65535` |
|
|
939
1067
|
| `name` | `string?` | Optional BLE device name for display purposes |
|
|
940
|
-
| `timeoutSeconds` | `number?` | Fire `onBeaconTimeout` once after the beacon
|
|
1068
|
+
| `timeoutSeconds` | `number?` | Fire `onBeaconTimeout` once, this many seconds after the beacon exits range. Cancelled if the beacon is seen again first. |
|
|
941
1069
|
|
|
942
|
-
**Possible errors**: `INVALID_UUID`, `INVALID_MAJOR`, `INVALID_MINOR
|
|
1070
|
+
**Possible errors**: `INVALID_UUID`, `INVALID_MAJOR`, `INVALID_MINOR`, `DUPLICATE_IDENTIFIER` (identifier already used by a paired Eddystone).
|
|
943
1071
|
|
|
944
1072
|
```ts
|
|
945
1073
|
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42);
|
|
946
1074
|
|
|
947
|
-
// With timeout — fires onBeaconTimeout
|
|
1075
|
+
// With timeout — fires onBeaconTimeout 30 s after the beacon leaves range
|
|
948
1076
|
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42, undefined, 30);
|
|
949
1077
|
```
|
|
950
1078
|
|
|
@@ -989,22 +1117,22 @@ const paired = ExpoBeacon.getPairedBeacons();
|
|
|
989
1117
|
pairEddystone(identifier: string, namespace: string, instance: string, name?: string, timeoutSeconds?: number): void
|
|
990
1118
|
```
|
|
991
1119
|
|
|
992
|
-
Registers an Eddystone-UID beacon for persistent monitoring.
|
|
1120
|
+
Registers an Eddystone-UID beacon for persistent monitoring. The namespace and instance are normalized to lowercase before storage.
|
|
993
1121
|
|
|
994
1122
|
| Parameter | Type | Description |
|
|
995
1123
|
|---|---|---|
|
|
996
|
-
| `identifier` | `string` | Unique label (e.g. `"meeting-room"`) |
|
|
1124
|
+
| `identifier` | `string` | Unique label (e.g. `"meeting-room"`). Re-using an Eddystone identifier replaces the previous entry. |
|
|
997
1125
|
| `namespace` | `string` | 10-byte namespace ID as hex string — must be exactly **20 hex characters** |
|
|
998
1126
|
| `instance` | `string` | 6-byte instance ID as hex string — must be exactly **12 hex characters** |
|
|
999
1127
|
| `name` | `string?` | Optional BLE device name for display purposes |
|
|
1000
|
-
| `timeoutSeconds` | `number?` | Fire `onEddystoneTimeout` once after the beacon
|
|
1128
|
+
| `timeoutSeconds` | `number?` | Fire `onEddystoneTimeout` once, this many seconds after the beacon exits range. Cancelled if the beacon is seen again first. |
|
|
1001
1129
|
|
|
1002
|
-
**Possible errors**: `INVALID_NAMESPACE`, `INVALID_INSTANCE
|
|
1130
|
+
**Possible errors**: `INVALID_NAMESPACE`, `INVALID_INSTANCE`, `DUPLICATE_IDENTIFIER` (identifier already used by a paired iBeacon).
|
|
1003
1131
|
|
|
1004
1132
|
```ts
|
|
1005
1133
|
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab");
|
|
1006
1134
|
|
|
1007
|
-
// With timeout — fires onEddystoneTimeout
|
|
1135
|
+
// With timeout — fires onEddystoneTimeout 60 s after the beacon leaves range
|
|
1008
1136
|
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab", undefined, 60);
|
|
1009
1137
|
```
|
|
1010
1138
|
|
|
@@ -1101,7 +1229,7 @@ await ExpoBeacon.startMonitoring();
|
|
|
1101
1229
|
stopMonitoring(): Promise<void>
|
|
1102
1230
|
```
|
|
1103
1231
|
|
|
1104
|
-
Stops all background monitoring. On Android, stops the foreground service.
|
|
1232
|
+
Stops all background monitoring. On Android, stops the foreground service. Persisted monitoring options (`maxDistance`, `exitDistance`, `level`, `exitTimeoutSeconds`, …) are cleared on both platforms.
|
|
1105
1233
|
|
|
1106
1234
|
```ts
|
|
1107
1235
|
await ExpoBeacon.stopMonitoring();
|
|
@@ -1271,6 +1399,24 @@ ExpoBeacon.destroyEventLogs();
|
|
|
1271
1399
|
|
|
1272
1400
|
---
|
|
1273
1401
|
|
|
1402
|
+
### `setApiEndpoint(url, apiKey?, id?)`
|
|
1403
|
+
|
|
1404
|
+
```ts
|
|
1405
|
+
setApiEndpoint(url: string, apiKey?: string, id?: string): void
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
Configures a remote endpoint to which native code POSTs every beacon event — delivery works even when the JS bridge is not active (app backgrounded). The configuration persists until changed.
|
|
1409
|
+
|
|
1410
|
+
| Parameter | Type | Description |
|
|
1411
|
+
|---|---|---|
|
|
1412
|
+
| `url` | `string` | The API endpoint URL to POST events to. |
|
|
1413
|
+
| `apiKey` | `string?` | Sent as the `X-CSFR-Token` header (sic — the header is literally `X-CSFR-Token`, not `X-CSRF-Token`). |
|
|
1414
|
+
| `id` | `string?` | Identifier appended to every forwarded event payload. |
|
|
1415
|
+
|
|
1416
|
+
Use `getApiEndpoint()` to read back the current configuration (each field is `null` if unset).
|
|
1417
|
+
|
|
1418
|
+
---
|
|
1419
|
+
|
|
1274
1420
|
## Events
|
|
1275
1421
|
|
|
1276
1422
|
Subscribe with `ExpoBeacon.addListener(eventName, handler)`. Always call `.remove()` on the returned subscription during cleanup.
|
|
@@ -1293,8 +1439,8 @@ sub.remove();
|
|
|
1293
1439
|
| `onEddystoneEnter` | Paired Eddystone enters range (respects `maxDistance`) | `EddystoneRegionEvent` |
|
|
1294
1440
|
| `onEddystoneExit` | Paired Eddystone leaves range (always fires) | `EddystoneRegionEvent` |
|
|
1295
1441
|
| `onEddystoneDistance` | Periodic Eddystone distance update during monitoring | `EddystoneDistanceEvent` |
|
|
1296
|
-
| `onBeaconTimeout` | Paired iBeacon
|
|
1297
|
-
| `onEddystoneTimeout` | Paired Eddystone
|
|
1442
|
+
| `onBeaconTimeout` | Paired iBeacon out of range for configured `timeoutSeconds` | `BeaconTimeoutEvent` |
|
|
1443
|
+
| `onEddystoneTimeout` | Paired Eddystone out of range for configured `timeoutSeconds` | `EddystoneTimeoutEvent` |
|
|
1298
1444
|
|
|
1299
1445
|
### Event Detail
|
|
1300
1446
|
|
|
@@ -1391,25 +1537,25 @@ ExpoBeacon.addListener("onEddystoneDistance", (e) => {
|
|
|
1391
1537
|
|
|
1392
1538
|
#### `onBeaconTimeout`
|
|
1393
1539
|
|
|
1394
|
-
Fired **once
|
|
1540
|
+
Fired **once**, `timeoutSeconds` after a paired iBeacon exits range (or after BLE readings stop for 60 s). Re-detection cancels the pending timer.
|
|
1395
1541
|
|
|
1396
1542
|
```ts
|
|
1397
1543
|
ExpoBeacon.addListener("onBeaconTimeout", (e) => {
|
|
1398
1544
|
// e.identifier — "lobby-entrance"
|
|
1399
1545
|
// e.uuid, e.major, e.minor — beacon identity
|
|
1400
|
-
// e.distance —
|
|
1401
|
-
console.log(`Beacon "${e.identifier}" timeout —
|
|
1546
|
+
// e.distance — usually –1 (the beacon is out of range when this fires)
|
|
1547
|
+
console.log(`Beacon "${e.identifier}" timeout — out of range for configured duration`);
|
|
1402
1548
|
});
|
|
1403
1549
|
```
|
|
1404
1550
|
|
|
1405
1551
|
#### `onEddystoneTimeout`
|
|
1406
1552
|
|
|
1407
|
-
Fired **once
|
|
1553
|
+
Fired **once**, `timeoutSeconds` after a paired Eddystone exits range (or after BLE readings stop for 60 s). Re-detection cancels the pending timer.
|
|
1408
1554
|
|
|
1409
1555
|
```ts
|
|
1410
1556
|
ExpoBeacon.addListener("onEddystoneTimeout", (e) => {
|
|
1411
1557
|
// e.identifier, e.namespace, e.instance — Eddystone identity
|
|
1412
|
-
// e.distance —
|
|
1558
|
+
// e.distance — usually –1 (the beacon is out of range when this fires)
|
|
1413
1559
|
console.log(`Eddystone "${e.identifier}" timeout`);
|
|
1414
1560
|
});
|
|
1415
1561
|
```
|
|
@@ -1433,6 +1579,7 @@ import type {
|
|
|
1433
1579
|
EddystoneRegionEvent,
|
|
1434
1580
|
EddystoneDistanceEvent,
|
|
1435
1581
|
EddystoneTimeoutEvent,
|
|
1582
|
+
BeaconErrorEvent,
|
|
1436
1583
|
ExpoBeaconModuleEvents,
|
|
1437
1584
|
MonitoringOptions,
|
|
1438
1585
|
NotificationConfig,
|
|
@@ -1454,8 +1601,8 @@ type BeaconScanResult = {
|
|
|
1454
1601
|
major: number; // 0–65535
|
|
1455
1602
|
minor: number; // 0–65535
|
|
1456
1603
|
rssi: number; // Signal strength in dBm (negative, e.g. –65)
|
|
1457
|
-
distance: number; // Estimated distance in metres
|
|
1458
|
-
txPower: number; // Calibrated TX power
|
|
1604
|
+
distance: number; // Estimated distance in metres (–1 when unavailable)
|
|
1605
|
+
txPower: number; // Calibrated TX power. Android only — always 0 on iOS (CoreLocation does not expose it)
|
|
1459
1606
|
};
|
|
1460
1607
|
```
|
|
1461
1608
|
|
|
@@ -1470,7 +1617,7 @@ type PairedBeacon = {
|
|
|
1470
1617
|
major: number;
|
|
1471
1618
|
minor: number;
|
|
1472
1619
|
name?: string; // Optional BLE device name
|
|
1473
|
-
timeoutSeconds?: number; // Fires onBeaconTimeout after
|
|
1620
|
+
timeoutSeconds?: number; // Fires onBeaconTimeout this many seconds after the beacon exits range
|
|
1474
1621
|
};
|
|
1475
1622
|
```
|
|
1476
1623
|
|
|
@@ -1529,7 +1676,7 @@ type PairedEddystone = {
|
|
|
1529
1676
|
namespace: string; // 20 hex chars
|
|
1530
1677
|
instance: string; // 12 hex chars
|
|
1531
1678
|
name?: string; // Optional BLE device name
|
|
1532
|
-
timeoutSeconds?: number; // Fires onEddystoneTimeout after
|
|
1679
|
+
timeoutSeconds?: number; // Fires onEddystoneTimeout this many seconds after the beacon exits range
|
|
1533
1680
|
};
|
|
1534
1681
|
```
|
|
1535
1682
|
|
|
@@ -1670,7 +1817,7 @@ type BeaconTimeoutEvent = {
|
|
|
1670
1817
|
uuid: string;
|
|
1671
1818
|
major: number;
|
|
1672
1819
|
minor: number;
|
|
1673
|
-
distance: number; //
|
|
1820
|
+
distance: number; // Usually –1 (the beacon is out of range when the timeout fires)
|
|
1674
1821
|
};
|
|
1675
1822
|
```
|
|
1676
1823
|
|
|
@@ -1683,7 +1830,7 @@ type EddystoneTimeoutEvent = {
|
|
|
1683
1830
|
identifier: string;
|
|
1684
1831
|
namespace: string;
|
|
1685
1832
|
instance: string;
|
|
1686
|
-
distance: number; //
|
|
1833
|
+
distance: number; // Usually –1 (the beacon is out of range when the timeout fires)
|
|
1687
1834
|
};
|
|
1688
1835
|
```
|
|
1689
1836
|
|
|
@@ -1737,13 +1884,13 @@ Follow [react-native-background-geolocation's native setup](https://transistorso
|
|
|
1737
1884
|
|
|
1738
1885
|
#### 2. Add the Expo config plugin
|
|
1739
1886
|
|
|
1740
|
-
In `app.json` (or `app.config.js`), add `expo-beacon
|
|
1887
|
+
In `app.json` (or `app.config.js`), add `expo-beacon` to your plugins list:
|
|
1741
1888
|
|
|
1742
1889
|
```json
|
|
1743
1890
|
{
|
|
1744
1891
|
"expo": {
|
|
1745
1892
|
"plugins": [
|
|
1746
|
-
"expo-beacon
|
|
1893
|
+
"expo-beacon"
|
|
1747
1894
|
]
|
|
1748
1895
|
}
|
|
1749
1896
|
}
|
|
@@ -1755,7 +1902,28 @@ Then run prebuild to apply the native changes:
|
|
|
1755
1902
|
npx expo prebuild --clean
|
|
1756
1903
|
```
|
|
1757
1904
|
|
|
1758
|
-
The plugin writes `BeaconGeoPlugin.swift` / `BeaconGeoPlugin.kt` into your native project and wires them up in `AppDelegate.swift` and `MainApplication.kt` automatically.
|
|
1905
|
+
The plugin writes `BeaconGeoPlugin.swift` / `BeaconGeoPlugin.kt` into your native project and wires them up in `AppDelegate.swift` and `MainApplication.kt` automatically. It also merges `location` into the iOS `UIBackgroundModes` (required for background ranging) and registers the app as Android-Auto-aware (see [CarPlay / Android Auto Detection](#carplay--android-auto-detection)).
|
|
1906
|
+
|
|
1907
|
+
> **Java projects**: the `MainApplication` patch is Kotlin-only. If your project still uses `MainApplication.java`, the plugin skips the patch and you must add `BeaconPluginRegistry.register(BeaconGeoPlugin(this))` manually.
|
|
1908
|
+
|
|
1909
|
+
##### The `backgroundGeolocation` prop
|
|
1910
|
+
|
|
1911
|
+
The `BeaconGeoPlugin` generation requires `react-native-background-geolocation` to be installed — without it, the generated native files fail to compile. If you don't use that library, disable the integration with the `backgroundGeolocation` prop (default: `true`):
|
|
1912
|
+
|
|
1913
|
+
```json
|
|
1914
|
+
{
|
|
1915
|
+
"expo": {
|
|
1916
|
+
"plugins": [
|
|
1917
|
+
["expo-beacon", {
|
|
1918
|
+
"ios": { "backgroundGeolocation": false },
|
|
1919
|
+
"android": { "backgroundGeolocation": false }
|
|
1920
|
+
}]
|
|
1921
|
+
]
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
```
|
|
1925
|
+
|
|
1926
|
+
Setting it to `false` skips the `BeaconGeoPlugin.swift` / `BeaconGeoPlugin.kt` generation and the `AppDelegate.swift` / `MainApplication.kt` patches. The `UIBackgroundModes` merge and the Android Auto registration are applied regardless of this prop.
|
|
1759
1927
|
|
|
1760
1928
|
#### 3. Configure BGLocation once at JS startup
|
|
1761
1929
|
|
|
@@ -1796,18 +1964,42 @@ import ExpoBeacon
|
|
|
1796
1964
|
import TSLocationManager
|
|
1797
1965
|
|
|
1798
1966
|
final class BeaconGeoPlugin: BeaconLifecycleDelegate {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1967
|
+
private func startTracking() {
|
|
1968
|
+
BackgroundGeolocation.sharedInstance().start()
|
|
1969
|
+
BackgroundGeolocation.sharedInstance().changePace(true)
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
private func stopTracking() {
|
|
1973
|
+
BackgroundGeolocation.sharedInstance().changePace(false)
|
|
1974
|
+
BackgroundGeolocation.sharedInstance().stop()
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
func beaconDidEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
1978
|
+
startTracking()
|
|
1979
|
+
}
|
|
1980
|
+
func beaconDidExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
1981
|
+
stopTracking()
|
|
1982
|
+
}
|
|
1983
|
+
func beaconDidTimeout(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
1984
|
+
stopTracking()
|
|
1985
|
+
}
|
|
1986
|
+
func eddystoneDidEnter(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
1987
|
+
startTracking()
|
|
1988
|
+
}
|
|
1989
|
+
func eddystoneDidExit(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
1990
|
+
stopTracking()
|
|
1991
|
+
}
|
|
1992
|
+
func eddystoneDidTimeout(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
1993
|
+
stopTracking()
|
|
1994
|
+
}
|
|
1995
|
+
// Start tracking when the device connects to CarPlay (wired or wireless),
|
|
1996
|
+
// stop when it disconnects.
|
|
1997
|
+
func carPlayDidConnect(transport: String) {
|
|
1998
|
+
startTracking()
|
|
1999
|
+
}
|
|
2000
|
+
func carPlayDidDisconnect() {
|
|
2001
|
+
stopTracking()
|
|
2002
|
+
}
|
|
1811
2003
|
}
|
|
1812
2004
|
```
|
|
1813
2005
|
|
|
@@ -1828,19 +2020,35 @@ package com.yourapp
|
|
|
1828
2020
|
|
|
1829
2021
|
import android.content.Context
|
|
1830
2022
|
import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation
|
|
2023
|
+
import com.transistorsoft.locationmanager.adapter.callback.TSCallback
|
|
1831
2024
|
import expo.modules.beacon.BeaconEventPlugin
|
|
1832
2025
|
|
|
1833
2026
|
class BeaconGeoPlugin(ctx: Context) : BeaconEventPlugin {
|
|
1834
2027
|
private val bgGeo = BackgroundGeolocation.getInstance(ctx, null)
|
|
2028
|
+
private val noOp = object : TSCallback {
|
|
2029
|
+
override fun onSuccess() {}
|
|
2030
|
+
override fun onFailure(error: String) {}
|
|
2031
|
+
}
|
|
1835
2032
|
|
|
1836
2033
|
override fun onBeaconEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
1837
|
-
bgGeo.start(
|
|
2034
|
+
bgGeo.start(noOp)
|
|
1838
2035
|
override fun onBeaconExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
1839
|
-
bgGeo.stop(
|
|
2036
|
+
bgGeo.stop(noOp)
|
|
2037
|
+
override fun onBeaconTimeout(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
2038
|
+
bgGeo.stop(noOp)
|
|
1840
2039
|
override fun onEddystoneEnter(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
1841
|
-
bgGeo.start(
|
|
2040
|
+
bgGeo.start(noOp)
|
|
1842
2041
|
override fun onEddystoneExit(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
1843
|
-
bgGeo.stop(
|
|
2042
|
+
bgGeo.stop(noOp)
|
|
2043
|
+
override fun onEddystoneTimeout(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
2044
|
+
bgGeo.stop(noOp)
|
|
2045
|
+
// Start tracking when the device connects to Android Auto, stop when it disconnects.
|
|
2046
|
+
override fun onCarPlayConnected(transport: String) {
|
|
2047
|
+
bgGeo.start(noOp)
|
|
2048
|
+
}
|
|
2049
|
+
override fun onCarPlayDisconnected() {
|
|
2050
|
+
bgGeo.stop(noOp)
|
|
2051
|
+
}
|
|
1844
2052
|
}
|
|
1845
2053
|
```
|
|
1846
2054
|
|
|
@@ -1878,7 +2086,7 @@ override fun onCreate() {
|
|
|
1878
2086
|
|---|---|
|
|
1879
2087
|
| Region monitoring | iOS wakes/relaunches the app on region boundary crossings — even if force-quit. |
|
|
1880
2088
|
| BLE scanning | Eddystones are monitored via CoreBluetooth. Works reliably in foreground; may be throttled when the app is suspended. |
|
|
1881
|
-
| Background modes | `allowsBackgroundLocationUpdates
|
|
2089
|
+
| Background modes | `allowsBackgroundLocationUpdates` is only enabled when `UIBackgroundModes` contains `location` (the config plugin adds it on prebuild); `pausesLocationUpdatesAutomatically = false` |
|
|
1882
2090
|
| Region limit | 20 simultaneous `CLBeaconRegion` registrations max. Eddystones don't count. |
|
|
1883
2091
|
|
|
1884
2092
|
---
|
|
@@ -1915,7 +2123,7 @@ Both the foreground service and enter/exit alerts share the channel ID `expo_bea
|
|
|
1915
2123
|
|
|
1916
2124
|
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.
|
|
1917
2125
|
|
|
1918
|
-
2. **Two-step location permission**: iOS requires requesting "When In Use" first, then upgrading to "Always".
|
|
2126
|
+
2. **Two-step location permission**: iOS requires requesting "When In Use" first, then upgrading to "Always". `requestPermissionsAsync()` requests (and resolves `true` with) "When In Use"; the "Always" upgrade prompt is triggered by `startMonitoring()`.
|
|
1919
2127
|
|
|
1920
2128
|
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.
|
|
1921
2129
|
|
|
@@ -1995,6 +2203,7 @@ The module uses hysteresis (3 consecutive readings) to prevent jitter. If you're
|
|
|
1995
2203
|
| `INVALID_MINOR` | `pairBeacon` | Minor value not in range 0–65535. |
|
|
1996
2204
|
| `INVALID_NAMESPACE` | `pairEddystone` | Namespace must be exactly 20 hex characters. |
|
|
1997
2205
|
| `INVALID_INSTANCE` | `pairEddystone` | Instance must be exactly 12 hex characters. |
|
|
2206
|
+
| `DUPLICATE_IDENTIFIER` | `pairBeacon`, `pairEddystone` | The identifier is already used by a paired beacon of the other type. |
|
|
1998
2207
|
| `PERMISSION_DENIED` | `scanForBeaconsAsync`, `startMonitoring` | Required permissions were not granted. |
|
|
1999
2208
|
| `WILDCARD_NOT_SUPPORTED` | `scanForBeaconsAsync` | iOS only: no UUIDs provided and no paired beacons exist. |
|
|
2000
2209
|
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
|
12
12
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
|
13
13
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
14
|
-
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
|
15
|
-
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
16
14
|
|
|
17
15
|
<!-- Location permissions required for BLE scanning -->
|
|
18
16
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|