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.
Files changed (42) hide show
  1. package/README.md +514 -0
  2. package/android/build.gradle +23 -0
  3. package/android/src/main/AndroidManifest.xml +57 -0
  4. package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +41 -0
  5. package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +300 -0
  6. package/android/src/main/java/expo/modules/beacon/BootReceiver.kt +18 -0
  7. package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +329 -0
  8. package/build/ExpoBeacon.types.d.ts +53 -0
  9. package/build/ExpoBeacon.types.d.ts.map +1 -0
  10. package/build/ExpoBeacon.types.js +2 -0
  11. package/build/ExpoBeacon.types.js.map +1 -0
  12. package/build/ExpoBeaconModule.d.ts +46 -0
  13. package/build/ExpoBeaconModule.d.ts.map +1 -0
  14. package/build/ExpoBeaconModule.js +3 -0
  15. package/build/ExpoBeaconModule.js.map +1 -0
  16. package/build/ExpoBeaconModule.web.d.ts +16 -0
  17. package/build/ExpoBeaconModule.web.d.ts.map +1 -0
  18. package/build/ExpoBeaconModule.web.js +18 -0
  19. package/build/ExpoBeaconModule.web.js.map +1 -0
  20. package/build/ExpoBeaconView.d.ts +2 -0
  21. package/build/ExpoBeaconView.d.ts.map +1 -0
  22. package/build/ExpoBeaconView.js +2 -0
  23. package/build/ExpoBeaconView.js.map +1 -0
  24. package/build/ExpoBeaconView.web.d.ts +2 -0
  25. package/build/ExpoBeaconView.web.d.ts.map +1 -0
  26. package/build/ExpoBeaconView.web.js +2 -0
  27. package/build/ExpoBeaconView.web.js.map +1 -0
  28. package/build/index.d.ts +3 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +3 -0
  31. package/build/index.js.map +1 -0
  32. package/expo-module.config.json +9 -0
  33. package/ios/ExpoBeacon.podspec +32 -0
  34. package/ios/ExpoBeaconModule.swift +432 -0
  35. package/ios/ExpoBeaconView.swift +5 -0
  36. package/package.json +67 -0
  37. package/src/ExpoBeacon.types.ts +57 -0
  38. package/src/ExpoBeaconModule.ts +64 -0
  39. package/src/ExpoBeaconModule.web.ts +31 -0
  40. package/src/ExpoBeaconView.tsx +2 -0
  41. package/src/ExpoBeaconView.web.tsx +2 -0
  42. 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
+ }