expo-beacon 0.7.2 → 0.7.4
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 +144 -0
- package/android/src/main/java/expo/modules/beacon/BeaconEventPlugin.kt +17 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +15 -0
- package/android/src/main/java/expo/modules/beacon/BeaconPluginRegistry.kt +61 -0
- package/ios/BeaconLifecycleDelegate.swift +54 -0
- package/ios/ExpoBeaconModule.swift +44 -0
- package/package.json +6 -3
- package/plugin/build/withBeaconBGLocation.d.ts +4 -0
- package/plugin/build/withBeaconBGLocation.d.ts.map +1 -0
- package/plugin/build/withBeaconBGLocation.js +197 -0
package/README.md
CHANGED
|
@@ -63,6 +63,8 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
63
63
|
- [destroyEventLogs()](#destroyeventlogs)
|
|
64
64
|
- [Events](#events)
|
|
65
65
|
- [TypeScript Types](#typescript-types)
|
|
66
|
+
- [Native Integrations](#native-integrations)
|
|
67
|
+
- [react-native-background-geolocation](#react-native-background-geolocation)
|
|
66
68
|
- [Background Behaviour](#background-behaviour)
|
|
67
69
|
- [Notifications](#notifications)
|
|
68
70
|
- [Platform-Specific Notes & Gotchas](#platform-specific-notes--gotchas)
|
|
@@ -1610,6 +1612,148 @@ type EventLogEntry = {
|
|
|
1610
1612
|
|
|
1611
1613
|
---
|
|
1612
1614
|
|
|
1615
|
+
## Native Integrations
|
|
1616
|
+
|
|
1617
|
+
Dispatching work in response to beacon enter/exit events can be done at the native level, before the JS bridge is involved. expo-beacon exposes a plugin registry on both platforms for this purpose.
|
|
1618
|
+
|
|
1619
|
+
When a plugin is registered, `onBeaconEnter` / `onBeaconExit` (and their Eddystone equivalents) are called synchronously inside the same choke point that fires the JS event — so the native side-effect is guaranteed even when the JS thread is sleeping.
|
|
1620
|
+
|
|
1621
|
+
### react-native-background-geolocation
|
|
1622
|
+
|
|
1623
|
+
This integration starts BGLocation when any beacon is entered and stops it when all beacons are exited.
|
|
1624
|
+
|
|
1625
|
+
> **Requirement**: bare workflow or `npx expo prebuild`. Does not work with Expo Go.
|
|
1626
|
+
|
|
1627
|
+
#### 1. Install packages
|
|
1628
|
+
|
|
1629
|
+
```sh
|
|
1630
|
+
npx expo install expo-beacon react-native-background-geolocation
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
Follow [react-native-background-geolocation's native setup](https://transistorsoft.github.io/react-native-background-geolocation) — it requires extra Gradle / CocoaPods config and a license key.
|
|
1634
|
+
|
|
1635
|
+
#### 2. Add the Expo config plugin
|
|
1636
|
+
|
|
1637
|
+
In `app.json` (or `app.config.js`), add `expo-beacon/plugin/withBeaconBGLocation` to your plugins list:
|
|
1638
|
+
|
|
1639
|
+
```json
|
|
1640
|
+
{
|
|
1641
|
+
"expo": {
|
|
1642
|
+
"plugins": [
|
|
1643
|
+
"expo-beacon/plugin/withBeaconBGLocation"
|
|
1644
|
+
]
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
```
|
|
1648
|
+
|
|
1649
|
+
Then run prebuild to apply the native changes:
|
|
1650
|
+
|
|
1651
|
+
```sh
|
|
1652
|
+
npx expo prebuild --clean
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
The plugin writes `BeaconGeoPlugin.swift` / `BeaconGeoPlugin.kt` into your native project and wires them up in `AppDelegate.swift` and `MainApplication.kt` automatically.
|
|
1656
|
+
|
|
1657
|
+
#### 3. Configure BGLocation once at JS startup
|
|
1658
|
+
|
|
1659
|
+
Call `ready()` once when your app starts, **not** inside a beacon callback:
|
|
1660
|
+
|
|
1661
|
+
```ts
|
|
1662
|
+
import BackgroundGeolocation from 'react-native-background-geolocation';
|
|
1663
|
+
|
|
1664
|
+
BackgroundGeolocation.ready({
|
|
1665
|
+
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
|
1666
|
+
distanceFilter: 10,
|
|
1667
|
+
stopOnTerminate: false,
|
|
1668
|
+
startOnBoot: true,
|
|
1669
|
+
// ...your config
|
|
1670
|
+
});
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
#### How it works at runtime
|
|
1674
|
+
|
|
1675
|
+
```
|
|
1676
|
+
Beacon region entered (native)
|
|
1677
|
+
→ BeaconForegroundService / ExpoBeaconModule (expo-beacon)
|
|
1678
|
+
→ BeaconPluginRegistry / BeaconLifecycleRegistry dispatches to plugins
|
|
1679
|
+
→ BeaconGeoPlugin.onBeaconEnter / beaconDidEnter
|
|
1680
|
+
→ BackgroundGeolocation.start() ← native only, no JS bridge involved
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
---
|
|
1684
|
+
|
|
1685
|
+
#### Manual wiring (without the config plugin)
|
|
1686
|
+
|
|
1687
|
+
If you prefer not to use `expo prebuild` (e.g. you manage your native project manually), create the following files yourself after each `npx expo prebuild`:
|
|
1688
|
+
|
|
1689
|
+
**iOS** — `ios/<AppName>/BeaconGeoPlugin.swift` (add to Xcode target):
|
|
1690
|
+
|
|
1691
|
+
```swift
|
|
1692
|
+
import ExpoBeacon
|
|
1693
|
+
import TSLocationManager
|
|
1694
|
+
|
|
1695
|
+
final class BeaconGeoPlugin: BeaconLifecycleDelegate {
|
|
1696
|
+
func beaconDidEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
1697
|
+
TSLocationManager.sharedManager().start()
|
|
1698
|
+
}
|
|
1699
|
+
func beaconDidExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
1700
|
+
TSLocationManager.sharedManager().stop()
|
|
1701
|
+
}
|
|
1702
|
+
func eddystoneDidEnter(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
1703
|
+
TSLocationManager.sharedManager().start()
|
|
1704
|
+
}
|
|
1705
|
+
func eddystoneDidExit(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
1706
|
+
TSLocationManager.sharedManager().stop()
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
Register in `ios/<AppName>/AppDelegate.swift` **before** `super`:
|
|
1712
|
+
|
|
1713
|
+
```swift
|
|
1714
|
+
import ExpoBeacon
|
|
1715
|
+
|
|
1716
|
+
// in application(_:didFinishLaunchingWithOptions:):
|
|
1717
|
+
BeaconLifecycleRegistry.register(BeaconGeoPlugin()) // ← before super
|
|
1718
|
+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
**Android** — `android/app/src/main/java/<pkg>/BeaconGeoPlugin.kt`:
|
|
1722
|
+
|
|
1723
|
+
```kotlin
|
|
1724
|
+
package com.yourapp
|
|
1725
|
+
|
|
1726
|
+
import android.content.Context
|
|
1727
|
+
import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation
|
|
1728
|
+
import expo.modules.beacon.BeaconEventPlugin
|
|
1729
|
+
|
|
1730
|
+
class BeaconGeoPlugin(ctx: Context) : BeaconEventPlugin {
|
|
1731
|
+
private val bgGeo = BackgroundGeolocation.getInstance(ctx, null)
|
|
1732
|
+
|
|
1733
|
+
override fun onBeaconEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
1734
|
+
bgGeo.start(null)
|
|
1735
|
+
override fun onBeaconExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
1736
|
+
bgGeo.stop(null)
|
|
1737
|
+
override fun onEddystoneEnter(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
1738
|
+
bgGeo.start(null)
|
|
1739
|
+
override fun onEddystoneExit(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
1740
|
+
bgGeo.stop(null)
|
|
1741
|
+
}
|
|
1742
|
+
```
|
|
1743
|
+
|
|
1744
|
+
Register in `MainApplication.kt` inside `onCreate()` after `super`:
|
|
1745
|
+
|
|
1746
|
+
```kotlin
|
|
1747
|
+
import expo.modules.beacon.BeaconPluginRegistry
|
|
1748
|
+
|
|
1749
|
+
override fun onCreate() {
|
|
1750
|
+
super.onCreate()
|
|
1751
|
+
BeaconPluginRegistry.register(BeaconGeoPlugin(this)) // ← after super
|
|
1752
|
+
}
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
---
|
|
1756
|
+
|
|
1613
1757
|
## Background Behaviour
|
|
1614
1758
|
|
|
1615
1759
|
### Android
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package expo.modules.beacon
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implement this interface in your app to react to beacon enter/exit events at the native level.
|
|
5
|
+
*
|
|
6
|
+
* Register your implementation via [BeaconPluginRegistry.register] from MainApplication.onCreate().
|
|
7
|
+
* Unregister via [BeaconPluginRegistry.unregister] if the plugin has a scoped lifetime.
|
|
8
|
+
*/
|
|
9
|
+
interface BeaconEventPlugin {
|
|
10
|
+
// iBeacon
|
|
11
|
+
fun onBeaconEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double)
|
|
12
|
+
fun onBeaconExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double)
|
|
13
|
+
|
|
14
|
+
// Eddystone
|
|
15
|
+
fun onEddystoneEnter(identifier: String, namespace: String, instance: String, distance: Double)
|
|
16
|
+
fun onEddystoneExit(identifier: String, namespace: String, instance: String, distance: Double)
|
|
17
|
+
}
|
|
@@ -614,6 +614,21 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
614
614
|
// Forward all produced events to remote API
|
|
615
615
|
apiForwarder?.forwardEvent(params)
|
|
616
616
|
|
|
617
|
+
// Dispatch enter/exit to registered plugins (e.g. to start/stop BGLocation)
|
|
618
|
+
if (eventType == "enter" || eventType == "exit") {
|
|
619
|
+
val identifier = region.uniqueId
|
|
620
|
+
val uuid = if (!isEddystone) id1Str else ""
|
|
621
|
+
val major = if (!isEddystone) region.id2?.toInt() ?: 0 else 0
|
|
622
|
+
val minor = if (!isEddystone) region.id3?.toInt() ?: 0 else 0
|
|
623
|
+
val namespace = if (isEddystone) id1Str.removePrefix("0x") else ""
|
|
624
|
+
val instance = if (isEddystone) region.id2?.toString()?.removePrefix("0x") ?: "" else ""
|
|
625
|
+
if (eventType == "enter") {
|
|
626
|
+
BeaconPluginRegistry.dispatchEnter(isEddystone, identifier, uuid, major, minor, namespace, instance, distance)
|
|
627
|
+
} else {
|
|
628
|
+
BeaconPluginRegistry.dispatchExit(isEddystone, identifier, uuid, major, minor, namespace, instance, distance)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
617
632
|
val intent = Intent(ACTION_BEACON_EVENT).apply {
|
|
618
633
|
putExtra("identifier", region.uniqueId)
|
|
619
634
|
putExtra("event", eventType)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package expo.modules.beacon
|
|
2
|
+
|
|
3
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thread-safe singleton registry for [BeaconEventPlugin] implementations.
|
|
7
|
+
*
|
|
8
|
+
* Register your plugin once from MainApplication.onCreate():
|
|
9
|
+
* BeaconPluginRegistry.register(MyBeaconPlugin(this))
|
|
10
|
+
*
|
|
11
|
+
* If no plugins are registered, all dispatch calls are no-ops.
|
|
12
|
+
*/
|
|
13
|
+
object BeaconPluginRegistry {
|
|
14
|
+
private val plugins = CopyOnWriteArrayList<BeaconEventPlugin>()
|
|
15
|
+
|
|
16
|
+
fun register(plugin: BeaconEventPlugin) {
|
|
17
|
+
plugins.add(plugin)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fun unregister(plugin: BeaconEventPlugin) {
|
|
21
|
+
plugins.remove(plugin)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
internal fun dispatchEnter(
|
|
25
|
+
isEddystone: Boolean,
|
|
26
|
+
identifier: String,
|
|
27
|
+
uuid: String,
|
|
28
|
+
major: Int,
|
|
29
|
+
minor: Int,
|
|
30
|
+
namespace: String,
|
|
31
|
+
instance: String,
|
|
32
|
+
distance: Double,
|
|
33
|
+
) {
|
|
34
|
+
plugins.forEach { plugin ->
|
|
35
|
+
if (isEddystone) {
|
|
36
|
+
plugin.onEddystoneEnter(identifier, namespace, instance, distance)
|
|
37
|
+
} else {
|
|
38
|
+
plugin.onBeaconEnter(identifier, uuid, major, minor, distance)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
internal fun dispatchExit(
|
|
44
|
+
isEddystone: Boolean,
|
|
45
|
+
identifier: String,
|
|
46
|
+
uuid: String,
|
|
47
|
+
major: Int,
|
|
48
|
+
minor: Int,
|
|
49
|
+
namespace: String,
|
|
50
|
+
instance: String,
|
|
51
|
+
distance: Double,
|
|
52
|
+
) {
|
|
53
|
+
plugins.forEach { plugin ->
|
|
54
|
+
if (isEddystone) {
|
|
55
|
+
plugin.onEddystoneExit(identifier, namespace, instance, distance)
|
|
56
|
+
} else {
|
|
57
|
+
plugin.onBeaconExit(identifier, uuid, major, minor, distance)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/// Implement this protocol in your app to react to beacon enter/exit events at the native level.
|
|
2
|
+
///
|
|
3
|
+
/// Register your implementation once, before the Expo module is created:
|
|
4
|
+
/// BeaconLifecycleRegistry.register(MyPlugin())
|
|
5
|
+
///
|
|
6
|
+
/// The registry holds a strong reference — register early (e.g. AppDelegate.didFinishLaunching
|
|
7
|
+
/// before super, or in a +load / initialize method).
|
|
8
|
+
public protocol BeaconLifecycleDelegate: AnyObject {
|
|
9
|
+
// MARK: iBeacon
|
|
10
|
+
func beaconDidEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double)
|
|
11
|
+
func beaconDidExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double)
|
|
12
|
+
|
|
13
|
+
// MARK: Eddystone
|
|
14
|
+
func eddystoneDidEnter(identifier: String, namespace: String, instance: String, distance: Double)
|
|
15
|
+
func eddystoneDidExit(identifier: String, namespace: String, instance: String, distance: Double)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Thread-safe registry for [BeaconLifecycleDelegate] plugins.
|
|
19
|
+
/// Mirrors the Android BeaconPluginRegistry pattern so both platforms use the same app-side wiring.
|
|
20
|
+
public final class BeaconLifecycleRegistry {
|
|
21
|
+
public static let shared = BeaconLifecycleRegistry()
|
|
22
|
+
private init() {}
|
|
23
|
+
|
|
24
|
+
private let lock = NSLock()
|
|
25
|
+
private var plugins: [any BeaconLifecycleDelegate] = []
|
|
26
|
+
|
|
27
|
+
public static func register(_ plugin: any BeaconLifecycleDelegate) {
|
|
28
|
+
shared.lock.lock(); defer { shared.lock.unlock() }
|
|
29
|
+
shared.plugins.append(plugin)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public static func unregister(_ plugin: any BeaconLifecycleDelegate) {
|
|
33
|
+
shared.lock.lock(); defer { shared.lock.unlock() }
|
|
34
|
+
shared.plugins.removeAll { $0 === plugin }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
internal func dispatchEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
38
|
+
snapshot().forEach { $0.beaconDidEnter(identifier: identifier, uuid: uuid, major: major, minor: minor, distance: distance) }
|
|
39
|
+
}
|
|
40
|
+
internal func dispatchExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
41
|
+
snapshot().forEach { $0.beaconDidExit(identifier: identifier, uuid: uuid, major: major, minor: minor, distance: distance) }
|
|
42
|
+
}
|
|
43
|
+
internal func dispatchEddystoneEnter(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
44
|
+
snapshot().forEach { $0.eddystoneDidEnter(identifier: identifier, namespace: namespace, instance: instance, distance: distance) }
|
|
45
|
+
}
|
|
46
|
+
internal func dispatchEddystoneExit(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
47
|
+
snapshot().forEach { $0.eddystoneDidExit(identifier: identifier, namespace: namespace, instance: instance, distance: distance) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func snapshot() -> [any BeaconLifecycleDelegate] {
|
|
51
|
+
lock.lock(); defer { lock.unlock() }
|
|
52
|
+
return plugins
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -123,6 +123,8 @@ public class ExpoBeaconModule: Module {
|
|
|
123
123
|
private var eventLogger: BeaconEventLogger?
|
|
124
124
|
private var loggingEnabled = false
|
|
125
125
|
|
|
126
|
+
|
|
127
|
+
|
|
126
128
|
// Native API forwarder (fire-and-forget HTTP)
|
|
127
129
|
private lazy var apiForwarder = BeaconApiForwarder(defaults: defaults)
|
|
128
130
|
|
|
@@ -1038,9 +1040,51 @@ public class ExpoBeaconModule: Module {
|
|
|
1038
1040
|
}
|
|
1039
1041
|
// Forward all produced events to remote API
|
|
1040
1042
|
apiForwarder.forwardEvent(params)
|
|
1043
|
+
// Dispatch enter/exit to registered plugins (e.g. to start/stop BGLocation)
|
|
1044
|
+
dispatchToLifecycleRegistry(eventName: eventName, params: params)
|
|
1041
1045
|
sendEvent(eventName, params)
|
|
1042
1046
|
}
|
|
1043
1047
|
|
|
1048
|
+
private func dispatchToLifecycleRegistry(eventName: String, params: [String: Any]) {
|
|
1049
|
+
let r = BeaconLifecycleRegistry.shared
|
|
1050
|
+
let identifier = params["identifier"] as? String ?? ""
|
|
1051
|
+
let distance = params["distance"] as? Double ?? -1.0
|
|
1052
|
+
switch eventName {
|
|
1053
|
+
case "onBeaconEnter":
|
|
1054
|
+
r.dispatchEnter(
|
|
1055
|
+
identifier: identifier,
|
|
1056
|
+
uuid: params["uuid"] as? String ?? "",
|
|
1057
|
+
major: params["major"] as? Int ?? 0,
|
|
1058
|
+
minor: params["minor"] as? Int ?? 0,
|
|
1059
|
+
distance: distance
|
|
1060
|
+
)
|
|
1061
|
+
case "onBeaconExit":
|
|
1062
|
+
r.dispatchExit(
|
|
1063
|
+
identifier: identifier,
|
|
1064
|
+
uuid: params["uuid"] as? String ?? "",
|
|
1065
|
+
major: params["major"] as? Int ?? 0,
|
|
1066
|
+
minor: params["minor"] as? Int ?? 0,
|
|
1067
|
+
distance: distance
|
|
1068
|
+
)
|
|
1069
|
+
case "onEddystoneEnter":
|
|
1070
|
+
r.dispatchEddystoneEnter(
|
|
1071
|
+
identifier: identifier,
|
|
1072
|
+
namespace: params["namespace"] as? String ?? "",
|
|
1073
|
+
instance: params["instance"] as? String ?? "",
|
|
1074
|
+
distance: distance
|
|
1075
|
+
)
|
|
1076
|
+
case "onEddystoneExit":
|
|
1077
|
+
r.dispatchEddystoneExit(
|
|
1078
|
+
identifier: identifier,
|
|
1079
|
+
namespace: params["namespace"] as? String ?? "",
|
|
1080
|
+
instance: params["instance"] as? String ?? "",
|
|
1081
|
+
distance: distance
|
|
1082
|
+
)
|
|
1083
|
+
default:
|
|
1084
|
+
break
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1044
1088
|
private func getOrCreateEventLogger() -> BeaconEventLogger {
|
|
1045
1089
|
if let logger = eventLogger {
|
|
1046
1090
|
return logger
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-beacon",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"build/",
|
|
16
|
+
"plugin/build/",
|
|
16
17
|
"ios/",
|
|
17
18
|
"android/src/",
|
|
18
19
|
"android/build.gradle",
|
|
@@ -21,11 +22,12 @@
|
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "expo-module build",
|
|
25
|
+
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
24
26
|
"clean": "expo-module clean",
|
|
25
27
|
"lint": "expo-module lint",
|
|
26
28
|
"test": "expo-module test",
|
|
27
|
-
"prepare": "expo-module prepare",
|
|
28
|
-
"prepublishOnly": "expo-module prepublishOnly",
|
|
29
|
+
"prepare": "expo-module prepare && npm run build:plugin",
|
|
30
|
+
"prepublishOnly": "expo-module prepublishOnly && npm run build:plugin",
|
|
29
31
|
"expo-module": "expo-module",
|
|
30
32
|
"open:ios": "xed example/ios",
|
|
31
33
|
"open:android": "open -a \"Android Studio\" example/android"
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
"react-native": "0.83.2"
|
|
61
63
|
},
|
|
62
64
|
"peerDependencies": {
|
|
65
|
+
"@expo/config-plugins": "*",
|
|
63
66
|
"expo": ">=51.0.0",
|
|
64
67
|
"react": "*",
|
|
65
68
|
"react-native": "*"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withBeaconBGLocation.d.ts","sourceRoot":"","sources":["../src/withBeaconBGLocation.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAKb,MAAM,sBAAsB,CAAC;;AA4P9B,wBAIE"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
// ─── Generated file contents ─────────────────────────────────────────────────
|
|
7
|
+
const IOS_PLUGIN_SWIFT = `\
|
|
8
|
+
import ExpoBeacon
|
|
9
|
+
import TSLocationManager
|
|
10
|
+
|
|
11
|
+
final class BeaconGeoPlugin: BeaconLifecycleDelegate {
|
|
12
|
+
func beaconDidEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
13
|
+
TSLocationManager.sharedManager().start()
|
|
14
|
+
}
|
|
15
|
+
func beaconDidExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
|
|
16
|
+
TSLocationManager.sharedManager().stop()
|
|
17
|
+
}
|
|
18
|
+
func eddystoneDidEnter(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
19
|
+
TSLocationManager.sharedManager().start()
|
|
20
|
+
}
|
|
21
|
+
func eddystoneDidExit(identifier: String, namespace: String, instance: String, distance: Double) {
|
|
22
|
+
TSLocationManager.sharedManager().stop()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
function getAndroidPluginKotlin(packageName) {
|
|
27
|
+
return `\
|
|
28
|
+
package ${packageName}
|
|
29
|
+
|
|
30
|
+
import android.content.Context
|
|
31
|
+
import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation
|
|
32
|
+
import expo.modules.beacon.BeaconEventPlugin
|
|
33
|
+
|
|
34
|
+
class BeaconGeoPlugin(ctx: Context) : BeaconEventPlugin {
|
|
35
|
+
private val bgGeo = BackgroundGeolocation.getInstance(ctx, null)
|
|
36
|
+
|
|
37
|
+
override fun onBeaconEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
38
|
+
bgGeo.start(null)
|
|
39
|
+
override fun onBeaconExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
|
|
40
|
+
bgGeo.stop(null)
|
|
41
|
+
override fun onEddystoneEnter(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
42
|
+
bgGeo.start(null)
|
|
43
|
+
override fun onEddystoneExit(identifier: String, namespace: String, instance: String, distance: Double) =
|
|
44
|
+
bgGeo.stop(null)
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
// ─── iOS helpers ─────────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Finds the UUID (key) of a PBXGroup by name, handling both quoted and unquoted
|
|
51
|
+
* name values produced by node-xcode's parser.
|
|
52
|
+
*/
|
|
53
|
+
function findPBXGroupKeyByName(xcodeProject, name) {
|
|
54
|
+
var _a;
|
|
55
|
+
const pbxGroups = (_a = xcodeProject.hash.project.objects['PBXGroup']) !== null && _a !== void 0 ? _a : {};
|
|
56
|
+
for (const key of Object.keys(pbxGroups)) {
|
|
57
|
+
if (key.endsWith('_comment'))
|
|
58
|
+
continue;
|
|
59
|
+
const group = pbxGroups[key];
|
|
60
|
+
// node-xcode stores quoted strings with literal quote characters in the value.
|
|
61
|
+
if (group.name === `"${name}"` || group.name === name)
|
|
62
|
+
return key;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** Returns true if a PBXFileReference for filename already exists (idempotency guard). */
|
|
67
|
+
function isFileInXcodeProject(xcodeProject, filename) {
|
|
68
|
+
var _a;
|
|
69
|
+
const refs = (_a = xcodeProject.hash.project.objects['PBXFileReference']) !== null && _a !== void 0 ? _a : {};
|
|
70
|
+
return Object.values(refs).some((f) => typeof f === 'object' &&
|
|
71
|
+
f !== null &&
|
|
72
|
+
(f.path === `"${filename}"` || f.path === filename));
|
|
73
|
+
}
|
|
74
|
+
function modifySwiftAppDelegate(contents) {
|
|
75
|
+
// Insert `import ExpoBeacon` after the last existing import line.
|
|
76
|
+
if (!contents.includes('import ExpoBeacon')) {
|
|
77
|
+
const lines = contents.split('\n');
|
|
78
|
+
const lastImportIdx = lines.reduce((last, line, i) => (line.trimStart().startsWith('import ') ? i : last), -1);
|
|
79
|
+
if (lastImportIdx >= 0) {
|
|
80
|
+
lines.splice(lastImportIdx + 1, 0, 'import ExpoBeacon');
|
|
81
|
+
contents = lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Insert `BeaconLifecycleRegistry.register(BeaconGeoPlugin())` immediately
|
|
85
|
+
// before `return super.application(`, preserving indentation.
|
|
86
|
+
const marker = 'BeaconLifecycleRegistry.register(BeaconGeoPlugin())';
|
|
87
|
+
if (!contents.includes(marker)) {
|
|
88
|
+
contents = contents.replace(/([ \t]*)(return super\.application\()/, `$1${marker}\n$1$2`);
|
|
89
|
+
}
|
|
90
|
+
return contents;
|
|
91
|
+
}
|
|
92
|
+
// ─── Android helpers ─────────────────────────────────────────────────────────
|
|
93
|
+
function modifyMainApplication(contents) {
|
|
94
|
+
const importLine = 'import expo.modules.beacon.BeaconPluginRegistry';
|
|
95
|
+
// Add missing import after the last existing import.
|
|
96
|
+
if (!contents.includes(importLine)) {
|
|
97
|
+
const lines = contents.split('\n');
|
|
98
|
+
const lastImportIdx = lines.reduce((last, line, i) => (line.trimStart().startsWith('import ') ? i : last), -1);
|
|
99
|
+
if (lastImportIdx >= 0) {
|
|
100
|
+
lines.splice(lastImportIdx + 1, 0, importLine);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// No imports yet — insert after the package declaration.
|
|
104
|
+
const pkgIdx = lines.findIndex((l) => l.startsWith('package '));
|
|
105
|
+
lines.splice(pkgIdx >= 0 ? pkgIdx + 1 : 0, 0, '', importLine);
|
|
106
|
+
}
|
|
107
|
+
contents = lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
// Add registration call immediately after `super.onCreate()`.
|
|
110
|
+
const registration = ' BeaconPluginRegistry.register(BeaconGeoPlugin(this))';
|
|
111
|
+
if (!contents.includes('BeaconPluginRegistry.register(BeaconGeoPlugin(this))')) {
|
|
112
|
+
contents = contents.replace(/(super\.onCreate\(\)[ \t]*\n)/, `$1${registration}\n`);
|
|
113
|
+
}
|
|
114
|
+
return contents;
|
|
115
|
+
}
|
|
116
|
+
// ─── iOS plugin steps ────────────────────────────────────────────────────────
|
|
117
|
+
const withBeaconIOSBGLocation = (config) => {
|
|
118
|
+
// Step 1 – write BeaconGeoPlugin.swift and add it to the Xcode project.
|
|
119
|
+
config = (0, config_plugins_1.withXcodeProject)(config, (config) => {
|
|
120
|
+
var _a;
|
|
121
|
+
const xcodeProject = config.modResults;
|
|
122
|
+
const { projectName, platformProjectRoot } = config.modRequest;
|
|
123
|
+
if (!projectName || !platformProjectRoot)
|
|
124
|
+
return config;
|
|
125
|
+
const swiftFilePath = path.join(platformProjectRoot, projectName, 'BeaconGeoPlugin.swift');
|
|
126
|
+
fs.writeFileSync(swiftFilePath, IOS_PLUGIN_SWIFT);
|
|
127
|
+
if (!isFileInXcodeProject(xcodeProject, 'BeaconGeoPlugin.swift')) {
|
|
128
|
+
const groupKey = (_a = findPBXGroupKeyByName(xcodeProject, projectName)) !== null && _a !== void 0 ? _a : undefined;
|
|
129
|
+
xcodeProject.addSourceFile('BeaconGeoPlugin.swift', null, groupKey !== null && groupKey !== void 0 ? groupKey : undefined);
|
|
130
|
+
}
|
|
131
|
+
return config;
|
|
132
|
+
});
|
|
133
|
+
// Step 2 – patch AppDelegate.swift to register the plugin before super.
|
|
134
|
+
config = (0, config_plugins_1.withAppDelegate)(config, (config) => {
|
|
135
|
+
if (config.modResults.language === 'swift') {
|
|
136
|
+
config.modResults.contents = modifySwiftAppDelegate(config.modResults.contents);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.warn('[expo-beacon] withBeaconBGLocation: AppDelegate is not Swift — ' +
|
|
140
|
+
'please add BeaconLifecycleRegistry.register(BeaconGeoPlugin()) manually.');
|
|
141
|
+
}
|
|
142
|
+
return config;
|
|
143
|
+
});
|
|
144
|
+
return config;
|
|
145
|
+
};
|
|
146
|
+
// ─── Android plugin steps ─────────────────────────────────────────────────────
|
|
147
|
+
const withBeaconAndroidBGLocation = (config) => {
|
|
148
|
+
// Step 1 – write BeaconGeoPlugin.kt into the app source tree.
|
|
149
|
+
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
150
|
+
'android',
|
|
151
|
+
(config) => {
|
|
152
|
+
var _a;
|
|
153
|
+
const pkgName = (_a = config.android) === null || _a === void 0 ? void 0 : _a.package;
|
|
154
|
+
if (!pkgName) {
|
|
155
|
+
console.warn('[expo-beacon] android.package not set — BeaconGeoPlugin.kt was not written.');
|
|
156
|
+
return config;
|
|
157
|
+
}
|
|
158
|
+
const pkgPath = pkgName.replace(/\./g, '/');
|
|
159
|
+
const outputPath = path.join(config.modRequest.platformProjectRoot, 'app/src/main/java', pkgPath, 'BeaconGeoPlugin.kt');
|
|
160
|
+
fs.writeFileSync(outputPath, getAndroidPluginKotlin(pkgName));
|
|
161
|
+
return config;
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
// Step 2 – patch MainApplication (Kotlin or Java) to call register().
|
|
165
|
+
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
166
|
+
'android',
|
|
167
|
+
(config) => {
|
|
168
|
+
var _a;
|
|
169
|
+
const pkgName = (_a = config.android) === null || _a === void 0 ? void 0 : _a.package;
|
|
170
|
+
if (!pkgName)
|
|
171
|
+
return config;
|
|
172
|
+
const pkgPath = pkgName.replace(/\./g, '/');
|
|
173
|
+
const javaRoot = path.join(config.modRequest.platformProjectRoot, 'app/src/main/java', pkgPath);
|
|
174
|
+
// Support both Kotlin and Java MainApplication.
|
|
175
|
+
const mainAppPath = [
|
|
176
|
+
path.join(javaRoot, 'MainApplication.kt'),
|
|
177
|
+
path.join(javaRoot, 'MainApplication.java'),
|
|
178
|
+
].find(fs.existsSync);
|
|
179
|
+
if (!mainAppPath) {
|
|
180
|
+
console.warn('[expo-beacon] MainApplication.kt / .java not found — ' +
|
|
181
|
+
'please add BeaconPluginRegistry.register(BeaconGeoPlugin(this)) manually.');
|
|
182
|
+
return config;
|
|
183
|
+
}
|
|
184
|
+
const original = fs.readFileSync(mainAppPath, 'utf-8');
|
|
185
|
+
fs.writeFileSync(mainAppPath, modifyMainApplication(original));
|
|
186
|
+
return config;
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
return config;
|
|
190
|
+
};
|
|
191
|
+
// ─── Combined plugin ─────────────────────────────────────────────────────────
|
|
192
|
+
const withBeaconBGLocation = (config) => {
|
|
193
|
+
config = withBeaconIOSBGLocation(config);
|
|
194
|
+
config = withBeaconAndroidBGLocation(config);
|
|
195
|
+
return config;
|
|
196
|
+
};
|
|
197
|
+
exports.default = (0, config_plugins_1.createRunOncePlugin)(withBeaconBGLocation, 'expo-beacon-bglocation', '1.0.0');
|