expo-beacon 0.7.24 → 0.7.25

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.
@@ -1,10 +1,10 @@
1
1
  package expo.modules.beacon
2
2
 
3
3
  // Shared constants used across ExpoBeaconModule and BeaconForegroundService.
4
- // NOTE: EXIT_MISS_THRESHOLD and scan timing deviate from iOS intentionally —
5
- // Android requires a non-zero between-scan gap to prevent OS-level BLE
6
- // throttling, and a higher miss threshold to tolerate brief scan pauses.
7
- // HYSTERESIS_COUNT should stay in sync with ExpoBeaconModule.swift (iOS).
4
+ // NOTE: Scan timing deviates from iOS intentionally — Android requires a non-zero
5
+ // between-scan gap to prevent OS-level BLE throttling.
6
+ // ENTER_HYSTERESIS_COUNT and EXIT_HYSTERESIS_COUNT should stay in sync with
7
+ // ExpoBeaconModule.swift (iOS).
8
8
 
9
9
  internal const val PREFS_NAME = "expo.beacon.paired"
10
10
  internal const val PREFS_KEY = "paired_beacons"
@@ -42,22 +42,19 @@ internal const val REGION_EXIT_PERIOD_MS = 60000L
42
42
  */
43
43
  internal const val RECENT_RANGING_SIGHTING_GRACE_MS = REGION_EXIT_PERIOD_MS
44
44
 
45
- /**
46
- * Number of consecutive ranging misses before emitting a distance-based exit event.
47
- * With a ~2.1 s scan cycle (1100 ms scan + 1000 ms gap), 10 misses ≈ 21 s of
48
- * silence before declaring exit — tolerant of brief BLE gaps while still
49
- * responsive to actual departures.
50
- */
51
- internal const val EXIT_MISS_THRESHOLD = 10
52
-
53
45
  /**
54
46
  * Milliseconds of no valid BLE readings before starting the timeout countdown.
55
47
  * Acts as a safety net when ranging cycles stop entirely (e.g. Doze mode).
56
48
  */
57
49
  internal const val DISTANCE_INACTIVITY_MS = 60_000L
58
50
 
59
- /** Number of consecutive readings required to confirm a distance-based enter or exit transition. */
60
- internal const val HYSTERESIS_COUNT = 3
51
+ /** Number of consecutive in-range readings required to confirm a distance-based enter transition. */
52
+ internal const val ENTER_HYSTERESIS_COUNT = 1
53
+ /** Number of consecutive out-of-range readings required to confirm a distance-based exit transition. */
54
+ internal const val EXIT_HYSTERESIS_COUNT = 3
55
+
56
+ /** Default seconds of silence after last sighting before a disappearance-based exit fires. */
57
+ internal const val DEFAULT_EXIT_TIMEOUT_SECONDS = 300.0
61
58
 
62
59
  /** Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable. */
63
60
  internal const val DEFAULT_MIN_RSSI = -85
@@ -81,6 +81,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
81
81
  private var apiForwarder: BeaconApiForwarder? = null
82
82
  // Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
83
83
  @Volatile private var eventLevel: String = "all"
84
+ // Seconds of silence after last valid sighting before a disappearance-based exit fires.
85
+ @Volatile private var exitTimeoutMs: Long = (DEFAULT_EXIT_TIMEOUT_SECONDS * 1000.0).toLong()
84
86
 
85
87
  override fun onCreate() {
86
88
  super.onCreate()
@@ -187,6 +189,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
187
189
  exitDistance = optPrefs.getString("exit_distance", null)?.toDoubleOrNull()
188
190
  minRssiThreshold = optPrefs.getInt("min_rssi", DEFAULT_MIN_RSSI)
189
191
  eventLevel = optPrefs.getString("level", "all") ?: "all"
192
+ exitTimeoutMs = ((optPrefs.getString("exit_timeout_seconds", null)?.toDoubleOrNull() ?: DEFAULT_EXIT_TIMEOUT_SECONDS) * 1000.0).toLong()
190
193
 
191
194
  beaconManager.addMonitorNotifier(monitorNotifier)
192
195
  beaconManager.addRangeNotifier(rangeNotifier)
@@ -442,14 +445,16 @@ class BeaconForegroundService : Service(), BeaconConsumer {
442
445
  // On Android 17+ (API 37) the BLE scan callbacks are more intermittent: valid
443
446
  // readings are interspersed with occasional null cycles even when the beacon is
444
447
  // nearby. Resetting direction counters on every null would prevent the hysteresis
445
- // from ever accumulating to HYSTERESIS_COUNT, breaking enter/exit entirely while
448
+ // from ever accumulating to ENTER_HYSTERESIS_COUNT, breaking enter/exit entirely while
446
449
  // still allowing distance events (which fire on each individual valid reading).
447
450
  // Direction counters are reset by evaluateDistanceHysteresis when a valid reading
448
451
  // contradicts the current direction (e.g., in-range reading resets exitCounters).
449
452
  val count = (missCounters[region.uniqueId] ?: 0) + 1
450
453
  missCounters[region.uniqueId] = count
451
454
 
452
- if (enteredRegions.contains(region.uniqueId) && count >= EXIT_MISS_THRESHOLD) {
455
+ val lastSeen = lastSeenAtMs[region.uniqueId]
456
+ val silentMs = if (lastSeen != null) SystemClock.elapsedRealtime() - lastSeen else Long.MAX_VALUE
457
+ if (enteredRegions.contains(region.uniqueId) && silentMs >= exitTimeoutMs) {
453
458
  enteredRegions.remove(region.uniqueId)
454
459
  missCounters[region.uniqueId] = 0
455
460
  enterCounters[region.uniqueId] = 0
@@ -520,7 +525,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
520
525
  }
521
526
  val count = (enterCounters[regionId] ?: 0) + 1
522
527
  enterCounters[regionId] = count
523
- if (count >= HYSTERESIS_COUNT) {
528
+ if (count >= ENTER_HYSTERESIS_COUNT) {
524
529
  enterCounters[regionId] = 0
525
530
  return HysteresisAction.ENTER
526
531
  }
@@ -533,7 +538,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
533
538
  exitCounters[regionId] = 0
534
539
  val count = (enterCounters[regionId] ?: 0) + 1
535
540
  enterCounters[regionId] = count
536
- if (!enteredRegions.contains(regionId) && count >= HYSTERESIS_COUNT) {
541
+ if (!enteredRegions.contains(regionId) && count >= ENTER_HYSTERESIS_COUNT) {
537
542
  enterCounters[regionId] = 0
538
543
  return HysteresisAction.ENTER
539
544
  }
@@ -542,7 +547,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
542
547
  enterCounters[regionId] = 0
543
548
  val count = (exitCounters[regionId] ?: 0) + 1
544
549
  exitCounters[regionId] = count
545
- if (enteredRegions.contains(regionId) && count >= HYSTERESIS_COUNT) {
550
+ if (enteredRegions.contains(regionId) && count >= EXIT_HYSTERESIS_COUNT) {
546
551
  exitCounters[regionId] = 0
547
552
  return HysteresisAction.EXIT
548
553
  }
@@ -290,6 +290,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
290
290
  var exitDistance: Double? = null
291
291
  var minRssi: Int? = null
292
292
  var level: String = "all"
293
+ var exitTimeoutSeconds: Double? = null
293
294
  when (options) {
294
295
  is Double -> maxDistance = options
295
296
  is Map<*, *> -> {
@@ -299,6 +300,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
299
300
  exitDistance = (map["exitDistance"] as? Number)?.toDouble()
300
301
  minRssi = (map["minRssi"] as? Number)?.toInt()
301
302
  level = (map["level"] as? String) ?: "all"
303
+ exitTimeoutSeconds = (map["exitTimeoutSeconds"] as? Number)?.toDouble()
302
304
  val notifications = map["notifications"]
303
305
  if (notifications is Map<*, *>) {
304
306
  @Suppress("UNCHECKED_CAST")
@@ -327,6 +329,11 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
327
329
  sendEvent("onBeaconError", mapOf("identifier" to "", "code" to "INVALID_EXIT_DISTANCE", "message" to "exitDistance must be greater than or equal to maxDistance"))
328
330
  return@AsyncFunction
329
331
  }
332
+ if (exitTimeoutSeconds != null && (!exitTimeoutSeconds.isFinite() || exitTimeoutSeconds <= 0.0)) {
333
+ promise.reject("INVALID_EXIT_TIMEOUT", "exitTimeoutSeconds must be a finite number greater than 0", null)
334
+ sendEvent("onBeaconError", mapOf("identifier" to "", "code" to "INVALID_EXIT_TIMEOUT", "message" to "exitTimeoutSeconds must be a finite number greater than 0"))
335
+ return@AsyncFunction
336
+ }
330
337
  ctx.getSharedPreferences(MONITORING_OPTIONS_PREFS, Context.MODE_PRIVATE)
331
338
  .edit().apply {
332
339
  if (maxDistance != null) putString("max_distance", maxDistance.toString())
@@ -336,6 +343,8 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
336
343
  if (minRssi != null) putInt("min_rssi", minRssi)
337
344
  else remove("min_rssi")
338
345
  putString("level", level)
346
+ if (exitTimeoutSeconds != null) putString("exit_timeout_seconds", exitTimeoutSeconds.toString())
347
+ else remove("exit_timeout_seconds")
339
348
  }.apply()
340
349
  // Verify we have the permissions needed for background monitoring
341
350
  val hasLocation = ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
@@ -495,6 +504,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
495
504
  optPrefs.getString("exit_distance", null)?.toDoubleOrNull()?.let { put("exitDistance", it) }
496
505
  if (optPrefs.contains("min_rssi")) put("minRssi", optPrefs.getInt("min_rssi", -85))
497
506
  optPrefs.getString("level", null)?.let { put("level", it) }
507
+ optPrefs.getString("exit_timeout_seconds", null)?.toDoubleOrNull()?.let { put("exitTimeoutSeconds", it) }
498
508
  val json = ctx.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
499
509
  .getString("config", null)
500
510
  if (json != null) {
@@ -121,6 +121,8 @@ export type MonitoringConfig = {
121
121
  exitDistance?: number;
122
122
  minRssi?: number;
123
123
  level?: 'all' | 'events';
124
+ /** Seconds after last beacon sighting before an exit event fires. Default: 300. */
125
+ exitTimeoutSeconds?: number;
124
126
  notifications?: NotificationConfig;
125
127
  };
126
128
  /** Current state snapshot for a paired monitored device. */
@@ -173,6 +175,13 @@ export type MonitoringOptions = {
173
175
  * - `'events'`: enter + exit + timeout only (no distance events).
174
176
  */
175
177
  level?: 'all' | 'events';
178
+ /**
179
+ * Seconds after last beacon sighting before an exit event fires when the beacon
180
+ * disappears without moving outside the exit distance threshold.
181
+ *
182
+ * Default: 300 (5 minutes). Minimum: 1.
183
+ */
184
+ exitTimeoutSeconds?: number;
176
185
  /** Notification configuration overrides to apply for this monitoring session. */
177
186
  notifications?: NotificationConfig;
178
187
  };
@@ -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 * 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/** Snapshot of the current monitoring configuration and active state. */\r\nexport type MonitoringConfig = {\r\n /** Whether background monitoring is currently active. */\r\n isMonitoring: boolean;\r\n maxDistance?: number;\r\n exitDistance?: number;\r\n minRssi?: number;\r\n level?: 'all' | 'events';\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Current state snapshot for a paired monitored device. */\r\nexport type MonitoredDeviceState =\r\n | {\r\n kind: \"ibeacon\";\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\r\n }\r\n | {\r\n kind: \"eddystone\";\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\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/** Payload for native beacon error events (monitoring/ranging failures). */\r\nexport type BeaconErrorEvent = {\r\n /** Region or constraint identifier, empty string if unavailable. */\r\n identifier: string;\r\n /** Machine-readable error code (e.g. \"MONITORING_FAILED\", \"RANGING_FAILED\", \"SECURITY_EXCEPTION\"). */\r\n code: string;\r\n /** Human-readable error message from the native layer. */\r\n message: string;\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 /** Fired when a native monitoring or ranging failure occurs (logged to DB and forwarded to JS). */\r\n onBeaconError: (params: BeaconErrorEvent) => 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/** Snapshot of the current monitoring configuration and active state. */\r\nexport type MonitoringConfig = {\r\n /** Whether background monitoring is currently active. */\r\n isMonitoring: boolean;\r\n maxDistance?: number;\r\n exitDistance?: number;\r\n minRssi?: number;\r\n level?: 'all' | 'events';\r\n /** Seconds after last beacon sighting before an exit event fires. Default: 300. */\r\n exitTimeoutSeconds?: number;\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Current state snapshot for a paired monitored device. */\r\nexport type MonitoredDeviceState =\r\n | {\r\n kind: \"ibeacon\";\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\r\n }\r\n | {\r\n kind: \"eddystone\";\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\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 /**\r\n * Seconds after last beacon sighting before an exit event fires when the beacon\r\n * disappears without moving outside the exit distance threshold.\r\n *\r\n * Default: 300 (5 minutes). Minimum: 1.\r\n */\r\n exitTimeoutSeconds?: 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 * 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/** Payload for native beacon error events (monitoring/ranging failures). */\r\nexport type BeaconErrorEvent = {\r\n /** Region or constraint identifier, empty string if unavailable. */\r\n identifier: string;\r\n /** Machine-readable error code (e.g. \"MONITORING_FAILED\", \"RANGING_FAILED\", \"SECURITY_EXCEPTION\"). */\r\n code: string;\r\n /** Human-readable error message from the native layer. */\r\n message: string;\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 /** Fired when a native monitoring or ranging failure occurs (logged to DB and forwarded to JS). */\r\n onBeaconError: (params: BeaconErrorEvent) => 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"]}
@@ -13,19 +13,19 @@ 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
15
  private let EVENT_LEVEL_KEY = "expo.beacon.event_level"
16
+ private let EXIT_TIMEOUT_SECONDS_KEY = "expo.beacon.exit_timeout_seconds"
16
17
 
17
18
  /// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
18
19
  private let DEFAULT_MIN_RSSI: Int = -85
20
+ /// Default seconds of silence after last beacon sighting before a disappearance-based exit fires.
21
+ private let DEFAULT_EXIT_TIMEOUT_SECONDS: TimeInterval = 300.0
19
22
 
20
- /// Number of consecutive ranging misses before emitting a distance-based exit event.
21
- /// With ~1 s CoreLocation ranging cycles (iBeacon) or ~2 s Eddystone monitoring ticks,
22
- /// 20 misses ≈ 20–40 s of silence before declaring exit — tolerant of iOS background
23
- /// BLE throttling while still responsive to actual departures.
24
- /// NOTE: Android uses 10 with a ~2.1 s scan cycle (≈21 s effective).
25
- private let EXIT_MISS_THRESHOLD = 20
26
- /// Number of consecutive readings required to confirm a distance-based enter or exit transition.
23
+ /// Number of consecutive in-range readings required before an enter event is emitted.
27
24
  /// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
28
- private let HYSTERESIS_COUNT = 3
25
+ private let ENTER_HYSTERESIS_COUNT = 1
26
+ /// Number of consecutive out-of-range readings required before an exit event is emitted.
27
+ /// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
28
+ private let EXIT_HYSTERESIS_COUNT = 3
29
29
 
30
30
  /// Eddystone monitoring timer interval in seconds.
31
31
  private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
@@ -67,8 +67,10 @@ public class ExpoBeaconModule: Module {
67
67
  private var distanceRangingConstraints: [String: CLBeaconIdentityConstraint] = [:]
68
68
  // Identifiers currently in "entered" state (used for distance-driven enter/exit)
69
69
  private var enteredRegions: Set<String> = []
70
- // Consecutive miss counter per identifier (for distance-based exit when beacon disappears)
70
+ // Consecutive miss counter per identifier (for tracking silence; exit now time-based)
71
71
  private var missCounters: [String: Int] = [:]
72
+ // Per-identifier timestamp of the last valid ranging reading (for time-based exit detection)
73
+ private var lastSeenTimes: [String: Date] = [:]
72
74
  // Hysteresis counters: consecutive readings inside/outside threshold per identifier
73
75
  private var enterCounters: [String: Int] = [:]
74
76
  private var exitCounters: [String: Int] = [:]
@@ -105,6 +107,9 @@ public class ExpoBeaconModule: Module {
105
107
  /// Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
106
108
  private var eventLevel: String = "all"
107
109
 
110
+ /// Seconds of silence after last valid beacon sighting before a miss-based exit fires.
111
+ private var exitTimeoutSeconds: TimeInterval = DEFAULT_EXIT_TIMEOUT_SECONDS
112
+
108
113
  /// Distance smoothing (EMA) state per identifier.
109
114
  private var smoothedDistances: [String: Double] = [:]
110
115
  /// EMA weight for new readings. 0.4 balances responsiveness vs noise rejection.
@@ -342,12 +347,14 @@ public class ExpoBeaconModule: Module {
342
347
  var maxDistance: Double? = nil
343
348
  var exitDistance: Double? = nil
344
349
  var minRssi: Int? = nil
350
+ var exitTimeoutSecs: Double? = nil
345
351
  if let dist: Double = options?.get() {
346
352
  maxDistance = dist
347
353
  } else if let map: [String: Any] = options?.get() {
348
354
  maxDistance = map["maxDistance"] as? Double
349
355
  exitDistance = map["exitDistance"] as? Double
350
356
  minRssi = map["minRssi"] as? Int
357
+ exitTimeoutSecs = map["exitTimeoutSeconds"] as? Double
351
358
  if let lvl = map["level"] as? String, lvl == "events" || lvl == "all" {
352
359
  self.eventLevel = lvl
353
360
  self.defaults.set(lvl, forKey: EVENT_LEVEL_KEY)
@@ -381,6 +388,11 @@ public class ExpoBeaconModule: Module {
381
388
  self.sendLoggedEvent("onBeaconError", ["identifier": "", "code": "INVALID_EXIT_DISTANCE", "message": "exitDistance must be greater than or equal to maxDistance"])
382
389
  return
383
390
  }
391
+ if let t = exitTimeoutSecs, (!t.isFinite || t <= 0) {
392
+ promise.reject("INVALID_EXIT_TIMEOUT", "exitTimeoutSeconds must be a finite number greater than 0")
393
+ self.sendLoggedEvent("onBeaconError", ["identifier": "", "code": "INVALID_EXIT_TIMEOUT", "message": "exitTimeoutSeconds must be a finite number greater than 0"])
394
+ return
395
+ }
384
396
  if let dist = maxDistance {
385
397
  self.defaults.set(dist, forKey: MAX_DISTANCE_KEY)
386
398
  } else {
@@ -398,6 +410,13 @@ public class ExpoBeaconModule: Module {
398
410
  self.defaults.removeObject(forKey: MIN_RSSI_KEY)
399
411
  self.minRssiThreshold = DEFAULT_MIN_RSSI
400
412
  }
413
+ if let t = exitTimeoutSecs {
414
+ self.defaults.set(t, forKey: EXIT_TIMEOUT_SECONDS_KEY)
415
+ self.exitTimeoutSeconds = t
416
+ } else {
417
+ self.defaults.removeObject(forKey: EXIT_TIMEOUT_SECONDS_KEY)
418
+ self.exitTimeoutSeconds = DEFAULT_EXIT_TIMEOUT_SECONDS
419
+ }
401
420
  self.defaults.set(true, forKey: IS_MONITORING_KEY)
402
421
  self.requestLocationPermission { granted in
403
422
  guard granted else {
@@ -422,7 +441,10 @@ public class ExpoBeaconModule: Module {
422
441
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
423
442
  self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
424
443
  self.defaults.removeObject(forKey: EVENT_LEVEL_KEY)
444
+ self.defaults.removeObject(forKey: EXIT_TIMEOUT_SECONDS_KEY)
425
445
  self.eventLevel = "all"
446
+ self.exitTimeoutSeconds = DEFAULT_EXIT_TIMEOUT_SECONDS
447
+ self.lastSeenTimes.removeAll()
426
448
  self.stopRegionMonitoring()
427
449
  promise.resolve(nil)
428
450
  }
@@ -541,6 +563,9 @@ public class ExpoBeaconModule: Module {
541
563
  if let level = self.defaults.string(forKey: EVENT_LEVEL_KEY) {
542
564
  result["level"] = level
543
565
  }
566
+ if let t = self.defaults.object(forKey: EXIT_TIMEOUT_SECONDS_KEY) as? Double {
567
+ result["exitTimeoutSeconds"] = t
568
+ }
544
569
  if let json = self.defaults.string(forKey: NOTIFICATION_CONFIG_KEY),
545
570
  let data = json.data(using: .utf8),
546
571
  let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
@@ -647,6 +672,13 @@ public class ExpoBeaconModule: Module {
647
672
  // Restore persisted event level (survives app restarts)
648
673
  eventLevel = defaults.string(forKey: EVENT_LEVEL_KEY) ?? "all"
649
674
 
675
+ // Restore persisted exit timeout (survives app restarts)
676
+ if let t = defaults.object(forKey: EXIT_TIMEOUT_SECONDS_KEY) as? Double, t > 0 {
677
+ exitTimeoutSeconds = t
678
+ } else {
679
+ exitTimeoutSeconds = DEFAULT_EXIT_TIMEOUT_SECONDS
680
+ }
681
+
650
682
  let beacons = loadPairedBeaconsRaw()
651
683
 
652
684
  // CLLocationManager supports a maximum of 20 monitored regions.
@@ -708,6 +740,7 @@ public class ExpoBeaconModule: Module {
708
740
  distanceRangingConstraints.removeAll()
709
741
  enteredRegions.removeAll()
710
742
  missCounters.removeAll()
743
+ lastSeenTimes.removeAll()
711
744
  enterCounters.removeAll()
712
745
  exitCounters.removeAll()
713
746
  smoothedDistances.removeAll()
@@ -1261,14 +1294,23 @@ public class ExpoBeaconModule: Module {
1261
1294
 
1262
1295
  // Not seen recently — reset exit counter (miss counter handles exit
1263
1296
  // separately) but preserve enter counter so that background BLE
1264
- // throttling gaps don't force re-accumulating HYSTERESIS_COUNT reads.
1297
+ // throttling gaps don't force re-accumulating ENTER_HYSTERESIS_COUNT reads.
1265
1298
  eddystoneExitCounters[identifier] = 0
1266
1299
  guard eddystoneEnteredRegions.contains(identifier) else { continue }
1267
1300
 
1268
1301
  let count = (eddystoneMissCounters[identifier] ?? 0) + 1
1269
1302
  eddystoneMissCounters[identifier] = count
1270
1303
 
1271
- if count >= EXIT_MISS_THRESHOLD {
1304
+ // Fire exit only after silence exceeds the configured exitTimeoutSeconds.
1305
+ // eddystoneLatestSeen already tracks Date of last sighting for this identifier.
1306
+ let silentLongEnough: Bool
1307
+ if let lastSeen = eddystoneLatestSeen[identifier] {
1308
+ silentLongEnough = now.timeIntervalSince(lastSeen) >= exitTimeoutSeconds
1309
+ } else {
1310
+ silentLongEnough = false
1311
+ }
1312
+
1313
+ if silentLongEnough {
1272
1314
  eddystoneEnteredRegions.remove(identifier)
1273
1315
  eddystoneMissCounters[identifier] = 0
1274
1316
  eddystoneEnterCounters[identifier] = 0
@@ -1492,7 +1534,7 @@ public class ExpoBeaconModule: Module {
1492
1534
  if distance <= maxDist {
1493
1535
  exitCtrs[identifier] = 0
1494
1536
  enterCtrs[identifier] = (enterCtrs[identifier] ?? 0) + 1
1495
- if !entered.contains(identifier) && (enterCtrs[identifier] ?? 0) >= HYSTERESIS_COUNT {
1537
+ if !entered.contains(identifier) && (enterCtrs[identifier] ?? 0) >= ENTER_HYSTERESIS_COUNT {
1496
1538
  entered.insert(identifier)
1497
1539
  enterCtrs[identifier] = 0
1498
1540
  return .enter
@@ -1500,7 +1542,7 @@ public class ExpoBeaconModule: Module {
1500
1542
  } else if distance > exitDist {
1501
1543
  enterCtrs[identifier] = 0
1502
1544
  exitCtrs[identifier] = (exitCtrs[identifier] ?? 0) + 1
1503
- if entered.contains(identifier) && (exitCtrs[identifier] ?? 0) >= HYSTERESIS_COUNT {
1545
+ if entered.contains(identifier) && (exitCtrs[identifier] ?? 0) >= EXIT_HYSTERESIS_COUNT {
1504
1546
  entered.remove(identifier)
1505
1547
  exitCtrs[identifier] = 0
1506
1548
  return .exit
@@ -1512,7 +1554,7 @@ public class ExpoBeaconModule: Module {
1512
1554
  }
1513
1555
  } else {
1514
1556
  enterCtrs[identifier] = (enterCtrs[identifier] ?? 0) + 1
1515
- if !entered.contains(identifier) && (enterCtrs[identifier] ?? 0) >= HYSTERESIS_COUNT {
1557
+ if !entered.contains(identifier) && (enterCtrs[identifier] ?? 0) >= ENTER_HYSTERESIS_COUNT {
1516
1558
  entered.insert(identifier)
1517
1559
  enterCtrs[identifier] = 0
1518
1560
  return .enter
@@ -1574,8 +1616,9 @@ public class ExpoBeaconModule: Module {
1574
1616
  let validBeacon = beacons.first(where: { $0.accuracy >= 0 && $0.rssi >= minRssiThreshold })
1575
1617
 
1576
1618
  if let beacon = validBeacon {
1577
- // Got a valid reading — reset miss counter
1619
+ // Got a valid reading — reset miss counter and record sighting time
1578
1620
  missCounters[identifier] = 0
1621
+ lastSeenTimes[identifier] = Date()
1579
1622
  // Valid BLE reading — reset inactivity timer.
1580
1623
  rescheduleBeaconInactivity(identifier: identifier, beacon: beacon)
1581
1624
 
@@ -1587,7 +1630,7 @@ public class ExpoBeaconModule: Module {
1587
1630
  // Enter/exit synthesis with hysteresis — always applied.
1588
1631
  // When maxDistance is set, distance thresholds control transitions.
1589
1632
  // When maxDistance is nil, pure presence-based hysteresis is used
1590
- // (HYSTERESIS_COUNT consecutive readings to confirm enter).
1633
+ // (ENTER_HYSTERESIS_COUNT consecutive readings to confirm enter).
1591
1634
  let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
1592
1635
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
1593
1636
 
@@ -1626,16 +1669,19 @@ public class ExpoBeaconModule: Module {
1626
1669
  } else {
1627
1670
  // No valid beacon reading — beacon may have disappeared.
1628
1671
  // Preserve enter counter so background accuracy=-1 gaps don't
1629
- // force re-accumulating HYSTERESIS_COUNT reads from scratch.
1672
+ // force re-accumulating ENTER_HYSTERESIS_COUNT reads from scratch.
1630
1673
  exitCounters[identifier] = 0
1631
1674
  let count = (missCounters[identifier] ?? 0) + 1
1632
1675
  missCounters[identifier] = count
1633
1676
 
1634
- if enteredRegions.contains(identifier) && count >= EXIT_MISS_THRESHOLD {
1677
+ if enteredRegions.contains(identifier),
1678
+ let lastSeen = lastSeenTimes[identifier],
1679
+ Date().timeIntervalSince(lastSeen) >= exitTimeoutSeconds {
1635
1680
  enteredRegions.remove(identifier)
1636
1681
  missCounters[identifier] = 0
1637
1682
  enterCounters[identifier] = 0
1638
1683
  exitCounters[identifier] = 0
1684
+ lastSeenTimes.removeValue(forKey: identifier)
1639
1685
  smoothedDistances.removeValue(forKey: identifier)
1640
1686
 
1641
1687
  // Look up region info for the exit event payload
@@ -1670,7 +1716,7 @@ public class ExpoBeaconModule: Module {
1670
1716
  fileprivate func handleDidEnterRegion(_ region: CLRegion) {
1671
1717
  // Region callbacks are suppressed — all enter/exit logic goes through
1672
1718
  // ranging-based hysteresis in handleDidRange for consistent behaviour
1673
- // with HYSTERESIS_COUNT, regardless of whether maxDistance is set.
1719
+ // with ENTER_HYSTERESIS_COUNT / EXIT_HYSTERESIS_COUNT, regardless of whether maxDistance is set.
1674
1720
  }
1675
1721
 
1676
1722
  fileprivate func handleDidExitRegion(_ region: CLRegion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.7.24",
3
+ "version": "0.7.25",
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",
@@ -130,6 +130,8 @@ export type MonitoringConfig = {
130
130
  exitDistance?: number;
131
131
  minRssi?: number;
132
132
  level?: 'all' | 'events';
133
+ /** Seconds after last beacon sighting before an exit event fires. Default: 300. */
134
+ exitTimeoutSeconds?: number;
133
135
  notifications?: NotificationConfig;
134
136
  };
135
137
 
@@ -186,6 +188,13 @@ export type MonitoringOptions = {
186
188
  * - `'events'`: enter + exit + timeout only (no distance events).
187
189
  */
188
190
  level?: 'all' | 'events';
191
+ /**
192
+ * Seconds after last beacon sighting before an exit event fires when the beacon
193
+ * disappears without moving outside the exit distance threshold.
194
+ *
195
+ * Default: 300 (5 minutes). Minimum: 1.
196
+ */
197
+ exitTimeoutSeconds?: number;
189
198
  /** Notification configuration overrides to apply for this monitoring session. */
190
199
  notifications?: NotificationConfig;
191
200
  };