expo-beacon 0.6.13 → 0.6.15

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.
@@ -37,6 +37,12 @@ internal const val RECENT_RANGING_SIGHTING_GRACE_MS = 25000L
37
37
  */
38
38
  internal const val EXIT_MISS_THRESHOLD = 10
39
39
 
40
+ /**
41
+ * Milliseconds of no valid BLE readings before starting the timeout countdown.
42
+ * Acts as a safety net when ranging cycles stop entirely (e.g. Doze mode).
43
+ */
44
+ internal const val DISTANCE_INACTIVITY_MS = 60_000L
45
+
40
46
  /** Number of consecutive readings required to confirm a distance-based enter or exit transition. */
41
47
  internal const val HYSTERESIS_COUNT = 3
42
48
 
@@ -64,8 +64,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
64
64
  private val timeoutRunnables = java.util.concurrent.ConcurrentHashMap<String, Runnable>()
65
65
  // Per-beacon timeout seconds lookup (identifier → seconds), loaded from paired data
66
66
  private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
67
+ // Inactivity timers — start timeout countdown when no BLE readings for 60 s
68
+ private val inactivityRunnables = java.util.concurrent.ConcurrentHashMap<String, Runnable>()
67
69
  private var eventLogger: BeaconEventLogger? = null
68
70
  private var apiForwarder: BeaconApiForwarder? = null
71
+ // Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
72
+ @Volatile private var eventLevel: String = "all"
69
73
 
70
74
  override fun onCreate() {
71
75
  super.onCreate()
@@ -125,6 +129,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
125
129
  maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
126
130
  exitDistance = optPrefs.getString("exit_distance", null)?.toDoubleOrNull()
127
131
  minRssiThreshold = optPrefs.getInt("min_rssi", DEFAULT_MIN_RSSI)
132
+ eventLevel = optPrefs.getString("level", "all") ?: "all"
128
133
 
129
134
  beaconManager.addMonitorNotifier(monitorNotifier)
130
135
  beaconManager.addRangeNotifier(rangeNotifier)
@@ -174,6 +179,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
174
179
  lastSeenAtMs.clear()
175
180
  timeoutHandler.removeCallbacksAndMessages(null)
176
181
  timeoutRunnables.clear()
182
+ inactivityRunnables.clear()
177
183
  synchronized(distanceLock) {
178
184
  enterCounters.clear()
179
185
  exitCounters.clear()
@@ -256,10 +262,13 @@ class BeaconForegroundService : Service(), BeaconConsumer {
256
262
 
257
263
  // Distance logging only — emits distance broadcasts. Enter/exit logic lives in rangeNotifier.
258
264
  private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
265
+ if (eventLevel != "all") return@RangeNotifier
259
266
  if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
260
267
  val closest = beacons.filter { it.distance >= 0 && it.rssi >= minRssiThreshold }.minByOrNull { it.distance }
261
268
  if (closest != null) {
262
269
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
270
+ // Valid BLE reading — reset inactivity timer.
271
+ rescheduleInactivity(region)
263
272
  sendBeaconBroadcast(region, "distance", closest.distance, closest.rssi)
264
273
  }
265
274
  }
@@ -290,7 +299,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
290
299
  if (wasEntered) {
291
300
  sendBeaconBroadcast(region, "exit", -1.0)
292
301
  showEnterExitNotification(region, "exit")
293
- // OS-level exit safety net — start the timeout clock.
302
+ // OS-level exit safety net — cancel inactivity timer and start the timeout clock.
303
+ cancelInactivity(region.uniqueId)
294
304
  scheduleTimeoutIfConfigured(region)
295
305
  }
296
306
  }
@@ -316,13 +326,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
316
326
  // Got a valid reading — reset miss counter
317
327
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
318
328
  missCounters[region.uniqueId] = 0
329
+ // Valid BLE reading — reset inactivity timer.
330
+ rescheduleInactivity(region)
319
331
 
320
- // Apply EMA smoothing; jump guard returns null for outliers
332
+ // Apply EMA smoothing; jump resets EMA to the new value
321
333
  val smoothed = smoothDistance(region.uniqueId, beacon.distance)
322
- if (smoothed == null) {
323
- // Outlier — treat as miss without resetting enter counter
324
- return@RangeNotifier
325
- }
326
334
 
327
335
  val action = evaluateDistanceHysteresis(region.uniqueId, smoothed, maxDist)
328
336
  when (action) {
@@ -337,7 +345,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
337
345
  enteredRegions.remove(region.uniqueId)
338
346
  sendBeaconBroadcast(region, "exit", beacon.distance, beacon.rssi)
339
347
  showEnterExitNotification(region, "exit")
340
- // Beacon left — start the timeout clock.
348
+ // Beacon left — cancel inactivity timer and start the timeout clock.
349
+ cancelInactivity(region.uniqueId)
341
350
  scheduleTimeoutIfConfigured(region)
342
351
  }
343
352
  HysteresisAction.NONE -> {}
@@ -357,7 +366,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
357
366
  exitCounters[region.uniqueId] = 0
358
367
  sendBeaconBroadcast(region, "exit", -1.0)
359
368
  showEnterExitNotification(region, "exit")
360
- // Beacon disappeared — start the timeout clock.
369
+ // Beacon disappeared — cancel inactivity timer and start the timeout clock.
370
+ cancelInactivity(region.uniqueId)
361
371
  scheduleTimeoutIfConfigured(region)
362
372
  }
363
373
  }
@@ -370,18 +380,23 @@ class BeaconForegroundService : Service(), BeaconConsumer {
370
380
 
371
381
  /**
372
382
  * Apply exponential moving average (EMA) smoothing to a raw distance reading.
373
- * Returns null if the reading is a jump outlier (raw differs from smoothed by > DISTANCE_JUMP_FACTOR).
383
+ * If the reading is a large jump (> DISTANCE_JUMP_FACTOR), resets the EMA to the new
384
+ * value instead of rejecting it — this ensures the hysteresis pipeline keeps receiving
385
+ * data and can fire exit events when the user moves away from a beacon, rather than
386
+ * freezing because the EMA is stuck at the old close-range value.
374
387
  */
375
- private fun smoothDistance(regionId: String, rawDistance: Double): Double? {
388
+ private fun smoothDistance(regionId: String, rawDistance: Double): Double {
376
389
  val prev = smoothedDistances[regionId]
377
390
  if (prev == null) {
378
391
  smoothedDistances[regionId] = rawDistance
379
392
  return rawDistance
380
393
  }
381
- // Jump guard: if the raw value is wildly different, treat as outlier
394
+ // Jump guard: if the raw value is wildly different, reset EMA to the new reading
395
+ // so the hysteresis pipeline keeps receiving data and can fire the exit event.
382
396
  val ratio = if (prev > 0.001) rawDistance / prev else rawDistance
383
397
  if (ratio > DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / DISTANCE_JUMP_FACTOR)) {
384
- return null
398
+ smoothedDistances[regionId] = rawDistance
399
+ return rawDistance
385
400
  }
386
401
  val smoothed = DISTANCE_EMA_ALPHA * rawDistance + (1 - DISTANCE_EMA_ALPHA) * prev
387
402
  smoothedDistances[regionId] = smoothed
@@ -472,6 +487,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
472
487
  timeoutRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
473
488
  }
474
489
 
490
+ // MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
491
+
492
+ private fun rescheduleInactivity(region: Region) {
493
+ val regionId = region.uniqueId
494
+ if (!beaconTimeouts.containsKey(regionId)) return
495
+ cancelInactivity(regionId)
496
+ val runnable = Runnable {
497
+ inactivityRunnables.remove(regionId)
498
+ // No BLE readings for 60 s — start the configured timeout countdown.
499
+ scheduleTimeoutIfConfigured(region)
500
+ }
501
+ inactivityRunnables[regionId] = runnable
502
+ timeoutHandler.postDelayed(runnable, DISTANCE_INACTIVITY_MS)
503
+ }
504
+
505
+ private fun cancelInactivity(regionId: String) {
506
+ inactivityRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
507
+ }
508
+
475
509
  private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double, rssi: Int = 0) {
476
510
  // Determine if this is an Eddystone region based on identifier format
477
511
  // Eddystone regions have id1 as a hex namespace (not a UUID)
@@ -500,10 +534,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
500
534
  }
501
535
  monitoringEventName(isEddystone, eventType)?.let { logBeaconEvent(it, params) }
502
536
 
503
- // Forward enter/exit/timeout events to remote API (skip distance — too frequent)
504
- if (eventType != "distance") {
505
- apiForwarder?.forwardEvent(params)
506
- }
537
+ // Forward all produced events to remote API
538
+ apiForwarder?.forwardEvent(params)
507
539
 
508
540
  val intent = Intent(ACTION_BEACON_EVENT).apply {
509
541
  putExtra("identifier", region.uniqueId)
@@ -733,6 +765,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
733
765
  serviceConnected = false
734
766
  timeoutHandler.removeCallbacksAndMessages(null)
735
767
  timeoutRunnables.clear()
768
+ inactivityRunnables.clear()
736
769
  beaconTimeouts.clear()
737
770
  lastSeenAtMs.clear()
738
771
  monitoredRegionIds.clear()
@@ -282,6 +282,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
282
282
  var maxDistance: Double? = null
283
283
  var exitDistance: Double? = null
284
284
  var minRssi: Int? = null
285
+ var level: String = "all"
285
286
  when (options) {
286
287
  is Double -> maxDistance = options
287
288
  is Map<*, *> -> {
@@ -290,6 +291,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
290
291
  maxDistance = (map["maxDistance"] as? Number)?.toDouble()
291
292
  exitDistance = (map["exitDistance"] as? Number)?.toDouble()
292
293
  minRssi = (map["minRssi"] as? Number)?.toInt()
294
+ level = (map["level"] as? String) ?: "all"
293
295
  val notifications = map["notifications"]
294
296
  if (notifications is Map<*, *>) {
295
297
  @Suppress("UNCHECKED_CAST")
@@ -322,6 +324,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
322
324
  else remove("exit_distance")
323
325
  if (minRssi != null) putInt("min_rssi", minRssi)
324
326
  else remove("min_rssi")
327
+ putString("level", level)
325
328
  }.apply()
326
329
  // Verify we have the permissions needed for background monitoring
327
330
  val hasLocation = ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
@@ -26,6 +26,9 @@ export type PairedBeacon = {
26
26
  * Timeout in seconds. When set, the module fires `onBeaconTimeout` once
27
27
  * after the beacon has been continuously in range for this duration.
28
28
  * The timer resets if the beacon exits and re-enters range.
29
+ *
30
+ * The timeout countdown also starts if no BLE readings are received
31
+ * for 60 seconds (e.g. due to Doze mode or background throttling).
29
32
  */
30
33
  timeoutSeconds?: number;
31
34
  };
@@ -132,6 +135,13 @@ export type MonitoringOptions = {
132
135
  * Default: -85. Typical range: -100 (very permissive) to -70 (strict).
133
136
  */
134
137
  minRssi?: number;
138
+ /**
139
+ * Controls which event types are emitted, logged, and forwarded to the API.
140
+ *
141
+ * - `'all'` (default): distance + enter + exit + timeout events.
142
+ * - `'events'`: enter + exit + timeout only (no distance events).
143
+ */
144
+ level?: 'all' | 'events';
135
145
  /** Notification configuration overrides to apply for this monitoring session. */
136
146
  notifications?: NotificationConfig;
137
147
  };
@@ -170,6 +180,9 @@ export type PairedEddystone = {
170
180
  * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
171
181
  * after the beacon has been continuously in range for this duration.
172
182
  * The timer resets if the beacon exits and re-enters range.
183
+ *
184
+ * The timeout countdown also starts if no BLE readings are received
185
+ * for 60 seconds (e.g. due to Doze mode or background throttling).
173
186
  */
174
187
  timeoutSeconds?: number;
175
188
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for beacon timeout events (beacon in range for configured duration). */\r\nexport type BeaconTimeoutEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Configuration for beacon enter/exit event notifications. */\r\nexport type BeaconNotificationConfig = {\r\n /** Whether to show enter/exit notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on beacon enter. Default: \"Beacon Entered\". */\r\n enterTitle?: string;\r\n /** Notification title on beacon exit. Default: \"Beacon Exited\". */\r\n exitTitle?: string;\r\n /**\r\n * Notification body template. Supports {identifier} and {event} placeholders.\r\n * Default: \"{identifier} region {event}ed\".\r\n */\r\n body?: string;\r\n /** Play a sound with the notification (iOS only). Default: true. */\r\n sound?: boolean;\r\n /** Android drawable resource name for the notification icon (e.g. \"ic_notification\"). */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android foreground service notification (persistent status bar entry). */\r\nexport type ForegroundServiceConfig = {\r\n /** Title of the persistent notification. Default: \"Beacon Monitoring Active\". */\r\n title?: string;\r\n /** Body text of the persistent notification. Default: \"Monitoring for iBeacons in the background\". */\r\n text?: string;\r\n /** Android drawable resource name for the notification icon. */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android notification channel. */\r\nexport type NotificationChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"Beacon Monitoring\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"Used for background iBeacon region monitoring\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'low'.\r\n * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.\r\n */\r\n importance?: \"low\" | \"default\" | \"high\";\r\n};\r\n\r\n/** Combined notification configuration for all notification types. */\r\nexport type NotificationConfig = {\r\n /** Settings for beacon enter/exit event notifications. */\r\n beaconEvents?: BeaconNotificationConfig;\r\n /** Settings for the persistent foreground service notification (Android only). */\r\n foregroundService?: ForegroundServiceConfig;\r\n /** Settings for the Android notification channel (Android only). */\r\n channel?: NotificationChannelConfig;\r\n};\r\n\r\n/** Options accepted by startMonitoring(). */\r\nexport type MonitoringOptions = {\r\n /**\r\n * Maximum distance in metres for distance-based enter events.\r\n * Exit events are always emitted when the region is lost.\r\n */\r\n maxDistance?: number;\r\n /**\r\n * Distance in metres at which exit events fire (must be ≥ maxDistance).\r\n * Creates a hysteresis band between enter and exit thresholds to prevent\r\n * rapid toggling near the boundary.\r\n *\r\n * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.\r\n * Only used when `maxDistance` is set.\r\n */\r\n exitDistance?: number;\r\n /**\r\n * Minimum RSSI (dBm) for a beacon reading to be considered valid.\r\n * Readings below this threshold are discarded as unreliable, preventing\r\n * false detections from reflected or distant signals.\r\n *\r\n * Default: -85. Typical range: -100 (very permissive) to -70 (strict).\r\n */\r\n minRssi?: number;\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for Eddystone enter/exit region events. */\r\nexport type EddystoneRegionEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic Eddystone distance update events during monitoring. */\r\nexport type EddystoneDistanceEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for Eddystone timeout events (beacon in range for configured duration). */\r\nexport type EddystoneTimeoutEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired once after a paired beacon has been continuously in range for its configured `timeoutSeconds`. */\r\n onBeaconTimeout: (params: BeaconTimeoutEvent) => void;\r\n /** Fired continuously during a live scan as each iBeacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n /** Fired continuously during a live scan as each Eddystone beacon is detected. */\r\n onEddystoneFound: (params: EddystoneScanResult) => void;\r\n onEddystoneEnter: (params: EddystoneRegionEvent) => void;\r\n onEddystoneExit: (params: EddystoneRegionEvent) => void;\r\n onEddystoneDistance: (params: EddystoneDistanceEvent) => void;\r\n /** Fired once after a paired Eddystone has been continuously in range for its configured `timeoutSeconds`. */\r\n onEddystoneTimeout: (params: EddystoneTimeoutEvent) => void;\r\n};\r\n\r\n/** Options for filtering event logs. */\r\nexport type EventLogQueryOptions = {\r\n /** Maximum number of log entries to return (default: 1000, max: 10000). */\r\n limit?: number;\r\n /** Filter by event type (e.g. \"onBeaconEnter\", \"onBeaconExit\"). */\r\n eventType?: string;\r\n /** Only return events with timestamp >= this value (ms since epoch). */\r\n sinceTimestamp?: number;\r\n};\r\n\r\n/** A single logged beacon event entry. */\r\nexport type EventLogEntry = {\r\n id: number;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** The event type that was logged (e.g. \"onBeaconEnter\"). */\r\n eventType: string;\r\n /** Beacon identifier, if available. */\r\n identifier?: string;\r\n /** The full event payload that was sent to JS. */\r\n data: Record<string, unknown>;\r\n};\r\n"]}
1
+ {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * The timeout countdown also starts if no BLE readings are received\r\n * for 60 seconds (e.g. due to Doze mode or background throttling).\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for beacon timeout events (beacon in range for configured duration). */\r\nexport type BeaconTimeoutEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Configuration for beacon enter/exit event notifications. */\r\nexport type BeaconNotificationConfig = {\r\n /** Whether to show enter/exit notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on beacon enter. Default: \"Beacon Entered\". */\r\n enterTitle?: string;\r\n /** Notification title on beacon exit. Default: \"Beacon Exited\". */\r\n exitTitle?: string;\r\n /**\r\n * Notification body template. Supports {identifier} and {event} placeholders.\r\n * Default: \"{identifier} region {event}ed\".\r\n */\r\n body?: string;\r\n /** Play a sound with the notification (iOS only). Default: true. */\r\n sound?: boolean;\r\n /** Android drawable resource name for the notification icon (e.g. \"ic_notification\"). */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android foreground service notification (persistent status bar entry). */\r\nexport type ForegroundServiceConfig = {\r\n /** Title of the persistent notification. Default: \"Beacon Monitoring Active\". */\r\n title?: string;\r\n /** Body text of the persistent notification. Default: \"Monitoring for iBeacons in the background\". */\r\n text?: string;\r\n /** Android drawable resource name for the notification icon. */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android notification channel. */\r\nexport type NotificationChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"Beacon Monitoring\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"Used for background iBeacon region monitoring\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'low'.\r\n * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.\r\n */\r\n importance?: \"low\" | \"default\" | \"high\";\r\n};\r\n\r\n/** Combined notification configuration for all notification types. */\r\nexport type NotificationConfig = {\r\n /** Settings for beacon enter/exit event notifications. */\r\n beaconEvents?: BeaconNotificationConfig;\r\n /** Settings for the persistent foreground service notification (Android only). */\r\n foregroundService?: ForegroundServiceConfig;\r\n /** Settings for the Android notification channel (Android only). */\r\n channel?: NotificationChannelConfig;\r\n};\r\n\r\n/** Options accepted by startMonitoring(). */\r\nexport type MonitoringOptions = {\r\n /**\r\n * Maximum distance in metres for distance-based enter events.\r\n * Exit events are always emitted when the region is lost.\r\n */\r\n maxDistance?: number;\r\n /**\r\n * Distance in metres at which exit events fire (must be ≥ maxDistance).\r\n * Creates a hysteresis band between enter and exit thresholds to prevent\r\n * rapid toggling near the boundary.\r\n *\r\n * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.\r\n * Only used when `maxDistance` is set.\r\n */\r\n exitDistance?: number;\r\n /**\r\n * Minimum RSSI (dBm) for a beacon reading to be considered valid.\r\n * Readings below this threshold are discarded as unreliable, preventing\r\n * false detections from reflected or distant signals.\r\n *\r\n * Default: -85. Typical range: -100 (very permissive) to -70 (strict).\r\n */\r\n minRssi?: number;\r\n /**\r\n * Controls which event types are emitted, logged, and forwarded to the API.\r\n *\r\n * - `'all'` (default): distance + enter + exit + timeout events.\r\n * - `'events'`: enter + exit + timeout only (no distance events).\r\n */\r\n level?: 'all' | 'events';\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * The timeout countdown also starts if no BLE readings are received\r\n * for 60 seconds (e.g. due to Doze mode or background throttling).\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for Eddystone enter/exit region events. */\r\nexport type EddystoneRegionEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic Eddystone distance update events during monitoring. */\r\nexport type EddystoneDistanceEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for Eddystone timeout events (beacon in range for configured duration). */\r\nexport type EddystoneTimeoutEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired once after a paired beacon has been continuously in range for its configured `timeoutSeconds`. */\r\n onBeaconTimeout: (params: BeaconTimeoutEvent) => void;\r\n /** Fired continuously during a live scan as each iBeacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n /** Fired continuously during a live scan as each Eddystone beacon is detected. */\r\n onEddystoneFound: (params: EddystoneScanResult) => void;\r\n onEddystoneEnter: (params: EddystoneRegionEvent) => void;\r\n onEddystoneExit: (params: EddystoneRegionEvent) => void;\r\n onEddystoneDistance: (params: EddystoneDistanceEvent) => void;\r\n /** Fired once after a paired Eddystone has been continuously in range for its configured `timeoutSeconds`. */\r\n onEddystoneTimeout: (params: EddystoneTimeoutEvent) => void;\r\n};\r\n\r\n/** Options for filtering event logs. */\r\nexport type EventLogQueryOptions = {\r\n /** Maximum number of log entries to return (default: 1000, max: 10000). */\r\n limit?: number;\r\n /** Filter by event type (e.g. \"onBeaconEnter\", \"onBeaconExit\"). */\r\n eventType?: string;\r\n /** Only return events with timestamp >= this value (ms since epoch). */\r\n sinceTimestamp?: number;\r\n};\r\n\r\n/** A single logged beacon event entry. */\r\nexport type EventLogEntry = {\r\n id: number;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** The event type that was logged (e.g. \"onBeaconEnter\"). */\r\n eventType: string;\r\n /** Beacon identifier, if available. */\r\n identifier?: string;\r\n /** The full event payload that was sent to JS. */\r\n data: Record<string, unknown>;\r\n};\r\n"]}
@@ -104,7 +104,7 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
104
104
  destroyEventLogs(): void;
105
105
  /**
106
106
  * Configure a remote API endpoint for native event forwarding.
107
- * Once set, enter/exit/timeout events are POSTed directly from native code,
107
+ * Once set, beacon events are POSTed directly from native code,
108
108
  * ensuring delivery even when the JS bridge is not active (app backgrounded).
109
109
  *
110
110
  * @param url The API endpoint URL to POST events to.
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAuKzD,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\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set, enter/exit/timeout events are POSTed directly from native code,\r\n * ensuring delivery even when the JS bridge is not active (app backgrounded).\r\n *\r\n * @param url The API endpoint URL to POST events to.\r\n * @param apiKey Optional API key sent as X-API-Key header.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string): 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;AAuKzD,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\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set, beacon events are POSTed directly from native code,\r\n * ensuring delivery even when the JS bridge is not active (app backgrounded).\r\n *\r\n * @param url The API endpoint URL to POST events to.\r\n * @param apiKey Optional API key sent as X-API-Key header.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
@@ -12,6 +12,7 @@ private let EXIT_DISTANCE_KEY = "expo.beacon.exit_distance"
12
12
  private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
13
13
  private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
14
14
  private let MIN_RSSI_KEY = "expo.beacon.min_rssi"
15
+ private let EVENT_LEVEL_KEY = "expo.beacon.event_level"
15
16
 
16
17
  /// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
17
18
  private let DEFAULT_MIN_RSSI: Int = -85
@@ -34,6 +35,9 @@ private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
34
35
  private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 15.0
35
36
  /// Minimum interval between consecutive distance event emissions per identifier.
36
37
  private let DISTANCE_EVENT_THROTTLE_INTERVAL: TimeInterval = 1.0
38
+ /// Seconds of no valid BLE readings before starting the timeout countdown.
39
+ /// Acts as a safety net when ranging cycles stop entirely (e.g. Doze mode).
40
+ private let DISTANCE_INACTIVITY_SECONDS: TimeInterval = 60.0
37
41
 
38
42
  public class ExpoBeaconModule: Module {
39
43
 
@@ -98,6 +102,9 @@ public class ExpoBeaconModule: Module {
98
102
  /// Minimum RSSI threshold — readings below this are treated as unreliable.
99
103
  private var minRssiThreshold: Int = DEFAULT_MIN_RSSI
100
104
 
105
+ /// Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
106
+ private var eventLevel: String = "all"
107
+
101
108
  /// Distance smoothing (EMA) state per identifier.
102
109
  private var smoothedDistances: [String: Double] = [:]
103
110
  /// EMA weight for new readings. 0.4 balances responsiveness vs noise rejection.
@@ -122,6 +129,9 @@ public class ExpoBeaconModule: Module {
122
129
  // Timeout timers — fire once after beacon stays in range for configured duration
123
130
  private var beaconTimeoutTimers: [String: DispatchWorkItem] = [:]
124
131
  private var eddystoneTimeoutTimers: [String: DispatchWorkItem] = [:]
132
+ // Inactivity timers — start timeout countdown when no BLE readings for 60 s
133
+ private var beaconInactivityTimers: [String: DispatchWorkItem] = [:]
134
+ private var eddystoneInactivityTimers: [String: DispatchWorkItem] = [:]
125
135
 
126
136
  // Custom UserDefaults suite to isolate beacon data from the host app's .standard
127
137
  private lazy var defaults: UserDefaults = {
@@ -331,6 +341,13 @@ public class ExpoBeaconModule: Module {
331
341
  maxDistance = map["maxDistance"] as? Double
332
342
  exitDistance = map["exitDistance"] as? Double
333
343
  minRssi = map["minRssi"] as? Int
344
+ if let lvl = map["level"] as? String, lvl == "events" || lvl == "all" {
345
+ self.eventLevel = lvl
346
+ self.defaults.set(lvl, forKey: EVENT_LEVEL_KEY)
347
+ } else {
348
+ self.eventLevel = "all"
349
+ self.defaults.set("all", forKey: EVENT_LEVEL_KEY)
350
+ }
334
351
  if let notifications = map["notifications"] as? [String: Any],
335
352
  let data = try? JSONSerialization.data(withJSONObject: notifications),
336
353
  let json = String(data: data, encoding: .utf8) {
@@ -392,6 +409,8 @@ public class ExpoBeaconModule: Module {
392
409
  self.defaults.set(false, forKey: IS_MONITORING_KEY)
393
410
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
394
411
  self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
412
+ self.defaults.removeObject(forKey: EVENT_LEVEL_KEY)
413
+ self.eventLevel = "all"
395
414
  self.stopRegionMonitoring()
396
415
  promise.resolve(nil)
397
416
  }
@@ -575,6 +594,9 @@ public class ExpoBeaconModule: Module {
575
594
  let storedRssi = defaults.object(forKey: MIN_RSSI_KEY) as? Int
576
595
  minRssiThreshold = storedRssi ?? DEFAULT_MIN_RSSI
577
596
 
597
+ // Restore persisted event level (survives app restarts)
598
+ eventLevel = defaults.string(forKey: EVENT_LEVEL_KEY) ?? "all"
599
+
578
600
  let beacons = loadPairedBeaconsRaw()
579
601
 
580
602
  // CLLocationManager supports a maximum of 20 monitored regions.
@@ -642,6 +664,9 @@ public class ExpoBeaconModule: Module {
642
664
  for timer in beaconTimeoutTimers.values { timer.cancel() }
643
665
  beaconTimeoutTimers.removeAll()
644
666
 
667
+ for timer in beaconInactivityTimers.values { timer.cancel() }
668
+ beaconInactivityTimers.removeAll()
669
+
645
670
  stopEddystoneMonitoring()
646
671
  }
647
672
 
@@ -810,6 +835,8 @@ public class ExpoBeaconModule: Module {
810
835
 
811
836
  eddystoneLatestSeen[identifier] = Date()
812
837
  eddystoneMissCounters[identifier] = 0
838
+ // Valid BLE reading — reset inactivity timer.
839
+ rescheduleEddystoneInactivity(identifier: identifier, namespace: ns, instance: inst)
813
840
 
814
841
  // Distance-driven enter/exit with hysteresis — evaluated on every
815
842
  // BLE callback (not throttled) so the hysteresis counters advance
@@ -818,13 +845,10 @@ public class ExpoBeaconModule: Module {
818
845
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
819
846
  let hasValidDistance = distance.isFinite && distance >= 0
820
847
  if hasValidDistance || maxDist == nil {
821
- // Apply EMA smoothing; jump guard returns nil for outliers
848
+ // Apply EMA smoothing; jump resets EMA to the new value
822
849
  let effectiveDistance: Double
823
- if hasValidDistance, let smoothed = smoothDistance(identifier: identifier, rawDistance: distance) {
824
- effectiveDistance = smoothed
825
- } else if hasValidDistance {
826
- // Jump outlier — skip this cycle without resetting counters
827
- break
850
+ if hasValidDistance {
851
+ effectiveDistance = smoothDistance(identifier: identifier, rawDistance: distance)!
828
852
  } else {
829
853
  effectiveDistance = distance
830
854
  }
@@ -852,6 +876,7 @@ public class ExpoBeaconModule: Module {
852
876
  cancelEddystoneTimeout(identifier: identifier)
853
877
  case .exit:
854
878
  smoothedDistances.removeValue(forKey: identifier)
879
+ print("[ExpoBeacon] DEBUG: Eddystone distance-based EXIT for \(identifier)")
855
880
  sendLoggedEvent("onEddystoneExit", [
856
881
  "identifier": identifier,
857
882
  "namespace": ns,
@@ -861,7 +886,8 @@ public class ExpoBeaconModule: Module {
861
886
  "rssi": beaconRssi
862
887
  ])
863
888
  postBeaconNotification(identifier: identifier, eventType: "exit")
864
- // Beacon left — start the timeout clock.
889
+ // Beacon left — cancel inactivity timer and start the timeout clock.
890
+ cancelEddystoneInactivity(identifier: identifier)
865
891
  scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
866
892
  case .none:
867
893
  break
@@ -869,6 +895,7 @@ public class ExpoBeaconModule: Module {
869
895
  }
870
896
 
871
897
  guard hasValidDistance else { break }
898
+ guard self.eventLevel == "all" else { break }
872
899
 
873
900
  // Throttle distance events — enter/exit above is evaluated on every
874
901
  // callback, but distance events are rate-limited to avoid flooding JS.
@@ -960,10 +987,8 @@ public class ExpoBeaconModule: Module {
960
987
  let identifier = params["identifier"] as? String
961
988
  getOrCreateEventLogger().logEvent(eventType: eventName, identifier: identifier, data: params)
962
989
  }
963
- // Forward enter/exit/timeout events to remote API (skip distance — too frequent)
964
- if !eventName.lowercased().contains("distance") {
965
- apiForwarder.forwardEvent(params)
966
- }
990
+ // Forward all produced events to remote API
991
+ apiForwarder.forwardEvent(params)
967
992
  sendEvent(eventName, params)
968
993
  }
969
994
 
@@ -1049,6 +1074,9 @@ public class ExpoBeaconModule: Module {
1049
1074
  for timer in eddystoneTimeoutTimers.values { timer.cancel() }
1050
1075
  eddystoneTimeoutTimers.removeAll()
1051
1076
 
1077
+ for timer in eddystoneInactivityTimers.values { timer.cancel() }
1078
+ eddystoneInactivityTimers.removeAll()
1079
+
1052
1080
  stopBleScanIfUnneeded()
1053
1081
  }
1054
1082
 
@@ -1093,8 +1121,10 @@ public class ExpoBeaconModule: Module {
1093
1121
  "distance": -1
1094
1122
  ]
1095
1123
  sendLoggedEvent("onEddystoneExit", params)
1124
+ print("[ExpoBeacon] DEBUG: Eddystone miss-based EXIT for \(identifier)")
1096
1125
  postBeaconNotification(identifier: identifier, eventType: "exit")
1097
- // Beacon disappeared — start the timeout clock.
1126
+ // Beacon disappeared — cancel inactivity timer and start the timeout clock.
1127
+ cancelEddystoneInactivity(identifier: identifier)
1098
1128
  scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
1099
1129
  }
1100
1130
  }
@@ -1147,6 +1177,48 @@ public class ExpoBeaconModule: Module {
1147
1177
  eddystoneTimeoutTimers.removeValue(forKey: identifier)?.cancel()
1148
1178
  }
1149
1179
 
1180
+ // MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
1181
+
1182
+ private func rescheduleBeaconInactivity(identifier: String, beacon: CLBeacon? = nil, region: CLBeaconRegion? = nil) {
1183
+ cancelBeaconInactivity(identifier: identifier)
1184
+
1185
+ let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
1186
+ guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
1187
+
1188
+ let work = DispatchWorkItem { [weak self] in
1189
+ guard let self = self else { return }
1190
+ self.beaconInactivityTimers.removeValue(forKey: identifier)
1191
+ // No BLE readings for 60 s — start the configured timeout countdown.
1192
+ self.scheduleBeaconTimeout(identifier: identifier, beacon: beacon, region: region)
1193
+ }
1194
+ beaconInactivityTimers[identifier] = work
1195
+ DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
1196
+ }
1197
+
1198
+ private func rescheduleEddystoneInactivity(identifier: String, namespace: String, instance: String) {
1199
+ cancelEddystoneInactivity(identifier: identifier)
1200
+
1201
+ let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
1202
+ guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
1203
+
1204
+ let work = DispatchWorkItem { [weak self] in
1205
+ guard let self = self else { return }
1206
+ self.eddystoneInactivityTimers.removeValue(forKey: identifier)
1207
+ // No BLE readings for 60 s — start the configured timeout countdown.
1208
+ self.scheduleEddystoneTimeout(identifier: identifier, namespace: namespace, instance: instance)
1209
+ }
1210
+ eddystoneInactivityTimers[identifier] = work
1211
+ DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
1212
+ }
1213
+
1214
+ private func cancelBeaconInactivity(identifier: String) {
1215
+ beaconInactivityTimers.removeValue(forKey: identifier)?.cancel()
1216
+ }
1217
+
1218
+ private func cancelEddystoneInactivity(identifier: String) {
1219
+ eddystoneInactivityTimers.removeValue(forKey: identifier)?.cancel()
1220
+ }
1221
+
1150
1222
  private func postBeaconNotification(identifier: String, eventType: String) {
1151
1223
  let cfg = loadNotificationConfig()
1152
1224
  let eventsCfg = cfg["beaconEvents"] as? [String: Any]
@@ -1328,9 +1400,13 @@ public class ExpoBeaconModule: Module {
1328
1400
  if let beacon = validBeacon {
1329
1401
  // Got a valid reading — reset miss counter
1330
1402
  missCounters[identifier] = 0
1403
+ // Valid BLE reading — reset inactivity timer.
1404
+ rescheduleBeaconInactivity(identifier: identifier, beacon: beacon)
1331
1405
 
1332
- // Emit distance event every ranging cycle (~1 s)
1333
- sendLoggedEvent("onBeaconDistance", makeBeaconEventParams(identifier: identifier, beacon: beacon))
1406
+ // Emit distance event every ranging cycle (~1 s) if level allows
1407
+ if self.eventLevel == "all" {
1408
+ sendLoggedEvent("onBeaconDistance", makeBeaconEventParams(identifier: identifier, beacon: beacon))
1409
+ }
1334
1410
 
1335
1411
  // Enter/exit synthesis with hysteresis — always applied.
1336
1412
  // When maxDistance is set, distance thresholds control transitions.
@@ -1339,11 +1415,8 @@ public class ExpoBeaconModule: Module {
1339
1415
  let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
1340
1416
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
1341
1417
 
1342
- // Apply EMA smoothing; jump guard returns nil for outliers
1343
- guard let smoothed = smoothDistance(identifier: identifier, rawDistance: beacon.accuracy) else {
1344
- // Jump outlier — skip this cycle without resetting counters
1345
- return
1346
- }
1418
+ // Apply EMA smoothing; jump resets EMA to the new value
1419
+ let smoothed = smoothDistance(identifier: identifier, rawDistance: beacon.accuracy)!
1347
1420
 
1348
1421
  let action = evaluateDistanceHysteresis(
1349
1422
  identifier: identifier,
@@ -1364,7 +1437,8 @@ public class ExpoBeaconModule: Module {
1364
1437
  smoothedDistances.removeValue(forKey: identifier)
1365
1438
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
1366
1439
  postBeaconNotification(identifier: identifier, eventType: "exit")
1367
- // Beacon left — start the timeout clock.
1440
+ // Beacon left — cancel inactivity timer and start the timeout clock.
1441
+ cancelBeaconInactivity(identifier: identifier)
1368
1442
  scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1369
1443
  case .none:
1370
1444
  break
@@ -1392,7 +1466,8 @@ public class ExpoBeaconModule: Module {
1392
1466
  let region = monitoredRegions.first { $0.identifier == identifier }
1393
1467
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: region, event: "exit"))
1394
1468
  postBeaconNotification(identifier: identifier, eventType: "exit")
1395
- // Beacon disappeared — start the timeout clock.
1469
+ // Beacon disappeared — cancel inactivity timer and start the timeout clock.
1470
+ cancelBeaconInactivity(identifier: identifier)
1396
1471
  scheduleBeaconTimeout(identifier: identifier, region: region)
1397
1472
  }
1398
1473
  }
@@ -1438,7 +1513,8 @@ public class ExpoBeaconModule: Module {
1438
1513
  if wasEntered {
1439
1514
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
1440
1515
  postBeaconNotification(identifier: identifier, eventType: "exit")
1441
- // OS-level exit safety net — start the timeout clock.
1516
+ // OS-level exit safety net — cancel inactivity timer and start the timeout clock.
1517
+ cancelBeaconInactivity(identifier: identifier)
1442
1518
  scheduleBeaconTimeout(identifier: identifier, region: beaconRegion)
1443
1519
  }
1444
1520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
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",
@@ -27,6 +27,9 @@ export type PairedBeacon = {
27
27
  * Timeout in seconds. When set, the module fires `onBeaconTimeout` once
28
28
  * after the beacon has been continuously in range for this duration.
29
29
  * The timer resets if the beacon exits and re-enters range.
30
+ *
31
+ * The timeout countdown also starts if no BLE readings are received
32
+ * for 60 seconds (e.g. due to Doze mode or background throttling).
30
33
  */
31
34
  timeoutSeconds?: number;
32
35
  };
@@ -141,6 +144,13 @@ export type MonitoringOptions = {
141
144
  * Default: -85. Typical range: -100 (very permissive) to -70 (strict).
142
145
  */
143
146
  minRssi?: number;
147
+ /**
148
+ * Controls which event types are emitted, logged, and forwarded to the API.
149
+ *
150
+ * - `'all'` (default): distance + enter + exit + timeout events.
151
+ * - `'events'`: enter + exit + timeout only (no distance events).
152
+ */
153
+ level?: 'all' | 'events';
144
154
  /** Notification configuration overrides to apply for this monitoring session. */
145
155
  notifications?: NotificationConfig;
146
156
  };
@@ -182,6 +192,9 @@ export type PairedEddystone = {
182
192
  * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
183
193
  * after the beacon has been continuously in range for this duration.
184
194
  * The timer resets if the beacon exits and re-enters range.
195
+ *
196
+ * The timeout countdown also starts if no BLE readings are received
197
+ * for 60 seconds (e.g. due to Doze mode or background throttling).
185
198
  */
186
199
  timeoutSeconds?: number;
187
200
  };
@@ -156,7 +156,7 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
156
156
 
157
157
  /**
158
158
  * Configure a remote API endpoint for native event forwarding.
159
- * Once set, enter/exit/timeout events are POSTed directly from native code,
159
+ * Once set, beacon events are POSTed directly from native code,
160
160
  * ensuring delivery even when the JS bridge is not active (app backgrounded).
161
161
  *
162
162
  * @param url The API endpoint URL to POST events to.