expo-beacon 0.6.14 → 0.6.16

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,6 +64,8 @@ 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
69
71
  // Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
@@ -177,6 +179,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
177
179
  lastSeenAtMs.clear()
178
180
  timeoutHandler.removeCallbacksAndMessages(null)
179
181
  timeoutRunnables.clear()
182
+ inactivityRunnables.clear()
180
183
  synchronized(distanceLock) {
181
184
  enterCounters.clear()
182
185
  exitCounters.clear()
@@ -264,6 +267,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
264
267
  val closest = beacons.filter { it.distance >= 0 && it.rssi >= minRssiThreshold }.minByOrNull { it.distance }
265
268
  if (closest != null) {
266
269
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
270
+ // Valid BLE reading — reset inactivity timer.
271
+ rescheduleInactivity(region)
267
272
  sendBeaconBroadcast(region, "distance", closest.distance, closest.rssi)
268
273
  }
269
274
  }
@@ -294,7 +299,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
294
299
  if (wasEntered) {
295
300
  sendBeaconBroadcast(region, "exit", -1.0)
296
301
  showEnterExitNotification(region, "exit")
297
- // 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)
298
304
  scheduleTimeoutIfConfigured(region)
299
305
  }
300
306
  }
@@ -320,6 +326,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
320
326
  // Got a valid reading — reset miss counter
321
327
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
322
328
  missCounters[region.uniqueId] = 0
329
+ // Valid BLE reading — reset inactivity timer.
330
+ rescheduleInactivity(region)
323
331
 
324
332
  // Apply EMA smoothing; jump resets EMA to the new value
325
333
  val smoothed = smoothDistance(region.uniqueId, beacon.distance)
@@ -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
  }
@@ -468,6 +478,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
468
478
  val runnable = Runnable {
469
479
  timeoutRunnables.remove(region.uniqueId)
470
480
  sendBeaconBroadcast(region, "timeout", -1.0)
481
+ showEnterExitNotification(region, "timeout")
471
482
  }
472
483
  timeoutRunnables[region.uniqueId] = runnable
473
484
  timeoutHandler.postDelayed(runnable, seconds * 1000L)
@@ -477,6 +488,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
477
488
  timeoutRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
478
489
  }
479
490
 
491
+ // MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
492
+
493
+ private fun rescheduleInactivity(region: Region) {
494
+ val regionId = region.uniqueId
495
+ if (!beaconTimeouts.containsKey(regionId)) return
496
+ cancelInactivity(regionId)
497
+ val runnable = Runnable {
498
+ inactivityRunnables.remove(regionId)
499
+ // No BLE readings for 60 s — start the configured timeout countdown.
500
+ scheduleTimeoutIfConfigured(region)
501
+ }
502
+ inactivityRunnables[regionId] = runnable
503
+ timeoutHandler.postDelayed(runnable, DISTANCE_INACTIVITY_MS)
504
+ }
505
+
506
+ private fun cancelInactivity(regionId: String) {
507
+ inactivityRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
508
+ }
509
+
480
510
  private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double, rssi: Int = 0) {
481
511
  // Determine if this is an Eddystone region based on identifier format
482
512
  // Eddystone regions have id1 as a hex namespace (not a UUID)
@@ -566,11 +596,15 @@ class BeaconForegroundService : Service(), BeaconConsumer {
566
596
  // Respect the enabled flag (defaults to true)
567
597
  if (eventsConfig != null && !eventsConfig.optBoolean("enabled", true)) return
568
598
 
569
- val defaultTitle = if (eventType == "enter") "Beacon Entered" else "Beacon Exited"
570
- val title = if (eventType == "enter") {
571
- eventsConfig?.optString("enterTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
572
- } else {
573
- eventsConfig?.optString("exitTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
599
+ val defaultTitle = when (eventType) {
600
+ "enter" -> "Beacon Entered"
601
+ "timeout" -> "Beacon Timeout"
602
+ else -> "Beacon Exited"
603
+ }
604
+ val title = when (eventType) {
605
+ "enter" -> eventsConfig?.optString("enterTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
606
+ "timeout" -> eventsConfig?.optString("timeoutTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
607
+ else -> eventsConfig?.optString("exitTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
574
608
  }
575
609
 
576
610
  val bodyTemplate = eventsConfig?.optString("body")?.takeIf { it.isNotEmpty() }
@@ -736,6 +770,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
736
770
  serviceConnected = false
737
771
  timeoutHandler.removeCallbacksAndMessages(null)
738
772
  timeoutRunnables.clear()
773
+ inactivityRunnables.clear()
739
774
  beaconTimeouts.clear()
740
775
  lastSeenAtMs.clear()
741
776
  monitoredRegionIds.clear()
@@ -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
  };
@@ -68,6 +71,8 @@ export type BeaconNotificationConfig = {
68
71
  enterTitle?: string;
69
72
  /** Notification title on beacon exit. Default: "Beacon Exited". */
70
73
  exitTitle?: string;
74
+ /** Notification title on beacon timeout. Default: "Beacon Timeout". */
75
+ timeoutTitle?: string;
71
76
  /**
72
77
  * Notification body template. Supports {identifier} and {event} placeholders.
73
78
  * Default: "{identifier} region {event}ed".
@@ -177,6 +182,9 @@ export type PairedEddystone = {
177
182
  * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
178
183
  * after the beacon has been continuously in range for this duration.
179
184
  * The timer resets if the beacon exits and re-enters range.
185
+ *
186
+ * The timeout countdown also starts if no BLE readings are received
187
+ * for 60 seconds (e.g. due to Doze mode or background throttling).
180
188
  */
181
189
  timeoutSeconds?: number;
182
190
  };
@@ -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 /**\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 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 /** Notification title on beacon timeout. Default: \"Beacon Timeout\". */\r\n timeoutTitle?: 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"]}
@@ -35,6 +35,9 @@ private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
35
35
  private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 15.0
36
36
  /// Minimum interval between consecutive distance event emissions per identifier.
37
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
38
41
 
39
42
  public class ExpoBeaconModule: Module {
40
43
 
@@ -126,6 +129,9 @@ public class ExpoBeaconModule: Module {
126
129
  // Timeout timers — fire once after beacon stays in range for configured duration
127
130
  private var beaconTimeoutTimers: [String: DispatchWorkItem] = [:]
128
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] = [:]
129
135
 
130
136
  // Custom UserDefaults suite to isolate beacon data from the host app's .standard
131
137
  private lazy var defaults: UserDefaults = {
@@ -658,6 +664,9 @@ public class ExpoBeaconModule: Module {
658
664
  for timer in beaconTimeoutTimers.values { timer.cancel() }
659
665
  beaconTimeoutTimers.removeAll()
660
666
 
667
+ for timer in beaconInactivityTimers.values { timer.cancel() }
668
+ beaconInactivityTimers.removeAll()
669
+
661
670
  stopEddystoneMonitoring()
662
671
  }
663
672
 
@@ -826,6 +835,8 @@ public class ExpoBeaconModule: Module {
826
835
 
827
836
  eddystoneLatestSeen[identifier] = Date()
828
837
  eddystoneMissCounters[identifier] = 0
838
+ // Valid BLE reading — reset inactivity timer.
839
+ rescheduleEddystoneInactivity(identifier: identifier, namespace: ns, instance: inst)
829
840
 
830
841
  // Distance-driven enter/exit with hysteresis — evaluated on every
831
842
  // BLE callback (not throttled) so the hysteresis counters advance
@@ -875,7 +886,8 @@ public class ExpoBeaconModule: Module {
875
886
  "rssi": beaconRssi
876
887
  ])
877
888
  postBeaconNotification(identifier: identifier, eventType: "exit")
878
- // Beacon left — start the timeout clock.
889
+ // Beacon left — cancel inactivity timer and start the timeout clock.
890
+ cancelEddystoneInactivity(identifier: identifier)
879
891
  scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
880
892
  case .none:
881
893
  break
@@ -1062,6 +1074,9 @@ public class ExpoBeaconModule: Module {
1062
1074
  for timer in eddystoneTimeoutTimers.values { timer.cancel() }
1063
1075
  eddystoneTimeoutTimers.removeAll()
1064
1076
 
1077
+ for timer in eddystoneInactivityTimers.values { timer.cancel() }
1078
+ eddystoneInactivityTimers.removeAll()
1079
+
1065
1080
  stopBleScanIfUnneeded()
1066
1081
  }
1067
1082
 
@@ -1108,7 +1123,8 @@ public class ExpoBeaconModule: Module {
1108
1123
  sendLoggedEvent("onEddystoneExit", params)
1109
1124
  print("[ExpoBeacon] DEBUG: Eddystone miss-based EXIT for \(identifier)")
1110
1125
  postBeaconNotification(identifier: identifier, eventType: "exit")
1111
- // Beacon disappeared — start the timeout clock.
1126
+ // Beacon disappeared — cancel inactivity timer and start the timeout clock.
1127
+ cancelEddystoneInactivity(identifier: identifier)
1112
1128
  scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
1113
1129
  }
1114
1130
  }
@@ -1127,6 +1143,7 @@ public class ExpoBeaconModule: Module {
1127
1143
  guard let self = self else { return }
1128
1144
  self.beaconTimeoutTimers.removeValue(forKey: identifier)
1129
1145
  self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon, region: region))
1146
+ self.postBeaconNotification(identifier: identifier, eventType: "timeout")
1130
1147
  }
1131
1148
  beaconTimeoutTimers[identifier] = work
1132
1149
  DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds), execute: work)
@@ -1152,6 +1169,7 @@ public class ExpoBeaconModule: Module {
1152
1169
  "instance": instance,
1153
1170
  "distance": -1
1154
1171
  ])
1172
+ self.postBeaconNotification(identifier: identifier, eventType: "timeout")
1155
1173
  }
1156
1174
  eddystoneTimeoutTimers[identifier] = work
1157
1175
  DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds), execute: work)
@@ -1161,6 +1179,48 @@ public class ExpoBeaconModule: Module {
1161
1179
  eddystoneTimeoutTimers.removeValue(forKey: identifier)?.cancel()
1162
1180
  }
1163
1181
 
1182
+ // MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
1183
+
1184
+ private func rescheduleBeaconInactivity(identifier: String, beacon: CLBeacon? = nil, region: CLBeaconRegion? = nil) {
1185
+ cancelBeaconInactivity(identifier: identifier)
1186
+
1187
+ let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
1188
+ guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
1189
+
1190
+ let work = DispatchWorkItem { [weak self] in
1191
+ guard let self = self else { return }
1192
+ self.beaconInactivityTimers.removeValue(forKey: identifier)
1193
+ // No BLE readings for 60 s — start the configured timeout countdown.
1194
+ self.scheduleBeaconTimeout(identifier: identifier, beacon: beacon, region: region)
1195
+ }
1196
+ beaconInactivityTimers[identifier] = work
1197
+ DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
1198
+ }
1199
+
1200
+ private func rescheduleEddystoneInactivity(identifier: String, namespace: String, instance: String) {
1201
+ cancelEddystoneInactivity(identifier: identifier)
1202
+
1203
+ let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
1204
+ guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
1205
+
1206
+ let work = DispatchWorkItem { [weak self] in
1207
+ guard let self = self else { return }
1208
+ self.eddystoneInactivityTimers.removeValue(forKey: identifier)
1209
+ // No BLE readings for 60 s — start the configured timeout countdown.
1210
+ self.scheduleEddystoneTimeout(identifier: identifier, namespace: namespace, instance: instance)
1211
+ }
1212
+ eddystoneInactivityTimers[identifier] = work
1213
+ DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
1214
+ }
1215
+
1216
+ private func cancelBeaconInactivity(identifier: String) {
1217
+ beaconInactivityTimers.removeValue(forKey: identifier)?.cancel()
1218
+ }
1219
+
1220
+ private func cancelEddystoneInactivity(identifier: String) {
1221
+ eddystoneInactivityTimers.removeValue(forKey: identifier)?.cancel()
1222
+ }
1223
+
1164
1224
  private func postBeaconNotification(identifier: String, eventType: String) {
1165
1225
  let cfg = loadNotificationConfig()
1166
1226
  let eventsCfg = cfg["beaconEvents"] as? [String: Any]
@@ -1168,11 +1228,19 @@ public class ExpoBeaconModule: Module {
1168
1228
  // Respect the enabled flag (defaults to true)
1169
1229
  if let enabled = eventsCfg?["enabled"] as? Bool, !enabled { return }
1170
1230
 
1171
- let defaultTitle = eventType == "enter" ? "Beacon Entered" : "Beacon Exited"
1231
+ let defaultTitle: String
1232
+ switch eventType {
1233
+ case "enter": defaultTitle = "Beacon Entered"
1234
+ case "timeout": defaultTitle = "Beacon Timeout"
1235
+ default: defaultTitle = "Beacon Exited"
1236
+ }
1172
1237
  let title: String
1173
- if eventType == "enter" {
1238
+ switch eventType {
1239
+ case "enter":
1174
1240
  title = (eventsCfg?["enterTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
1175
- } else {
1241
+ case "timeout":
1242
+ title = (eventsCfg?["timeoutTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
1243
+ default:
1176
1244
  title = (eventsCfg?["exitTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
1177
1245
  }
1178
1246
 
@@ -1342,6 +1410,8 @@ public class ExpoBeaconModule: Module {
1342
1410
  if let beacon = validBeacon {
1343
1411
  // Got a valid reading — reset miss counter
1344
1412
  missCounters[identifier] = 0
1413
+ // Valid BLE reading — reset inactivity timer.
1414
+ rescheduleBeaconInactivity(identifier: identifier, beacon: beacon)
1345
1415
 
1346
1416
  // Emit distance event every ranging cycle (~1 s) if level allows
1347
1417
  if self.eventLevel == "all" {
@@ -1377,7 +1447,8 @@ public class ExpoBeaconModule: Module {
1377
1447
  smoothedDistances.removeValue(forKey: identifier)
1378
1448
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
1379
1449
  postBeaconNotification(identifier: identifier, eventType: "exit")
1380
- // Beacon left — start the timeout clock.
1450
+ // Beacon left — cancel inactivity timer and start the timeout clock.
1451
+ cancelBeaconInactivity(identifier: identifier)
1381
1452
  scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1382
1453
  case .none:
1383
1454
  break
@@ -1405,7 +1476,8 @@ public class ExpoBeaconModule: Module {
1405
1476
  let region = monitoredRegions.first { $0.identifier == identifier }
1406
1477
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: region, event: "exit"))
1407
1478
  postBeaconNotification(identifier: identifier, eventType: "exit")
1408
- // Beacon disappeared — start the timeout clock.
1479
+ // Beacon disappeared — cancel inactivity timer and start the timeout clock.
1480
+ cancelBeaconInactivity(identifier: identifier)
1409
1481
  scheduleBeaconTimeout(identifier: identifier, region: region)
1410
1482
  }
1411
1483
  }
@@ -1451,7 +1523,8 @@ public class ExpoBeaconModule: Module {
1451
1523
  if wasEntered {
1452
1524
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
1453
1525
  postBeaconNotification(identifier: identifier, eventType: "exit")
1454
- // OS-level exit safety net — start the timeout clock.
1526
+ // OS-level exit safety net — cancel inactivity timer and start the timeout clock.
1527
+ cancelBeaconInactivity(identifier: identifier)
1455
1528
  scheduleBeaconTimeout(identifier: identifier, region: beaconRegion)
1456
1529
  }
1457
1530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
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
  };
@@ -73,6 +76,8 @@ export type BeaconNotificationConfig = {
73
76
  enterTitle?: string;
74
77
  /** Notification title on beacon exit. Default: "Beacon Exited". */
75
78
  exitTitle?: string;
79
+ /** Notification title on beacon timeout. Default: "Beacon Timeout". */
80
+ timeoutTitle?: string;
76
81
  /**
77
82
  * Notification body template. Supports {identifier} and {event} placeholders.
78
83
  * Default: "{identifier} region {event}ed".
@@ -189,6 +194,9 @@ export type PairedEddystone = {
189
194
  * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
190
195
  * after the beacon has been continuously in range for this duration.
191
196
  * The timer resets if the beacon exits and re-enters range.
197
+ *
198
+ * The timeout countdown also starts if no BLE readings are received
199
+ * for 60 seconds (e.g. due to Doze mode or background throttling).
192
200
  */
193
201
  timeoutSeconds?: number;
194
202
  };