expo-beacon 0.6.12 → 0.6.14

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.
@@ -66,6 +66,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
66
66
  private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
67
67
  private var eventLogger: BeaconEventLogger? = null
68
68
  private var apiForwarder: BeaconApiForwarder? = null
69
+ // Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
70
+ @Volatile private var eventLevel: String = "all"
69
71
 
70
72
  override fun onCreate() {
71
73
  super.onCreate()
@@ -125,6 +127,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
125
127
  maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
126
128
  exitDistance = optPrefs.getString("exit_distance", null)?.toDoubleOrNull()
127
129
  minRssiThreshold = optPrefs.getInt("min_rssi", DEFAULT_MIN_RSSI)
130
+ eventLevel = optPrefs.getString("level", "all") ?: "all"
128
131
 
129
132
  beaconManager.addMonitorNotifier(monitorNotifier)
130
133
  beaconManager.addRangeNotifier(rangeNotifier)
@@ -256,6 +259,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
256
259
 
257
260
  // Distance logging only — emits distance broadcasts. Enter/exit logic lives in rangeNotifier.
258
261
  private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
262
+ if (eventLevel != "all") return@RangeNotifier
259
263
  if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
260
264
  val closest = beacons.filter { it.distance >= 0 && it.rssi >= minRssiThreshold }.minByOrNull { it.distance }
261
265
  if (closest != null) {
@@ -288,9 +292,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
288
292
  missCounters.remove(region.uniqueId)
289
293
  }
290
294
  if (wasEntered) {
291
- cancelTimeout(region.uniqueId)
292
295
  sendBeaconBroadcast(region, "exit", -1.0)
293
296
  showEnterExitNotification(region, "exit")
297
+ // OS-level exit safety net — start the timeout clock.
298
+ scheduleTimeoutIfConfigured(region)
294
299
  }
295
300
  }
296
301
 
@@ -316,12 +321,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
316
321
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
317
322
  missCounters[region.uniqueId] = 0
318
323
 
319
- // Apply EMA smoothing; jump guard returns null for outliers
324
+ // Apply EMA smoothing; jump resets EMA to the new value
320
325
  val smoothed = smoothDistance(region.uniqueId, beacon.distance)
321
- if (smoothed == null) {
322
- // Outlier — treat as miss without resetting enter counter
323
- return@RangeNotifier
324
- }
325
326
 
326
327
  val action = evaluateDistanceHysteresis(region.uniqueId, smoothed, maxDist)
327
328
  when (action) {
@@ -329,13 +330,15 @@ class BeaconForegroundService : Service(), BeaconConsumer {
329
330
  enteredRegions.add(region.uniqueId)
330
331
  sendBeaconBroadcast(region, "enter", beacon.distance, beacon.rssi)
331
332
  showEnterExitNotification(region, "enter")
332
- scheduleTimeoutIfConfigured(region)
333
+ // Beacon returned — cancel any running timeout timer.
334
+ cancelTimeout(region.uniqueId)
333
335
  }
334
336
  HysteresisAction.EXIT -> {
335
- cancelTimeout(region.uniqueId)
336
337
  enteredRegions.remove(region.uniqueId)
337
338
  sendBeaconBroadcast(region, "exit", beacon.distance, beacon.rssi)
338
339
  showEnterExitNotification(region, "exit")
340
+ // Beacon left — start the timeout clock.
341
+ scheduleTimeoutIfConfigured(region)
339
342
  }
340
343
  HysteresisAction.NONE -> {}
341
344
  }
@@ -348,18 +351,14 @@ class BeaconForegroundService : Service(), BeaconConsumer {
348
351
  missCounters[region.uniqueId] = count
349
352
 
350
353
  if (enteredRegions.contains(region.uniqueId) && count >= EXIT_MISS_THRESHOLD) {
351
- // Do NOT cancel the timeout here. A miss-based exit is triggered by BLE
352
- // scan gaps (unreliable signal disappearance), not a confirmed physical
353
- // departure. Cancelling the timeout here would prevent it from ever firing
354
- // when the configured timeout (e.g. 25 s) exceeds the miss window (~21 s).
355
- // The timeout runnable fires unconditionally; distance-based exits still
356
- // call cancelTimeout() reliably when the beacon moves out of range.
357
354
  enteredRegions.remove(region.uniqueId)
358
355
  missCounters[region.uniqueId] = 0
359
356
  enterCounters[region.uniqueId] = 0
360
357
  exitCounters[region.uniqueId] = 0
361
358
  sendBeaconBroadcast(region, "exit", -1.0)
362
359
  showEnterExitNotification(region, "exit")
360
+ // Beacon disappeared — start the timeout clock.
361
+ scheduleTimeoutIfConfigured(region)
363
362
  }
364
363
  }
365
364
  }
@@ -371,18 +370,23 @@ class BeaconForegroundService : Service(), BeaconConsumer {
371
370
 
372
371
  /**
373
372
  * Apply exponential moving average (EMA) smoothing to a raw distance reading.
374
- * Returns null if the reading is a jump outlier (raw differs from smoothed by > DISTANCE_JUMP_FACTOR).
373
+ * If the reading is a large jump (> DISTANCE_JUMP_FACTOR), resets the EMA to the new
374
+ * value instead of rejecting it — this ensures the hysteresis pipeline keeps receiving
375
+ * data and can fire exit events when the user moves away from a beacon, rather than
376
+ * freezing because the EMA is stuck at the old close-range value.
375
377
  */
376
- private fun smoothDistance(regionId: String, rawDistance: Double): Double? {
378
+ private fun smoothDistance(regionId: String, rawDistance: Double): Double {
377
379
  val prev = smoothedDistances[regionId]
378
380
  if (prev == null) {
379
381
  smoothedDistances[regionId] = rawDistance
380
382
  return rawDistance
381
383
  }
382
- // Jump guard: if the raw value is wildly different, treat as outlier
384
+ // Jump guard: if the raw value is wildly different, reset EMA to the new reading
385
+ // so the hysteresis pipeline keeps receiving data and can fire the exit event.
383
386
  val ratio = if (prev > 0.001) rawDistance / prev else rawDistance
384
387
  if (ratio > DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / DISTANCE_JUMP_FACTOR)) {
385
- return null
388
+ smoothedDistances[regionId] = rawDistance
389
+ return rawDistance
386
390
  }
387
391
  val smoothed = DISTANCE_EMA_ALPHA * rawDistance + (1 - DISTANCE_EMA_ALPHA) * prev
388
392
  smoothedDistances[regionId] = smoothed
@@ -459,16 +463,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
459
463
 
460
464
  private fun scheduleTimeoutIfConfigured(region: Region) {
461
465
  val seconds = beaconTimeouts[region.uniqueId] ?: return
462
- // If a timer is already running, don't reset it. Miss-based exits clear
463
- // enteredRegions without cancelling the timer, so a subsequent re-entry
464
- // must not restart the clock — doing so would defer the timeout indefinitely.
465
- if (timeoutRunnables.containsKey(region.uniqueId)) return
466
+ // Cancel any existing timer so each exit resets the clock.
467
+ cancelTimeout(region.uniqueId)
466
468
  val runnable = Runnable {
467
469
  timeoutRunnables.remove(region.uniqueId)
468
- // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
469
- // the timer elapsed (BLE gaps can cause false exits at ~21 s), but the beacon
470
- // may still be physically present. Distance-based exits call cancelTimeout() so
471
- // this runnable is never queued when the beacon has genuinely moved away.
472
470
  sendBeaconBroadcast(region, "timeout", -1.0)
473
471
  }
474
472
  timeoutRunnables[region.uniqueId] = runnable
@@ -507,10 +505,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
507
505
  }
508
506
  monitoringEventName(isEddystone, eventType)?.let { logBeaconEvent(it, params) }
509
507
 
510
- // Forward enter/exit/timeout events to remote API (skip distance — too frequent)
511
- if (eventType != "distance") {
512
- apiForwarder?.forwardEvent(params)
513
- }
508
+ // Forward all produced events to remote API
509
+ apiForwarder?.forwardEvent(params)
514
510
 
515
511
  val intent = Intent(ACTION_BEACON_EVENT).apply {
516
512
  putExtra("identifier", region.uniqueId)
@@ -282,6 +282,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
282
282
  var maxDistance: Double? = null
283
283
  var exitDistance: Double? = null
284
284
  var minRssi: Int? = null
285
+ var level: String = "all"
285
286
  when (options) {
286
287
  is Double -> maxDistance = options
287
288
  is Map<*, *> -> {
@@ -290,6 +291,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
290
291
  maxDistance = (map["maxDistance"] as? Number)?.toDouble()
291
292
  exitDistance = (map["exitDistance"] as? Number)?.toDouble()
292
293
  minRssi = (map["minRssi"] as? Number)?.toInt()
294
+ level = (map["level"] as? String) ?: "all"
293
295
  val notifications = map["notifications"]
294
296
  if (notifications is Map<*, *>) {
295
297
  @Suppress("UNCHECKED_CAST")
@@ -322,6 +324,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
322
324
  else remove("exit_distance")
323
325
  if (minRssi != null) putInt("min_rssi", minRssi)
324
326
  else remove("min_rssi")
327
+ putString("level", level)
325
328
  }.apply()
326
329
  // Verify we have the permissions needed for background monitoring
327
330
  val hasLocation = ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
@@ -132,6 +132,13 @@ export type MonitoringOptions = {
132
132
  * Default: -85. Typical range: -100 (very permissive) to -70 (strict).
133
133
  */
134
134
  minRssi?: number;
135
+ /**
136
+ * Controls which event types are emitted, logged, and forwarded to the API.
137
+ *
138
+ * - `'all'` (default): distance + enter + exit + timeout events.
139
+ * - `'events'`: enter + exit + timeout only (no distance events).
140
+ */
141
+ level?: 'all' | 'events';
135
142
  /** Notification configuration overrides to apply for this monitoring session. */
136
143
  notifications?: NotificationConfig;
137
144
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for beacon timeout events (beacon in range for configured duration). */\r\nexport type BeaconTimeoutEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Configuration for beacon enter/exit event notifications. */\r\nexport type BeaconNotificationConfig = {\r\n /** Whether to show enter/exit notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on beacon enter. Default: \"Beacon Entered\". */\r\n enterTitle?: string;\r\n /** Notification title on beacon exit. Default: \"Beacon Exited\". */\r\n exitTitle?: string;\r\n /**\r\n * Notification body template. Supports {identifier} and {event} placeholders.\r\n * Default: \"{identifier} region {event}ed\".\r\n */\r\n body?: string;\r\n /** Play a sound with the notification (iOS only). Default: true. */\r\n sound?: boolean;\r\n /** Android drawable resource name for the notification icon (e.g. \"ic_notification\"). */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android foreground service notification (persistent status bar entry). */\r\nexport type ForegroundServiceConfig = {\r\n /** Title of the persistent notification. Default: \"Beacon Monitoring Active\". */\r\n title?: string;\r\n /** Body text of the persistent notification. Default: \"Monitoring for iBeacons in the background\". */\r\n text?: string;\r\n /** Android drawable resource name for the notification icon. */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android notification channel. */\r\nexport type NotificationChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"Beacon Monitoring\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"Used for background iBeacon region monitoring\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'low'.\r\n * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.\r\n */\r\n importance?: \"low\" | \"default\" | \"high\";\r\n};\r\n\r\n/** Combined notification configuration for all notification types. */\r\nexport type NotificationConfig = {\r\n /** Settings for beacon enter/exit event notifications. */\r\n beaconEvents?: BeaconNotificationConfig;\r\n /** Settings for the persistent foreground service notification (Android only). */\r\n foregroundService?: ForegroundServiceConfig;\r\n /** Settings for the Android notification channel (Android only). */\r\n channel?: NotificationChannelConfig;\r\n};\r\n\r\n/** Options accepted by startMonitoring(). */\r\nexport type MonitoringOptions = {\r\n /**\r\n * Maximum distance in metres for distance-based enter events.\r\n * Exit events are always emitted when the region is lost.\r\n */\r\n maxDistance?: number;\r\n /**\r\n * Distance in metres at which exit events fire (must be ≥ maxDistance).\r\n * Creates a hysteresis band between enter and exit thresholds to prevent\r\n * rapid toggling near the boundary.\r\n *\r\n * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.\r\n * Only used when `maxDistance` is set.\r\n */\r\n exitDistance?: number;\r\n /**\r\n * Minimum RSSI (dBm) for a beacon reading to be considered valid.\r\n * Readings below this threshold are discarded as unreliable, preventing\r\n * false detections from reflected or distant signals.\r\n *\r\n * Default: -85. Typical range: -100 (very permissive) to -70 (strict).\r\n */\r\n minRssi?: number;\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for Eddystone enter/exit region events. */\r\nexport type EddystoneRegionEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic Eddystone distance update events during monitoring. */\r\nexport type EddystoneDistanceEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for Eddystone timeout events (beacon in range for configured duration). */\r\nexport type EddystoneTimeoutEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired once after a paired beacon has been continuously in range for its configured `timeoutSeconds`. */\r\n onBeaconTimeout: (params: BeaconTimeoutEvent) => void;\r\n /** Fired continuously during a live scan as each iBeacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n /** Fired continuously during a live scan as each Eddystone beacon is detected. */\r\n onEddystoneFound: (params: EddystoneScanResult) => void;\r\n onEddystoneEnter: (params: EddystoneRegionEvent) => void;\r\n onEddystoneExit: (params: EddystoneRegionEvent) => void;\r\n onEddystoneDistance: (params: EddystoneDistanceEvent) => void;\r\n /** Fired once after a paired Eddystone has been continuously in range for its configured `timeoutSeconds`. */\r\n onEddystoneTimeout: (params: EddystoneTimeoutEvent) => void;\r\n};\r\n\r\n/** Options for filtering event logs. */\r\nexport type EventLogQueryOptions = {\r\n /** Maximum number of log entries to return (default: 1000, max: 10000). */\r\n limit?: number;\r\n /** Filter by event type (e.g. \"onBeaconEnter\", \"onBeaconExit\"). */\r\n eventType?: string;\r\n /** Only return events with timestamp >= this value (ms since epoch). */\r\n sinceTimestamp?: number;\r\n};\r\n\r\n/** A single logged beacon event entry. */\r\nexport type EventLogEntry = {\r\n id: number;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** The event type that was logged (e.g. \"onBeaconEnter\"). */\r\n eventType: string;\r\n /** Beacon identifier, if available. */\r\n identifier?: string;\r\n /** The full event payload that was sent to JS. */\r\n data: Record<string, unknown>;\r\n};\r\n"]}
1
+ {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n 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"]}
@@ -104,7 +104,7 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
104
104
  destroyEventLogs(): void;
105
105
  /**
106
106
  * Configure a remote API endpoint for native event forwarding.
107
- * Once set, enter/exit/timeout events are POSTed directly from native code,
107
+ * Once set, beacon events are POSTed directly from native code,
108
108
  * ensuring delivery even when the JS bridge is not active (app backgrounded).
109
109
  *
110
110
  * @param url The API endpoint URL to POST events to.
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAuKzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /**\r\n * Check whether the app is exempt from Android battery optimizations.\r\n * Always returns true on iOS and web (no equivalent concept).\r\n */\r\n isBatteryOptimizationExempt(): boolean;\r\n\r\n /**\r\n * Request exemption from Android battery optimizations.\r\n * Opens the system dialog asking the user to whitelist this app.\r\n * Returns true if the dialog was shown (or already exempt), false on failure.\r\n * Always resolves true on iOS and web.\r\n */\r\n requestBatteryOptimizationExemption(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n\r\n /**\r\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set, enter/exit/timeout events are POSTed directly from native code,\r\n * ensuring delivery even when the JS bridge is not active (app backgrounded).\r\n *\r\n * @param url The API endpoint URL to POST events to.\r\n * @param apiKey Optional API key sent as X-API-Key header.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
1
+ {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAuKzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /**\r\n * Check whether the app is exempt from Android battery optimizations.\r\n * Always returns true on iOS and web (no equivalent concept).\r\n */\r\n isBatteryOptimizationExempt(): boolean;\r\n\r\n /**\r\n * Request exemption from Android battery optimizations.\r\n * Opens the system dialog asking the user to whitelist this app.\r\n * Returns true if the dialog was shown (or already exempt), false on failure.\r\n * Always resolves true on iOS and web.\r\n */\r\n requestBatteryOptimizationExemption(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n\r\n /**\r\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set, beacon events are POSTed directly from native code,\r\n * ensuring delivery even when the JS bridge is not active (app backgrounded).\r\n *\r\n * @param url The API endpoint URL to POST events to.\r\n * @param apiKey Optional API key sent as X-API-Key header.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
@@ -12,6 +12,7 @@ private let EXIT_DISTANCE_KEY = "expo.beacon.exit_distance"
12
12
  private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
13
13
  private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
14
14
  private let MIN_RSSI_KEY = "expo.beacon.min_rssi"
15
+ private let EVENT_LEVEL_KEY = "expo.beacon.event_level"
15
16
 
16
17
  /// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
17
18
  private let DEFAULT_MIN_RSSI: Int = -85
@@ -98,6 +99,9 @@ public class ExpoBeaconModule: Module {
98
99
  /// Minimum RSSI threshold — readings below this are treated as unreliable.
99
100
  private var minRssiThreshold: Int = DEFAULT_MIN_RSSI
100
101
 
102
+ /// Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
103
+ private var eventLevel: String = "all"
104
+
101
105
  /// Distance smoothing (EMA) state per identifier.
102
106
  private var smoothedDistances: [String: Double] = [:]
103
107
  /// EMA weight for new readings. 0.4 balances responsiveness vs noise rejection.
@@ -331,6 +335,13 @@ public class ExpoBeaconModule: Module {
331
335
  maxDistance = map["maxDistance"] as? Double
332
336
  exitDistance = map["exitDistance"] as? Double
333
337
  minRssi = map["minRssi"] as? Int
338
+ if let lvl = map["level"] as? String, lvl == "events" || lvl == "all" {
339
+ self.eventLevel = lvl
340
+ self.defaults.set(lvl, forKey: EVENT_LEVEL_KEY)
341
+ } else {
342
+ self.eventLevel = "all"
343
+ self.defaults.set("all", forKey: EVENT_LEVEL_KEY)
344
+ }
334
345
  if let notifications = map["notifications"] as? [String: Any],
335
346
  let data = try? JSONSerialization.data(withJSONObject: notifications),
336
347
  let json = String(data: data, encoding: .utf8) {
@@ -392,6 +403,8 @@ public class ExpoBeaconModule: Module {
392
403
  self.defaults.set(false, forKey: IS_MONITORING_KEY)
393
404
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
394
405
  self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
406
+ self.defaults.removeObject(forKey: EVENT_LEVEL_KEY)
407
+ self.eventLevel = "all"
395
408
  self.stopRegionMonitoring()
396
409
  promise.resolve(nil)
397
410
  }
@@ -575,6 +588,9 @@ public class ExpoBeaconModule: Module {
575
588
  let storedRssi = defaults.object(forKey: MIN_RSSI_KEY) as? Int
576
589
  minRssiThreshold = storedRssi ?? DEFAULT_MIN_RSSI
577
590
 
591
+ // Restore persisted event level (survives app restarts)
592
+ eventLevel = defaults.string(forKey: EVENT_LEVEL_KEY) ?? "all"
593
+
578
594
  let beacons = loadPairedBeaconsRaw()
579
595
 
580
596
  // CLLocationManager supports a maximum of 20 monitored regions.
@@ -818,13 +834,10 @@ public class ExpoBeaconModule: Module {
818
834
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
819
835
  let hasValidDistance = distance.isFinite && distance >= 0
820
836
  if hasValidDistance || maxDist == nil {
821
- // Apply EMA smoothing; jump guard returns nil for outliers
837
+ // Apply EMA smoothing; jump resets EMA to the new value
822
838
  let effectiveDistance: Double
823
- if hasValidDistance, let smoothed = smoothDistance(identifier: identifier, rawDistance: distance) {
824
- effectiveDistance = smoothed
825
- } else if hasValidDistance {
826
- // Jump outlier — skip this cycle without resetting counters
827
- break
839
+ if hasValidDistance {
840
+ effectiveDistance = smoothDistance(identifier: identifier, rawDistance: distance)!
828
841
  } else {
829
842
  effectiveDistance = distance
830
843
  }
@@ -848,10 +861,11 @@ public class ExpoBeaconModule: Module {
848
861
  "rssi": beaconRssi
849
862
  ])
850
863
  postBeaconNotification(identifier: identifier, eventType: "enter")
851
- scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
864
+ // Beacon returned cancel any running timeout timer.
865
+ cancelEddystoneTimeout(identifier: identifier)
852
866
  case .exit:
853
867
  smoothedDistances.removeValue(forKey: identifier)
854
- cancelEddystoneTimeout(identifier: identifier)
868
+ print("[ExpoBeacon] DEBUG: Eddystone distance-based EXIT for \(identifier)")
855
869
  sendLoggedEvent("onEddystoneExit", [
856
870
  "identifier": identifier,
857
871
  "namespace": ns,
@@ -861,12 +875,15 @@ public class ExpoBeaconModule: Module {
861
875
  "rssi": beaconRssi
862
876
  ])
863
877
  postBeaconNotification(identifier: identifier, eventType: "exit")
878
+ // Beacon left — start the timeout clock.
879
+ scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
864
880
  case .none:
865
881
  break
866
882
  }
867
883
  }
868
884
 
869
885
  guard hasValidDistance else { break }
886
+ guard self.eventLevel == "all" else { break }
870
887
 
871
888
  // Throttle distance events — enter/exit above is evaluated on every
872
889
  // callback, but distance events are rate-limited to avoid flooding JS.
@@ -958,10 +975,8 @@ public class ExpoBeaconModule: Module {
958
975
  let identifier = params["identifier"] as? String
959
976
  getOrCreateEventLogger().logEvent(eventType: eventName, identifier: identifier, data: params)
960
977
  }
961
- // Forward enter/exit/timeout events to remote API (skip distance — too frequent)
962
- if !eventName.lowercased().contains("distance") {
963
- apiForwarder.forwardEvent(params)
964
- }
978
+ // Forward all produced events to remote API
979
+ apiForwarder.forwardEvent(params)
965
980
  sendEvent(eventName, params)
966
981
  }
967
982
 
@@ -1080,7 +1095,6 @@ public class ExpoBeaconModule: Module {
1080
1095
  eddystoneExitCounters[identifier] = 0
1081
1096
  eddystoneLatestSeen.removeValue(forKey: identifier)
1082
1097
  smoothedDistances.removeValue(forKey: identifier)
1083
- // Do NOT cancel the timeout here — same reason as iBeacon miss-based exit.
1084
1098
 
1085
1099
  let ns = paired["namespace"] as? String ?? ""
1086
1100
  let inst = paired["instance"] as? String ?? ""
@@ -1092,18 +1106,19 @@ public class ExpoBeaconModule: Module {
1092
1106
  "distance": -1
1093
1107
  ]
1094
1108
  sendLoggedEvent("onEddystoneExit", params)
1109
+ print("[ExpoBeacon] DEBUG: Eddystone miss-based EXIT for \(identifier)")
1095
1110
  postBeaconNotification(identifier: identifier, eventType: "exit")
1111
+ // Beacon disappeared — start the timeout clock.
1112
+ scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
1096
1113
  }
1097
1114
  }
1098
1115
  }
1099
1116
 
1100
1117
  // MARK: - Timeout timer helpers
1101
1118
 
1102
- private func scheduleBeaconTimeout(identifier: String, beacon: CLBeacon) {
1103
- // If a timer is already running, don't reset it. Miss-based exits clear
1104
- // enteredRegions without cancelling the timer, so a subsequent re-entry
1105
- // must not restart the clock — doing so would defer the timeout indefinitely.
1106
- guard beaconTimeoutTimers[identifier] == nil else { return }
1119
+ private func scheduleBeaconTimeout(identifier: String, beacon: CLBeacon? = nil, region: CLBeaconRegion? = nil) {
1120
+ // Cancel any existing timer so each exit resets the clock.
1121
+ cancelBeaconTimeout(identifier: identifier)
1107
1122
 
1108
1123
  let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
1109
1124
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1111,11 +1126,7 @@ public class ExpoBeaconModule: Module {
1111
1126
  let work = DispatchWorkItem { [weak self] in
1112
1127
  guard let self = self else { return }
1113
1128
  self.beaconTimeoutTimers.removeValue(forKey: identifier)
1114
- // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
1115
- // the timer elapsed (ranging gaps can cause false exits), but the beacon may still
1116
- // be physically present. Distance-based exits call cancelBeaconTimeout() so this
1117
- // work item is cancelled before it runs on genuine out-of-range departures.
1118
- self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon))
1129
+ self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon, region: region))
1119
1130
  }
1120
1131
  beaconTimeoutTimers[identifier] = work
1121
1132
  DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds), execute: work)
@@ -1126,8 +1137,8 @@ public class ExpoBeaconModule: Module {
1126
1137
  }
1127
1138
 
1128
1139
  private func scheduleEddystoneTimeout(identifier: String, namespace: String, instance: String) {
1129
- // If a timer is already running, don't reset it — same reason as iBeacon timeout.
1130
- guard eddystoneTimeoutTimers[identifier] == nil else { return }
1140
+ // Cancel any existing timer so each exit resets the clock.
1141
+ cancelEddystoneTimeout(identifier: identifier)
1131
1142
 
1132
1143
  let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
1133
1144
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1135,7 +1146,6 @@ public class ExpoBeaconModule: Module {
1135
1146
  let work = DispatchWorkItem { [weak self] in
1136
1147
  guard let self = self else { return }
1137
1148
  self.eddystoneTimeoutTimers.removeValue(forKey: identifier)
1138
- // Fire unconditionally — same reason as iBeacon timeout.
1139
1149
  self.sendLoggedEvent("onEddystoneTimeout", [
1140
1150
  "identifier": identifier,
1141
1151
  "namespace": namespace,
@@ -1333,8 +1343,10 @@ public class ExpoBeaconModule: Module {
1333
1343
  // Got a valid reading — reset miss counter
1334
1344
  missCounters[identifier] = 0
1335
1345
 
1336
- // Emit distance event every ranging cycle (~1 s)
1337
- sendLoggedEvent("onBeaconDistance", makeBeaconEventParams(identifier: identifier, beacon: beacon))
1346
+ // Emit distance event every ranging cycle (~1 s) if level allows
1347
+ if self.eventLevel == "all" {
1348
+ sendLoggedEvent("onBeaconDistance", makeBeaconEventParams(identifier: identifier, beacon: beacon))
1349
+ }
1338
1350
 
1339
1351
  // Enter/exit synthesis with hysteresis — always applied.
1340
1352
  // When maxDistance is set, distance thresholds control transitions.
@@ -1343,11 +1355,8 @@ public class ExpoBeaconModule: Module {
1343
1355
  let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
1344
1356
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
1345
1357
 
1346
- // Apply EMA smoothing; jump guard returns nil for outliers
1347
- guard let smoothed = smoothDistance(identifier: identifier, rawDistance: beacon.accuracy) else {
1348
- // Jump outlier — skip this cycle without resetting counters
1349
- return
1350
- }
1358
+ // Apply EMA smoothing; jump resets EMA to the new value
1359
+ let smoothed = smoothDistance(identifier: identifier, rawDistance: beacon.accuracy)!
1351
1360
 
1352
1361
  let action = evaluateDistanceHysteresis(
1353
1362
  identifier: identifier,
@@ -1362,12 +1371,14 @@ public class ExpoBeaconModule: Module {
1362
1371
  case .enter:
1363
1372
  sendLoggedEvent("onBeaconEnter", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "enter"))
1364
1373
  postBeaconNotification(identifier: identifier, eventType: "enter")
1365
- scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1374
+ // Beacon returned — cancel any running timeout timer.
1375
+ cancelBeaconTimeout(identifier: identifier)
1366
1376
  case .exit:
1367
1377
  smoothedDistances.removeValue(forKey: identifier)
1368
- cancelBeaconTimeout(identifier: identifier)
1369
1378
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
1370
1379
  postBeaconNotification(identifier: identifier, eventType: "exit")
1380
+ // Beacon left — start the timeout clock.
1381
+ scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1371
1382
  case .none:
1372
1383
  break
1373
1384
  }
@@ -1389,16 +1400,13 @@ public class ExpoBeaconModule: Module {
1389
1400
  enterCounters[identifier] = 0
1390
1401
  exitCounters[identifier] = 0
1391
1402
  smoothedDistances.removeValue(forKey: identifier)
1392
- // Do NOT cancel the timeout here. A miss-based exit is triggered by
1393
- // ranging gaps (e.g. accuracy == -1), not a confirmed physical departure.
1394
- // Cancelling here prevents the timeout from firing when it is longer than
1395
- // the miss window. Distance-based exits call cancelBeaconTimeout() so the
1396
- // timer is still cancelled on genuine out-of-range events.
1397
1403
 
1398
1404
  // Look up region info for the exit event payload
1399
1405
  let region = monitoredRegions.first { $0.identifier == identifier }
1400
1406
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: region, event: "exit"))
1401
1407
  postBeaconNotification(identifier: identifier, eventType: "exit")
1408
+ // Beacon disappeared — start the timeout clock.
1409
+ scheduleBeaconTimeout(identifier: identifier, region: region)
1402
1410
  }
1403
1411
  }
1404
1412
  return
@@ -1441,9 +1449,10 @@ public class ExpoBeaconModule: Module {
1441
1449
  missCounters.removeValue(forKey: identifier)
1442
1450
  smoothedDistances.removeValue(forKey: identifier)
1443
1451
  if wasEntered {
1444
- cancelBeaconTimeout(identifier: identifier)
1445
1452
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
1446
1453
  postBeaconNotification(identifier: identifier, eventType: "exit")
1454
+ // OS-level exit safety net — start the timeout clock.
1455
+ scheduleBeaconTimeout(identifier: identifier, region: beaconRegion)
1447
1456
  }
1448
1457
  }
1449
1458
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
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",
@@ -141,6 +141,13 @@ export type MonitoringOptions = {
141
141
  * Default: -85. Typical range: -100 (very permissive) to -70 (strict).
142
142
  */
143
143
  minRssi?: number;
144
+ /**
145
+ * Controls which event types are emitted, logged, and forwarded to the API.
146
+ *
147
+ * - `'all'` (default): distance + enter + exit + timeout events.
148
+ * - `'events'`: enter + exit + timeout only (no distance events).
149
+ */
150
+ level?: 'all' | 'events';
144
151
  /** Notification configuration overrides to apply for this monitoring session. */
145
152
  notifications?: NotificationConfig;
146
153
  };
@@ -156,7 +156,7 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
156
156
 
157
157
  /**
158
158
  * Configure a remote API endpoint for native event forwarding.
159
- * Once set, enter/exit/timeout events are POSTed directly from native code,
159
+ * Once set, beacon events are POSTed directly from native code,
160
160
  * ensuring delivery even when the JS bridge is not active (app backgrounded).
161
161
  *
162
162
  * @param url The API endpoint URL to POST events to.