expo-beacon 0.6.4 → 0.6.6

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()
@@ -315,7 +314,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
315
314
  HysteresisAction.NONE -> {}
316
315
  }
317
316
  } else {
318
- // No valid beacon reading — track consecutive misses for exit detection
317
+ // No valid beacon reading — break distance hysteresis streaks and
318
+ // track consecutive misses for disappearance-based exit detection.
319
+ enterCounters[region.uniqueId] = 0
320
+ exitCounters[region.uniqueId] = 0
319
321
  val count = (missCounters[region.uniqueId] ?: 0) + 1
320
322
  missCounters[region.uniqueId] = count
321
323
 
@@ -570,26 +572,94 @@ class BeaconForegroundService : Service(), BeaconConsumer {
570
572
  }
571
573
 
572
574
  private fun buildForegroundNotification(): Notification {
573
- val config = readNotificationConfig()
574
- val fgConfig = config.optJSONObject("foregroundService")
575
+ return Companion.buildForegroundNotification(this)
576
+ }
575
577
 
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
578
+ companion object {
579
+ private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
585
580
 
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()
581
+ fun start(context: Context) {
582
+ context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
583
+ .edit().putBoolean("active", true).apply()
584
+ val intent = Intent(context, BeaconForegroundService::class.java)
585
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
586
+ context.startForegroundService(intent)
587
+ } else {
588
+ context.startService(intent)
589
+ }
590
+ }
591
+
592
+ fun stop(context: Context) {
593
+ context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
594
+ .edit().putBoolean("active", false).apply()
595
+ context.stopService(Intent(context, BeaconForegroundService::class.java))
596
+ }
597
+
598
+ fun isMonitoringActive(context: Context): Boolean {
599
+ return context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
600
+ .getBoolean("active", false)
601
+ }
602
+
603
+ /**
604
+ * Ensure the notification channel exists. Must be called before building
605
+ * a notification from a non-service context (e.g. ExpoBeaconModule).
606
+ */
607
+ fun ensureNotificationChannel(context: Context) {
608
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
609
+ val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
610
+ .getString("config", null)
611
+ val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
612
+ val channelConfig = config.optJSONObject("channel")
613
+
614
+ val channelName = channelConfig?.optString("name")?.takeIf { it.isNotEmpty() }
615
+ ?: "Beacon Monitoring"
616
+ val channelDesc = channelConfig?.optString("description")?.takeIf { it.isNotEmpty() }
617
+ ?: "Used for background iBeacon region monitoring"
618
+ val importance = when (channelConfig?.optString("importance")) {
619
+ "high" -> NotificationManager.IMPORTANCE_HIGH
620
+ "default" -> NotificationManager.IMPORTANCE_DEFAULT
621
+ else -> NotificationManager.IMPORTANCE_LOW
622
+ }
623
+
624
+ val notifMgr = context.getSystemService(NotificationManager::class.java)
625
+ if (notifMgr?.getNotificationChannel(CHANNEL_ID) == null) {
626
+ val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply {
627
+ description = channelDesc
628
+ }
629
+ notifMgr?.createNotificationChannel(channel)
630
+ }
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Build the foreground notification from any Context (service or module).
636
+ * Shared so that ExpoBeaconModule can pass the same notification to
637
+ * enableForegroundServiceScanning() before the service starts.
638
+ */
639
+ fun buildForegroundNotification(context: Context): Notification {
640
+ val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
641
+ .getString("config", null)
642
+ val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
643
+ val fgConfig = config.optJSONObject("foregroundService")
644
+
645
+ val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() }
646
+ ?: "Beacon Monitoring Active"
647
+ val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
648
+ ?: "Monitoring for iBeacons in the background"
649
+ val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
650
+ val iconResId = iconName?.let { name ->
651
+ try { context.resources.getIdentifier(name, "drawable", context.packageName).takeIf { it != 0 } }
652
+ catch (_: Exception) { null }
653
+ } ?: android.R.drawable.ic_dialog_info
654
+
655
+ return NotificationCompat.Builder(context, CHANNEL_ID)
656
+ .setSmallIcon(iconResId)
657
+ .setContentTitle(title)
658
+ .setContentText(text)
659
+ .setPriority(NotificationCompat.PRIORITY_LOW)
660
+ .setOngoing(true)
661
+ .build()
662
+ }
593
663
  }
594
664
 
595
665
  private fun readNotificationConfig(): org.json.JSONObject {
@@ -599,6 +669,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
599
669
  }
600
670
 
601
671
  override fun onDestroy() {
672
+ serviceConnected = false
602
673
  timeoutHandler.removeCallbacksAndMessages(null)
603
674
  timeoutRunnables.clear()
604
675
  beaconTimeouts.clear()
@@ -620,6 +691,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
620
691
  monitoredRegions.forEach {
621
692
  try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
622
693
  }
694
+ try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
623
695
  beaconManager.unbind(this)
624
696
  super.onDestroy()
625
697
  }
@@ -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
@@ -326,6 +328,21 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
326
328
  return@AsyncFunction
327
329
  }
328
330
 
331
+ // Enable AltBeacon foreground service scanning so the internal BeaconService
332
+ // runs as a foreground service, preventing Samsung/OEM BLE throttling.
333
+ // Reset first to clear stale state from a previous session / hot-reload.
334
+ if (!isBoundForScan) {
335
+ try {
336
+ BeaconForegroundService.ensureNotificationChannel(ctx)
337
+ try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
338
+ beaconManager.enableForegroundServiceScanning(
339
+ BeaconForegroundService.buildForegroundNotification(ctx), FOREGROUND_NOTIF_ID
340
+ )
341
+ } catch (_: IllegalStateException) {
342
+ // Already bound — service onCreate() will enable it instead.
343
+ }
344
+ }
345
+
329
346
  registerEventReceiver()
330
347
  BeaconForegroundService.start(ctx)
331
348
  promise.resolve(null)
@@ -343,6 +360,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
343
360
  return@AsyncFunction
344
361
  }
345
362
  BeaconForegroundService.stop(ctx)
363
+ try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
346
364
  unregisterEventReceiver()
347
365
  promise.resolve(null)
348
366
  }
@@ -431,6 +449,43 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
431
449
  BeaconEventLogger.deleteLogDatabase(ctx)
432
450
  }
433
451
 
452
+ // MARK: - Battery Optimization
453
+
454
+ Function("isBatteryOptimizationExempt") {
455
+ val ctx = appContext.reactContext ?: return@Function false
456
+ val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
457
+ ?: return@Function false
458
+ pm.isIgnoringBatteryOptimizations(ctx.packageName)
459
+ }
460
+
461
+ AsyncFunction("requestBatteryOptimizationExemption") { promise: Promise ->
462
+ val ctx = appContext.reactContext ?: run {
463
+ promise.reject("NO_CONTEXT", "React context is not available", null)
464
+ return@AsyncFunction
465
+ }
466
+ val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
467
+ if (pm == null) {
468
+ promise.resolve(false)
469
+ return@AsyncFunction
470
+ }
471
+ if (pm.isIgnoringBatteryOptimizations(ctx.packageName)) {
472
+ promise.resolve(true)
473
+ return@AsyncFunction
474
+ }
475
+ try {
476
+ val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
477
+ data = Uri.parse("package:${ctx.packageName}")
478
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
479
+ }
480
+ ctx.startActivity(intent)
481
+ // The system dialog is fire-and-forget; we cannot observe the result
482
+ // from a non-Activity context. Resolve true to indicate the dialog was shown.
483
+ promise.resolve(true)
484
+ } catch (e: Exception) {
485
+ promise.reject("BATTERY_OPT_ERROR", "Failed to open battery optimization settings: ${e.message}", e)
486
+ }
487
+ }
488
+
434
489
  OnDestroy {
435
490
  with(this@ExpoBeaconModule) {
436
491
  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.6",
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
  }),