expo-beacon 0.6.4 → 0.6.7

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.
@@ -22,6 +22,9 @@
22
22
  <!-- Receive boot broadcast to restart monitoring after device reboot -->
23
23
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
24
24
 
25
+ <!-- Battery optimization exemption for reliable background BLE scanning -->
26
+ <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
27
+
25
28
  <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
26
29
 
27
30
  <application>
@@ -30,9 +33,10 @@
30
33
  android:name="org.altbeacon.beacon.service.BeaconService"
31
34
  android:enabled="true"
32
35
  android:exported="false"
36
+ android:foregroundServiceType="connectedDevice"
33
37
  android:isolatedProcess="false"
34
38
  android:label="iBeacon Service"
35
- tools:replace="android:label" />
39
+ tools:replace="android:label,android:foregroundServiceType" />
36
40
 
37
41
  <service
38
42
  android:name="org.altbeacon.beacon.BeaconIntentProcessor"
@@ -1,8 +1,10 @@
1
1
  package expo.modules.beacon
2
2
 
3
3
  // Shared constants used across ExpoBeaconModule and BeaconForegroundService.
4
- // IMPORTANT: Timing constants (EXIT_MISS_THRESHOLD, HYSTERESIS_COUNT) must stay
5
- // in sync with ExpoBeaconModule.swift (iOS).
4
+ // NOTE: EXIT_MISS_THRESHOLD and scan timing deviate from iOS intentionally —
5
+ // Android requires a non-zero between-scan gap to prevent OS-level BLE
6
+ // throttling, and a higher miss threshold to tolerate brief scan pauses.
7
+ // HYSTERESIS_COUNT should stay in sync with ExpoBeaconModule.swift (iOS).
6
8
 
7
9
  internal const val PREFS_NAME = "expo.beacon.paired"
8
10
  internal const val PREFS_KEY = "paired_beacons"
@@ -16,18 +18,35 @@ internal const val EVENT_LOGGING_ENABLED_KEY = "enabled"
16
18
  /** Foreground-service scan window for background monitoring responsiveness. */
17
19
  internal const val MONITORING_SCAN_PERIOD_MS = 1100L
18
20
 
19
- /** Gap between scan windows while the foreground service is active. */
20
- internal const val MONITORING_BETWEEN_SCAN_PERIOD_MS = 0L
21
+ /**
22
+ * Gap between scan windows while the foreground service is active.
23
+ * A non-zero gap prevents Android from throttling continuous BLE scans
24
+ * after a few minutes — the BLE stack needs brief idle periods to avoid
25
+ * OS-level scan suppression on many devices (Samsung, Pixel, etc.).
26
+ */
27
+ internal const val MONITORING_BETWEEN_SCAN_PERIOD_MS = 1000L
21
28
 
22
29
  /** Ignore monitor-based exits if ranging saw the beacon within this window. */
23
- internal const val RECENT_RANGING_SIGHTING_GRACE_MS = 4000L
30
+ internal const val RECENT_RANGING_SIGHTING_GRACE_MS = 25000L
24
31
 
25
- /** Number of consecutive ranging misses before emitting a distance-based exit event. */
26
- internal const val EXIT_MISS_THRESHOLD = 3
32
+ /**
33
+ * Number of consecutive ranging misses before emitting a distance-based exit event.
34
+ * With a ~2.1 s scan cycle (1100 ms scan + 1000 ms gap), 10 misses ≈ 21 s of
35
+ * silence before declaring exit — tolerant of brief BLE gaps while still
36
+ * responsive to actual departures.
37
+ */
38
+ internal const val EXIT_MISS_THRESHOLD = 10
27
39
 
28
40
  /** Number of consecutive readings required to confirm a distance-based enter or exit transition. */
29
41
  internal const val HYSTERESIS_COUNT = 3
30
42
 
43
+ /**
44
+ * AltBeacon region exit period — how long after the last sighting before
45
+ * MonitorNotifier.didExitRegion fires. Set generously to avoid premature
46
+ * monitor-level exits that bypass ranging hysteresis.
47
+ */
48
+ internal const val REGION_EXIT_PERIOD_MS = 60000L
49
+
31
50
  /** Shared log tag for the expo-beacon module. */
32
51
  internal const val TAG = "ExpoBeacon"
33
52
 
@@ -19,7 +19,7 @@ import org.altbeacon.beacon.*
19
19
  import org.json.JSONArray
20
20
 
21
21
  private const val CHANNEL_ID = "expo_beacon_channel"
22
- private const val FOREGROUND_NOTIF_ID = 1001
22
+ internal const val FOREGROUND_NOTIF_ID = 1001
23
23
  /**
24
24
  * Base ID for per-beacon enter/exit notifications; incremented per unique region.
25
25
  * With FOREGROUND_NOTIF_ID at 1001, this allows up to 999 unique regions
@@ -32,6 +32,9 @@ class BeaconForegroundService : Service(), BeaconConsumer {
32
32
  private lateinit var beaconManager: BeaconManager
33
33
  private val monitoredRegions = mutableListOf<Region>()
34
34
 
35
+ /** Tracks whether onBeaconServiceConnect has fired for the current bind. */
36
+ @Volatile private var serviceConnected = false
37
+
35
38
  // Distance filtering
36
39
  @Volatile private var maxDistance: Double? = null
37
40
  @Volatile private var exitDistance: Double? = null
@@ -59,32 +62,6 @@ class BeaconForegroundService : Service(), BeaconConsumer {
59
62
  private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
60
63
  private var eventLogger: BeaconEventLogger? = null
61
64
 
62
- companion object {
63
- private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
64
-
65
- fun start(context: Context) {
66
- context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
67
- .edit().putBoolean("active", true).apply()
68
- val intent = Intent(context, BeaconForegroundService::class.java)
69
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
70
- context.startForegroundService(intent)
71
- } else {
72
- context.startService(intent)
73
- }
74
- }
75
-
76
- fun stop(context: Context) {
77
- context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
78
- .edit().putBoolean("active", false).apply()
79
- context.stopService(Intent(context, BeaconForegroundService::class.java))
80
- }
81
-
82
- fun isMonitoringActive(context: Context): Boolean {
83
- return context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
84
- .getBoolean("active", false)
85
- }
86
- }
87
-
88
65
  override fun onCreate() {
89
66
  super.onCreate()
90
67
  createNotificationChannel()
@@ -96,6 +73,21 @@ class BeaconForegroundService : Service(), BeaconConsumer {
96
73
  manager.setForegroundScanPeriod(MONITORING_SCAN_PERIOD_MS)
97
74
  manager.setForegroundBetweenScanPeriod(MONITORING_BETWEEN_SCAN_PERIOD_MS)
98
75
  }
76
+ // Increase AltBeacon's region exit period so didExitRegion doesn't fire
77
+ // prematurely during brief BLE scan gaps.
78
+ BeaconManager.setRegionExitPeriod(REGION_EXIT_PERIOD_MS)
79
+ // Let AltBeacon's internal BeaconService run as a foreground service so
80
+ // Samsung/OEM battery enforcement doesn't kill BLE scanning after ~4 min.
81
+ // Reset first to clear any stale state from a previous session.
82
+ try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
83
+ try {
84
+ beaconManager.enableForegroundServiceScanning(
85
+ buildForegroundNotification(), FOREGROUND_NOTIF_ID
86
+ )
87
+ } catch (e: IllegalStateException) {
88
+ // Already bound by another consumer (e.g. a scan in progress) — non-fatal.
89
+ Log.w(TAG, "enableForegroundServiceScanning skipped (already bound)", e)
90
+ }
99
91
  }
100
92
 
101
93
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -108,11 +100,18 @@ class BeaconForegroundService : Service(), BeaconConsumer {
108
100
  } else {
109
101
  startForeground(FOREGROUND_NOTIF_ID, buildForegroundNotification())
110
102
  }
111
- beaconManager.bind(this)
103
+ if (serviceConnected) {
104
+ // Already bound from a prior onStartCommand — reload regions directly
105
+ // so that re-starting monitoring from JS always takes effect.
106
+ loadAndMonitorRegions()
107
+ } else {
108
+ beaconManager.bind(this)
109
+ }
112
110
  return START_STICKY
113
111
  }
114
112
 
115
113
  override fun onBeaconServiceConnect() {
114
+ serviceConnected = true
116
115
  // Read max distance and exit distance from options prefs
117
116
  val optPrefs = getSharedPreferences(MONITORING_OPTIONS_PREFS, Context.MODE_PRIVATE)
118
117
  maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
@@ -158,8 +157,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
158
157
  try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
159
158
  }
160
159
  distanceLogRegions.clear()
160
+ monitoredRegions.forEach {
161
+ try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
162
+ }
163
+ monitoredRegions.clear()
161
164
  monitoredRegionIds.clear()
162
- enteredRegions.clear()
163
165
  lastSeenAtMs.clear()
164
166
  timeoutHandler.removeCallbacksAndMessages(null)
165
167
  timeoutRunnables.clear()
@@ -168,10 +170,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
168
170
  exitCounters.clear()
169
171
  missCounters.clear()
170
172
  }
171
- monitoredRegions.forEach {
172
- try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
173
- }
174
- monitoredRegions.clear()
173
+ // NOTE: enteredRegions is intentionally NOT cleared here.
174
+ // Clearing it on every reload (e.g. START_STICKY restart or repeated
175
+ // startMonitoring calls) would reset the "already entered" state and
176
+ // cause the hysteresis to fire another ENTER event for beacons that
177
+ // are still nearby. Stale entries are pruned below after new regions
178
+ // are determined.
175
179
 
176
180
  // iBeacon regions
177
181
  for (i in 0 until beacons.length()) {
@@ -231,8 +235,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
231
235
 
232
236
  // If no regions to monitor, stop the service to avoid idling
233
237
  if (monitoredRegions.isEmpty()) {
238
+ enteredRegions.clear()
234
239
  Log.d(TAG, "No paired beacons — stopping idle foreground service")
235
240
  stopSelf()
241
+ } else {
242
+ // Prune enteredRegions for regions that are no longer monitored
243
+ enteredRegions.retainAll(monitoredRegionIds)
236
244
  }
237
245
  }
238
246
 
@@ -315,7 +323,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
315
323
  HysteresisAction.NONE -> {}
316
324
  }
317
325
  } else {
318
- // No valid beacon reading — track consecutive misses for exit detection
326
+ // No valid beacon reading — break distance hysteresis streaks and
327
+ // track consecutive misses for disappearance-based exit detection.
328
+ enterCounters[region.uniqueId] = 0
329
+ exitCounters[region.uniqueId] = 0
319
330
  val count = (missCounters[region.uniqueId] ?: 0) + 1
320
331
  missCounters[region.uniqueId] = count
321
332
 
@@ -570,26 +581,94 @@ class BeaconForegroundService : Service(), BeaconConsumer {
570
581
  }
571
582
 
572
583
  private fun buildForegroundNotification(): Notification {
573
- val config = readNotificationConfig()
574
- val fgConfig = config.optJSONObject("foregroundService")
584
+ return Companion.buildForegroundNotification(this)
585
+ }
575
586
 
576
- val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() }
577
- ?: "Beacon Monitoring Active"
578
- val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
579
- ?: "Monitoring for iBeacons in the background"
580
- val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
581
- val iconResId = iconName?.let { name ->
582
- try { resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 } }
583
- catch (_: Exception) { null }
584
- } ?: android.R.drawable.ic_dialog_info
587
+ companion object {
588
+ private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
585
589
 
586
- return NotificationCompat.Builder(this, CHANNEL_ID)
587
- .setSmallIcon(iconResId)
588
- .setContentTitle(title)
589
- .setContentText(text)
590
- .setPriority(NotificationCompat.PRIORITY_LOW)
591
- .setOngoing(true)
592
- .build()
590
+ fun start(context: Context) {
591
+ context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
592
+ .edit().putBoolean("active", true).apply()
593
+ val intent = Intent(context, BeaconForegroundService::class.java)
594
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
595
+ context.startForegroundService(intent)
596
+ } else {
597
+ context.startService(intent)
598
+ }
599
+ }
600
+
601
+ fun stop(context: Context) {
602
+ context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
603
+ .edit().putBoolean("active", false).apply()
604
+ context.stopService(Intent(context, BeaconForegroundService::class.java))
605
+ }
606
+
607
+ fun isMonitoringActive(context: Context): Boolean {
608
+ return context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
609
+ .getBoolean("active", false)
610
+ }
611
+
612
+ /**
613
+ * Ensure the notification channel exists. Must be called before building
614
+ * a notification from a non-service context (e.g. ExpoBeaconModule).
615
+ */
616
+ fun ensureNotificationChannel(context: Context) {
617
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
618
+ val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
619
+ .getString("config", null)
620
+ val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
621
+ val channelConfig = config.optJSONObject("channel")
622
+
623
+ val channelName = channelConfig?.optString("name")?.takeIf { it.isNotEmpty() }
624
+ ?: "Beacon Monitoring"
625
+ val channelDesc = channelConfig?.optString("description")?.takeIf { it.isNotEmpty() }
626
+ ?: "Used for background iBeacon region monitoring"
627
+ val importance = when (channelConfig?.optString("importance")) {
628
+ "high" -> NotificationManager.IMPORTANCE_HIGH
629
+ "default" -> NotificationManager.IMPORTANCE_DEFAULT
630
+ else -> NotificationManager.IMPORTANCE_LOW
631
+ }
632
+
633
+ val notifMgr = context.getSystemService(NotificationManager::class.java)
634
+ if (notifMgr?.getNotificationChannel(CHANNEL_ID) == null) {
635
+ val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply {
636
+ description = channelDesc
637
+ }
638
+ notifMgr?.createNotificationChannel(channel)
639
+ }
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Build the foreground notification from any Context (service or module).
645
+ * Shared so that ExpoBeaconModule can pass the same notification to
646
+ * enableForegroundServiceScanning() before the service starts.
647
+ */
648
+ fun buildForegroundNotification(context: Context): Notification {
649
+ val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
650
+ .getString("config", null)
651
+ val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
652
+ val fgConfig = config.optJSONObject("foregroundService")
653
+
654
+ val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() }
655
+ ?: "Beacon Monitoring Active"
656
+ val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
657
+ ?: "Monitoring for iBeacons in the background"
658
+ val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
659
+ val iconResId = iconName?.let { name ->
660
+ try { context.resources.getIdentifier(name, "drawable", context.packageName).takeIf { it != 0 } }
661
+ catch (_: Exception) { null }
662
+ } ?: android.R.drawable.ic_dialog_info
663
+
664
+ return NotificationCompat.Builder(context, CHANNEL_ID)
665
+ .setSmallIcon(iconResId)
666
+ .setContentTitle(title)
667
+ .setContentText(text)
668
+ .setPriority(NotificationCompat.PRIORITY_LOW)
669
+ .setOngoing(true)
670
+ .build()
671
+ }
593
672
  }
594
673
 
595
674
  private fun readNotificationConfig(): org.json.JSONObject {
@@ -599,6 +678,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
599
678
  }
600
679
 
601
680
  override fun onDestroy() {
681
+ serviceConnected = false
602
682
  timeoutHandler.removeCallbacksAndMessages(null)
603
683
  timeoutRunnables.clear()
604
684
  beaconTimeouts.clear()
@@ -620,6 +700,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
620
700
  monitoredRegions.forEach {
621
701
  try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
622
702
  }
703
+ try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
623
704
  beaconManager.unbind(this)
624
705
  super.onDestroy()
625
706
  }
@@ -7,7 +7,9 @@ import android.content.IntentFilter
7
7
  import android.content.ServiceConnection
8
8
  import android.content.SharedPreferences
9
9
  import android.content.pm.PackageManager
10
+ import android.net.Uri
10
11
  import android.os.Build
12
+ import android.os.PowerManager
11
13
  import android.os.RemoteException
12
14
  import androidx.core.content.ContextCompat
13
15
  import expo.modules.interfaces.permissions.PermissionsStatus
@@ -343,6 +345,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
343
345
  return@AsyncFunction
344
346
  }
345
347
  BeaconForegroundService.stop(ctx)
348
+ try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
346
349
  unregisterEventReceiver()
347
350
  promise.resolve(null)
348
351
  }
@@ -431,6 +434,43 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
431
434
  BeaconEventLogger.deleteLogDatabase(ctx)
432
435
  }
433
436
 
437
+ // MARK: - Battery Optimization
438
+
439
+ Function("isBatteryOptimizationExempt") {
440
+ val ctx = appContext.reactContext ?: return@Function false
441
+ val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
442
+ ?: return@Function false
443
+ pm.isIgnoringBatteryOptimizations(ctx.packageName)
444
+ }
445
+
446
+ AsyncFunction("requestBatteryOptimizationExemption") { promise: Promise ->
447
+ val ctx = appContext.reactContext ?: run {
448
+ promise.reject("NO_CONTEXT", "React context is not available", null)
449
+ return@AsyncFunction
450
+ }
451
+ val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
452
+ if (pm == null) {
453
+ promise.resolve(false)
454
+ return@AsyncFunction
455
+ }
456
+ if (pm.isIgnoringBatteryOptimizations(ctx.packageName)) {
457
+ promise.resolve(true)
458
+ return@AsyncFunction
459
+ }
460
+ try {
461
+ val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
462
+ data = Uri.parse("package:${ctx.packageName}")
463
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
464
+ }
465
+ ctx.startActivity(intent)
466
+ // The system dialog is fire-and-forget; we cannot observe the result
467
+ // from a non-Activity context. Resolve true to indicate the dialog was shown.
468
+ promise.resolve(true)
469
+ } catch (e: Exception) {
470
+ promise.reject("BATTERY_OPT_ERROR", "Failed to open battery optimization settings: ${e.message}", e)
471
+ }
472
+ }
473
+
434
474
  OnDestroy {
435
475
  with(this@ExpoBeaconModule) {
436
476
  unregisterEventReceiver()
@@ -77,6 +77,18 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
77
77
  cancelScan(): void;
78
78
  /** Request Bluetooth + Location permissions. Returns true if granted. */
79
79
  requestPermissionsAsync(): Promise<boolean>;
80
+ /**
81
+ * Check whether the app is exempt from Android battery optimizations.
82
+ * Always returns true on iOS and web (no equivalent concept).
83
+ */
84
+ isBatteryOptimizationExempt(): boolean;
85
+ /**
86
+ * Request exemption from Android battery optimizations.
87
+ * Opens the system dialog asking the user to whitelist this app.
88
+ * Returns true if the dialog was shown (or already exempt), false on failure.
89
+ * Always resolves true on iOS and web.
90
+ */
91
+ requestBatteryOptimizationExemption(): Promise<boolean>;
80
92
  /** Enable SQLite event logging. All beacon events will be persisted to a local database. */
81
93
  enableEventLogging(): void;
82
94
  /** Disable event logging. Previously logged events are retained. */
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI;IAElB,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAE3C,4FAA4F;IAC5F,kBAAkB,IAAI,IAAI;IAE1B,oEAAoE;IACpE,mBAAmB,IAAI,IAAI;IAE3B;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;CACzB;;AAED,wBAAmE"}
1
+ {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI;IAElB,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAE3C;;;OAGG;IACH,2BAA2B,IAAI,OAAO;IAEtC;;;;;OAKG;IACH,mCAAmC,IAAI,OAAO,CAAC,OAAO,CAAC;IAEvD,4FAA4F;IAC5F,kBAAkB,IAAI,IAAI;IAE1B,oEAAoE;IACpE,mBAAmB,IAAI,IAAI;IAE3B;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;CACzB;;AAED,wBAAmE"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+IzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
1
+ {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA6JzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /**\r\n * Check whether the app is exempt from Android battery optimizations.\r\n * Always returns true on iOS and web (no equivalent concept).\r\n */\r\n isBatteryOptimizationExempt(): boolean;\r\n\r\n /**\r\n * Request exemption from Android battery optimizations.\r\n * Opens the system dialog asking the user to whitelist this app.\r\n * Returns true if the dialog was shown (or already exempt), false on failure.\r\n * Always resolves true on iOS and web.\r\n */\r\n requestBatteryOptimizationExemption(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
@@ -20,6 +20,8 @@ declare const stub: {
20
20
  getEventLogs: (_options?: EventLogQueryOptions) => EventLogEntry[];
21
21
  clearEventLogs: () => void;
22
22
  destroyEventLogs: () => void;
23
+ isBatteryOptimizationExempt: () => boolean;
24
+ requestBatteryOptimizationExemption: () => Promise<boolean>;
23
25
  addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => {
24
26
  remove: () => void;
25
27
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,QAAA,MAAM,IAAI;kCAEE,MAAM,EAAE,kBACA,MAAM,KACrB,OAAO,CAAC,gBAAgB,EAAE,CAAC;6CAEZ,MAAM,KACrB,OAAO,CAAC,mBAAmB,EAAE,CAAC;8BAElB,MAAM,SACZ,MAAM,UACL,MAAM,UACN,MAAM,KACb,IAAI;gCACqB,MAAM,KAAG,IAAI;4BACnB,YAAY,EAAE;iCAErB,MAAM,cACP,MAAM,aACP,MAAM,KAChB,IAAI;mCACwB,MAAM,KAAG,IAAI;+BACnB,eAAe,EAAE;2BACrB,OAAO,CAAC,IAAI,CAAC;0BACd,OAAO,CAAC,IAAI,CAAC;+BACR,IAAI;8BACL,IAAI;sBACZ,IAAI;qCACa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAAI;mCAClC,OAAO,CAAC,OAAO,CAAC;8BACrB,IAAI;+BACH,IAAI;8BACH,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;8BACA,MAAM,sBAAsB,aAAa,GAAG;;;qCAGrC,MAAM,sBAAsB;CAC9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"ExpoBeaconModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,QAAA,MAAM,IAAI;kCAEE,MAAM,EAAE,kBACA,MAAM,KACrB,OAAO,CAAC,gBAAgB,EAAE,CAAC;6CAEZ,MAAM,KACrB,OAAO,CAAC,mBAAmB,EAAE,CAAC;8BAElB,MAAM,SACZ,MAAM,UACL,MAAM,UACN,MAAM,KACb,IAAI;gCACqB,MAAM,KAAG,IAAI;4BACnB,YAAY,EAAE;iCAErB,MAAM,cACP,MAAM,aACP,MAAM,KAChB,IAAI;mCACwB,MAAM,KAAG,IAAI;+BACnB,eAAe,EAAE;2BACrB,OAAO,CAAC,IAAI,CAAC;0BACd,OAAO,CAAC,IAAI,CAAC;+BACR,IAAI;8BACL,IAAI;sBACZ,IAAI;qCACa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAAI;mCAClC,OAAO,CAAC,OAAO,CAAC;8BACrB,IAAI;+BACH,IAAI;8BACH,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;uCACO,OAAO;+CACC,OAAO,CAAC,OAAO,CAAC;8BAC/B,MAAM,sBAAsB,aAAa,GAAG;;;qCAGrC,MAAM,sBAAsB;CAC9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -22,6 +22,8 @@ const stub = {
22
22
  getEventLogs: (_options) => notSupported(),
23
23
  clearEventLogs: () => notSupported(),
24
24
  destroyEventLogs: () => notSupported(),
25
+ isBatteryOptimizationExempt: () => true,
26
+ requestBatteryOptimizationExemption: () => Promise.resolve(true),
25
27
  addListener: (_eventName, _listener) => ({
26
28
  remove: () => { },
27
29
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAUA,MAAM,YAAY,GAAG,GAAU,EAAE;IAC/B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG;IACX,mBAAmB,EAAE,CACnB,MAAgB,EAChB,aAAsB,EACO,EAAE,CAAC,YAAY,EAAE;IAChD,sBAAsB,EAAE,CACtB,aAAsB,EACU,EAAE,CAAC,YAAY,EAAE;IACnD,UAAU,EAAE,CACV,WAAmB,EACnB,KAAa,EACb,MAAc,EACd,MAAc,EACR,EAAE,CAAC,YAAY,EAAE;IACzB,YAAY,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC3D,gBAAgB,EAAE,GAAmB,EAAE,CAAC,YAAY,EAAE;IACtD,aAAa,EAAE,CACb,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACX,EAAE,CAAC,YAAY,EAAE;IACzB,eAAe,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC9D,mBAAmB,EAAE,GAAsB,EAAE,CAAC,YAAY,EAAE;IAC5D,eAAe,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACpD,cAAc,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACnD,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,UAAU,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IACtC,qBAAqB,EAAE,CAAC,OAAgC,EAAQ,EAAE,CAAC,YAAY,EAAE;IACjF,uBAAuB,EAAE,GAAqB,EAAE,CAAC,YAAY,EAAE;IAC/D,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,YAAY,EAAE,CAAC,QAA+B,EAAmB,EAAE,CAAC,YAAY,EAAE;IAClF,cAAc,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC1C,gBAAgB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC5C,WAAW,EAAE,CAAC,UAAwC,EAAE,SAAc,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IACF,kBAAkB,EAAE,CAAC,UAAwC,EAAE,EAAE,GAAE,CAAC;CACrE,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\nconst notSupported = (): never => {\r\n throw new Error(\"expo-beacon is not supported on web.\");\r\n};\r\n\r\nconst stub = {\r\n scanForBeaconsAsync: (\r\n _uuids: string[],\r\n _scanDuration?: number,\r\n ): Promise<BeaconScanResult[]> => notSupported(),\r\n scanForEddystonesAsync: (\r\n _scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]> => notSupported(),\r\n pairBeacon: (\r\n _identifier: string,\r\n _uuid: string,\r\n _major: number,\r\n _minor: number,\r\n ): void => notSupported(),\r\n unpairBeacon: (_identifier: string): void => notSupported(),\r\n getPairedBeacons: (): PairedBeacon[] => notSupported(),\r\n pairEddystone: (\r\n _identifier: string,\r\n _namespace: string,\r\n _instance: string,\r\n ): void => notSupported(),\r\n unpairEddystone: (_identifier: string): void => notSupported(),\r\n getPairedEddystones: (): PairedEddystone[] => notSupported(),\r\n startMonitoring: (): Promise<void> => notSupported(),\r\n stopMonitoring: (): Promise<void> => notSupported(),\r\n startContinuousScan: (): void => notSupported(),\r\n stopContinuousScan: (): void => notSupported(),\r\n cancelScan: (): void => notSupported(),\r\n setNotificationConfig: (_config: Record<string, unknown>): void => notSupported(),\r\n requestPermissionsAsync: (): Promise<boolean> => notSupported(),\r\n enableEventLogging: (): void => notSupported(),\r\n disableEventLogging: (): void => notSupported(),\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({\r\n remove: () => {},\r\n }),\r\n removeAllListeners: (_eventName: keyof ExpoBeaconModuleEvents) => {},\r\n};\r\n\r\nexport default stub;\r\n"]}
1
+ {"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAUA,MAAM,YAAY,GAAG,GAAU,EAAE;IAC/B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG;IACX,mBAAmB,EAAE,CACnB,MAAgB,EAChB,aAAsB,EACO,EAAE,CAAC,YAAY,EAAE;IAChD,sBAAsB,EAAE,CACtB,aAAsB,EACU,EAAE,CAAC,YAAY,EAAE;IACnD,UAAU,EAAE,CACV,WAAmB,EACnB,KAAa,EACb,MAAc,EACd,MAAc,EACR,EAAE,CAAC,YAAY,EAAE;IACzB,YAAY,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC3D,gBAAgB,EAAE,GAAmB,EAAE,CAAC,YAAY,EAAE;IACtD,aAAa,EAAE,CACb,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACX,EAAE,CAAC,YAAY,EAAE;IACzB,eAAe,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC9D,mBAAmB,EAAE,GAAsB,EAAE,CAAC,YAAY,EAAE;IAC5D,eAAe,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACpD,cAAc,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACnD,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,UAAU,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IACtC,qBAAqB,EAAE,CAAC,OAAgC,EAAQ,EAAE,CAAC,YAAY,EAAE;IACjF,uBAAuB,EAAE,GAAqB,EAAE,CAAC,YAAY,EAAE;IAC/D,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,YAAY,EAAE,CAAC,QAA+B,EAAmB,EAAE,CAAC,YAAY,EAAE;IAClF,cAAc,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC1C,gBAAgB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC5C,2BAA2B,EAAE,GAAY,EAAE,CAAC,IAAI;IAChD,mCAAmC,EAAE,GAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IAClF,WAAW,EAAE,CAAC,UAAwC,EAAE,SAAc,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IACF,kBAAkB,EAAE,CAAC,UAAwC,EAAE,EAAE,GAAE,CAAC;CACrE,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\nconst notSupported = (): never => {\r\n throw new Error(\"expo-beacon is not supported on web.\");\r\n};\r\n\r\nconst stub = {\r\n scanForBeaconsAsync: (\r\n _uuids: string[],\r\n _scanDuration?: number,\r\n ): Promise<BeaconScanResult[]> => notSupported(),\r\n scanForEddystonesAsync: (\r\n _scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]> => notSupported(),\r\n pairBeacon: (\r\n _identifier: string,\r\n _uuid: string,\r\n _major: number,\r\n _minor: number,\r\n ): void => notSupported(),\r\n unpairBeacon: (_identifier: string): void => notSupported(),\r\n getPairedBeacons: (): PairedBeacon[] => notSupported(),\r\n pairEddystone: (\r\n _identifier: string,\r\n _namespace: string,\r\n _instance: string,\r\n ): void => notSupported(),\r\n unpairEddystone: (_identifier: string): void => notSupported(),\r\n getPairedEddystones: (): PairedEddystone[] => notSupported(),\r\n startMonitoring: (): Promise<void> => notSupported(),\r\n stopMonitoring: (): Promise<void> => notSupported(),\r\n startContinuousScan: (): void => notSupported(),\r\n stopContinuousScan: (): void => notSupported(),\r\n cancelScan: (): void => notSupported(),\r\n setNotificationConfig: (_config: Record<string, unknown>): void => notSupported(),\r\n requestPermissionsAsync: (): Promise<boolean> => notSupported(),\r\n enableEventLogging: (): void => notSupported(),\r\n disableEventLogging: (): void => notSupported(),\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n isBatteryOptimizationExempt: (): boolean => true,\r\n requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),\r\n addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({\r\n remove: () => {},\r\n }),\r\n removeAllListeners: (_eventName: keyof ExpoBeaconModuleEvents) => {},\r\n};\r\n\r\nexport default stub;\r\n"]}
@@ -13,8 +13,10 @@ private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
13
13
  private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
14
14
 
15
15
  /// Number of consecutive ranging misses before emitting a distance-based exit event.
16
- /// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
17
- private let EXIT_MISS_THRESHOLD = 3
16
+ /// With ~1 s CoreLocation ranging cycles, 10 misses ≈ 10 s of silence before
17
+ /// declaring exit tolerant of brief ranging gaps while still responsive.
18
+ /// NOTE: Android uses the same value but with a different scan cycle (~2.1 s).
19
+ private let EXIT_MISS_THRESHOLD = 10
18
20
  /// Number of consecutive readings required to confirm a distance-based enter or exit transition.
19
21
  /// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
20
22
  private let HYSTERESIS_COUNT = 3
@@ -22,7 +24,8 @@ private let HYSTERESIS_COUNT = 3
22
24
  /// Eddystone monitoring timer interval in seconds.
23
25
  private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
24
26
  /// Maximum age (in seconds) before a beacon is considered "not recently seen".
25
- private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 3.0
27
+ /// Set higher than a few tick intervals to tolerate brief BLE advertisement gaps.
28
+ private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 8.0
26
29
  /// Minimum interval between consecutive distance event emissions per identifier.
27
30
  private let DISTANCE_EVENT_THROTTLE_INTERVAL: TimeInterval = 1.0
28
31
 
@@ -451,6 +454,16 @@ public class ExpoBeaconModule: Module {
451
454
  self.eventLogger = nil
452
455
  }
453
456
 
457
+ // MARK: - Battery Optimization (Android-only; no-op on iOS)
458
+
459
+ Function("isBatteryOptimizationExempt") { () -> Bool in
460
+ return true
461
+ }
462
+
463
+ AsyncFunction("requestBatteryOptimizationExemption") { (promise: Promise) in
464
+ promise.resolve(true)
465
+ }
466
+
454
467
  // MARK: - Lifecycle
455
468
 
456
469
  OnDestroy {
@@ -988,7 +1001,10 @@ public class ExpoBeaconModule: Module {
988
1001
  continue
989
1002
  }
990
1003
 
991
- // Not seen recently — increment miss counter
1004
+ // Not seen recently — break distance hysteresis streaks and, if the
1005
+ // beacon was already entered, increment miss counter toward exit.
1006
+ eddystoneEnterCounters[identifier] = 0
1007
+ eddystoneExitCounters[identifier] = 0
992
1008
  guard eddystoneEnteredRegions.contains(identifier) else { continue }
993
1009
 
994
1010
  let count = (eddystoneMissCounters[identifier] ?? 0) + 1
@@ -1263,6 +1279,8 @@ public class ExpoBeaconModule: Module {
1263
1279
  // duplicate events when both monitoring and continuous scan are active.
1264
1280
  } else {
1265
1281
  // No valid beacon reading — beacon may have disappeared
1282
+ enterCounters[identifier] = 0
1283
+ exitCounters[identifier] = 0
1266
1284
  let count = (missCounters[identifier] ?? 0) + 1
1267
1285
  missCounters[identifier] = count
1268
1286
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.4",
3
+ "version": "0.6.7",
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",
@@ -122,6 +122,20 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
122
122
  /** Request Bluetooth + Location permissions. Returns true if granted. */
123
123
  requestPermissionsAsync(): Promise<boolean>;
124
124
 
125
+ /**
126
+ * Check whether the app is exempt from Android battery optimizations.
127
+ * Always returns true on iOS and web (no equivalent concept).
128
+ */
129
+ isBatteryOptimizationExempt(): boolean;
130
+
131
+ /**
132
+ * Request exemption from Android battery optimizations.
133
+ * Opens the system dialog asking the user to whitelist this app.
134
+ * Returns true if the dialog was shown (or already exempt), false on failure.
135
+ * Always resolves true on iOS and web.
136
+ */
137
+ requestBatteryOptimizationExemption(): Promise<boolean>;
138
+
125
139
  /** Enable SQLite event logging. All beacon events will be persisted to a local database. */
126
140
  enableEventLogging(): void;
127
141
 
@@ -47,6 +47,8 @@ const stub = {
47
47
  getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),
48
48
  clearEventLogs: (): void => notSupported(),
49
49
  destroyEventLogs: (): void => notSupported(),
50
+ isBatteryOptimizationExempt: (): boolean => true,
51
+ requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),
50
52
  addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({
51
53
  remove: () => {},
52
54
  }),