expo-beacon 0.5.0 → 0.5.2

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.
package/README.md CHANGED
@@ -832,6 +832,7 @@ Accepts a `MonitoringOptions` object, a plain `number` (shorthand for `maxDistan
832
832
  | Property | Type | Default | Description |
833
833
  |---|---|---|---|
834
834
  | `maxDistance` | `number` | `undefined` | Distance threshold in metres. `onBeaconEnter` / `onEddystoneEnter` only fires when measured distance ≤ this value. `onBeaconExit` / `onEddystoneExit` always fires. Omit to disable filtering. |
835
+ | `exitDistance` | `number` | `maxDistance + min(maxDistance × 0.5, 2.5)` | Distance in metres at which exit events fire. Must be ≥ `maxDistance`. Creates a hysteresis band between enter and exit thresholds to prevent rapid toggling near the boundary. Only used when `maxDistance` is set. |
835
836
  | `notifications` | `NotificationConfig` | `undefined` | Notification overrides for this session (persisted). |
836
837
 
837
838
  **What happens on each platform**:
@@ -847,9 +848,10 @@ Accepts a `MonitoringOptions` object, a plain `number` (shorthand for `maxDistan
847
848
  // Shorthand — just a distance threshold
848
849
  await ExpoBeacon.startMonitoring(5);
849
850
 
850
- // Full options
851
+ // Full options with custom exit threshold
851
852
  await ExpoBeacon.startMonitoring({
852
853
  maxDistance: 10,
854
+ exitDistance: 15, // Exit fires when distance exceeds 15m
853
855
  notifications: {
854
856
  beaconEvents: {
855
857
  enterTitle: "Welcome!",
@@ -1181,6 +1183,7 @@ Passed to `startMonitoring()`.
1181
1183
  ```ts
1182
1184
  type MonitoringOptions = {
1183
1185
  maxDistance?: number;
1186
+ exitDistance?: number;
1184
1187
  notifications?: NotificationConfig;
1185
1188
  };
1186
1189
  ```
@@ -31,6 +31,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
31
31
 
32
32
  // Distance filtering
33
33
  @Volatile private var maxDistance: Double? = null
34
+ @Volatile private var exitDistance: Double? = null
34
35
  private val rangingRegions = java.util.concurrent.CopyOnWriteArraySet<Region>()
35
36
  private val enteredRegions = java.util.concurrent.CopyOnWriteArraySet<String>()
36
37
 
@@ -101,9 +102,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
101
102
  }
102
103
 
103
104
  override fun onBeaconServiceConnect() {
104
- // Read max distance from options prefs
105
+ // Read max distance and exit distance from options prefs
105
106
  val optPrefs = getSharedPreferences(MONITORING_OPTIONS_PREFS, Context.MODE_PRIVATE)
106
107
  maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
108
+ exitDistance = optPrefs.getString("exit_distance", null)?.toDoubleOrNull()
107
109
 
108
110
  beaconManager.addMonitorNotifier(monitorNotifier)
109
111
  beaconManager.addRangeNotifier(rangeNotifier)
@@ -296,6 +298,15 @@ class BeaconForegroundService : Service(), BeaconConsumer {
296
298
 
297
299
  private enum class HysteresisAction { NONE, ENTER, EXIT }
298
300
 
301
+ /**
302
+ * Computes the effective exit distance from maxDistance and an optional explicit exitDistance.
303
+ * Default: maxDistance + min(maxDistance × 0.5, 2.5).
304
+ */
305
+ private fun effectiveExitDistance(maxDist: Double): Double {
306
+ exitDistance?.let { return it }
307
+ return maxDist + minOf(maxDist * 0.5, 2.5)
308
+ }
309
+
299
310
  /**
300
311
  * Evaluate distance-based enter/exit with hysteresis counters.
301
312
  * Must be called within synchronized(distanceLock).
@@ -306,8 +317,9 @@ class BeaconForegroundService : Service(), BeaconConsumer {
306
317
  distance: Double,
307
318
  maxDist: Double
308
319
  ): HysteresisAction {
320
+ val exitDist = effectiveExitDistance(maxDist)
309
321
  if (distance <= maxDist) {
310
- // Inside threshold
322
+ // Inside enter threshold
311
323
  exitCounters[regionId] = 0
312
324
  val count = (enterCounters[regionId] ?: 0) + 1
313
325
  enterCounters[regionId] = count
@@ -315,8 +327,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
315
327
  enterCounters[regionId] = 0
316
328
  return HysteresisAction.ENTER
317
329
  }
318
- } else {
319
- // Outside threshold
330
+ } else if (distance > exitDist) {
331
+ // Outside exit threshold
320
332
  enterCounters[regionId] = 0
321
333
  val count = (exitCounters[regionId] ?: 0) + 1
322
334
  exitCounters[regionId] = count
@@ -324,6 +336,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
324
336
  exitCounters[regionId] = 0
325
337
  return HysteresisAction.EXIT
326
338
  }
339
+ } else {
340
+ // In the hysteresis band (maxDist < distance <= exitDist) — do nothing
341
+ enterCounters[regionId] = 0
342
+ exitCounters[regionId] = 0
327
343
  }
328
344
  return HysteresisAction.NONE
329
345
  }
@@ -264,12 +264,14 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
264
264
  return@AsyncFunction
265
265
  }
266
266
  var maxDistance: Double? = null
267
+ var exitDistance: Double? = null
267
268
  when (options) {
268
269
  is Double -> maxDistance = options
269
270
  is Map<*, *> -> {
270
271
  @Suppress("UNCHECKED_CAST")
271
272
  val map = options as Map<String, Any?>
272
273
  maxDistance = (map["maxDistance"] as? Number)?.toDouble()
274
+ exitDistance = (map["exitDistance"] as? Number)?.toDouble()
273
275
  val notifications = map["notifications"]
274
276
  if (notifications is Map<*, *>) {
275
277
  @Suppress("UNCHECKED_CAST")
@@ -282,6 +284,8 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
282
284
  .edit().apply {
283
285
  if (maxDistance != null) putString("max_distance", maxDistance.toString())
284
286
  else remove("max_distance")
287
+ if (exitDistance != null) putString("exit_distance", exitDistance.toString())
288
+ else remove("exit_distance")
285
289
  }.apply()
286
290
  // Verify we have the permissions needed for background monitoring
287
291
  val hasLocation = ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
@@ -92,6 +92,15 @@ export type MonitoringOptions = {
92
92
  * Exit events are always emitted when the region is lost.
93
93
  */
94
94
  maxDistance?: number;
95
+ /**
96
+ * Distance in metres at which exit events fire (must be ≥ maxDistance).
97
+ * Creates a hysteresis band between enter and exit thresholds to prevent
98
+ * rapid toggling near the boundary.
99
+ *
100
+ * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.
101
+ * Only used when `maxDistance` is set.
102
+ */
103
+ exitDistance?: number;
95
104
  /** Notification configuration overrides to apply for this monitoring session. */
96
105
  notifications?: NotificationConfig;
97
106
  };
@@ -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};\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};\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};\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};\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 /** 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};\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};\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};\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};\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 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};\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};\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};\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};\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};\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 /** 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};\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};\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};\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};\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 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};\r\n"]}
@@ -8,6 +8,7 @@ private let PAIRED_BEACONS_KEY = "expo.beacon.paired"
8
8
  private let PAIRED_EDDYSTONES_KEY = "expo.beacon.paired_eddystones"
9
9
  private let IS_MONITORING_KEY = "expo.beacon.is_monitoring"
10
10
  private let MAX_DISTANCE_KEY = "expo.beacon.max_distance"
11
+ private let EXIT_DISTANCE_KEY = "expo.beacon.exit_distance"
11
12
  private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
12
13
 
13
14
  /// Number of consecutive ranging misses before emitting a distance-based exit event.
@@ -97,10 +98,12 @@ public class ExpoBeaconModule: Module {
97
98
  }()
98
99
 
99
100
  public func definition() -> ModuleDefinition {
100
- migrateUserDefaultsIfNeeded()
101
-
102
101
  Name("ExpoBeacon")
103
102
 
103
+ OnCreate {
104
+ self.migrateUserDefaultsIfNeeded()
105
+ }
106
+
104
107
  Events("onBeaconEnter", "onBeaconExit", "onBeaconDistance", "onBeaconFound", "onEddystoneFound", "onEddystoneEnter", "onEddystoneExit", "onEddystoneDistance")
105
108
 
106
109
  // MARK: - Scan
@@ -282,10 +285,12 @@ public class ExpoBeaconModule: Module {
282
285
 
283
286
  AsyncFunction("startMonitoring") { (options: Either<Double, [String: Any]>?, promise: Promise) in
284
287
  var maxDistance: Double? = nil
288
+ var exitDistance: Double? = nil
285
289
  if let dist: Double = options?.get() {
286
290
  maxDistance = dist
287
291
  } else if let map: [String: Any] = options?.get() {
288
292
  maxDistance = map["maxDistance"] as? Double
293
+ exitDistance = map["exitDistance"] as? Double
289
294
  if let notifications = map["notifications"] as? [String: Any],
290
295
  let data = try? JSONSerialization.data(withJSONObject: notifications),
291
296
  let json = String(data: data, encoding: .utf8) {
@@ -297,6 +302,11 @@ public class ExpoBeaconModule: Module {
297
302
  } else {
298
303
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
299
304
  }
305
+ if let exitDist = exitDistance {
306
+ self.defaults.set(exitDist, forKey: EXIT_DISTANCE_KEY)
307
+ } else {
308
+ self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
309
+ }
300
310
  self.defaults.set(true, forKey: IS_MONITORING_KEY)
301
311
  self.requestLocationPermission(requireAlways: true) { granted in
302
312
  guard granted else {
@@ -312,6 +322,7 @@ public class ExpoBeaconModule: Module {
312
322
  AsyncFunction("stopMonitoring") { (promise: Promise) in
313
323
  self.defaults.set(false, forKey: IS_MONITORING_KEY)
314
324
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
325
+ self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
315
326
  self.stopRegionMonitoring()
316
327
  promise.resolve(nil)
317
328
  }
@@ -674,10 +685,12 @@ public class ExpoBeaconModule: Module {
674
685
 
675
686
  // Distance-driven enter/exit with hysteresis
676
687
  let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
688
+ let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
677
689
  let action = evaluateDistanceHysteresis(
678
690
  identifier: identifier,
679
691
  distance: distance,
680
692
  maxDistance: maxDist,
693
+ exitDistance: exitDist,
681
694
  entered: &eddystoneEnteredRegions,
682
695
  enterCtrs: &eddystoneEnterCounters,
683
696
  exitCtrs: &eddystoneExitCounters
@@ -921,17 +934,26 @@ public class ExpoBeaconModule: Module {
921
934
  case none, enter, exit
922
935
  }
923
936
 
937
+ /// Computes the effective exit distance from maxDistance and an optional explicit exitDistance.
938
+ /// Default: maxDistance + min(maxDistance × 0.5, 2.5).
939
+ private static func effectiveExitDistance(maxDistance: Double, exitDistance: Double?) -> Double {
940
+ if let explicit = exitDistance { return explicit }
941
+ return maxDistance + min(maxDistance * 0.5, 2.5)
942
+ }
943
+
924
944
  /// Shared distance-based enter/exit evaluation with hysteresis.
925
945
  /// Used by both iBeacon (handleDidRange) and Eddystone (handleEddystoneDiscovery) paths.
926
946
  private func evaluateDistanceHysteresis(
927
947
  identifier: String,
928
948
  distance: Double,
929
949
  maxDistance: Double?,
950
+ exitDistance: Double?,
930
951
  entered: inout Set<String>,
931
952
  enterCtrs: inout [String: Int],
932
953
  exitCtrs: inout [String: Int]
933
954
  ) -> HysteresisAction {
934
955
  if let maxDist = maxDistance {
956
+ let exitDist = Self.effectiveExitDistance(maxDistance: maxDist, exitDistance: exitDistance)
935
957
  if distance <= maxDist {
936
958
  exitCtrs[identifier] = 0
937
959
  enterCtrs[identifier] = (enterCtrs[identifier] ?? 0) + 1
@@ -940,7 +962,7 @@ public class ExpoBeaconModule: Module {
940
962
  enterCtrs[identifier] = 0
941
963
  return .enter
942
964
  }
943
- } else {
965
+ } else if distance > exitDist {
944
966
  enterCtrs[identifier] = 0
945
967
  exitCtrs[identifier] = (exitCtrs[identifier] ?? 0) + 1
946
968
  if entered.contains(identifier) && (exitCtrs[identifier] ?? 0) >= HYSTERESIS_COUNT {
@@ -948,6 +970,10 @@ public class ExpoBeaconModule: Module {
948
970
  exitCtrs[identifier] = 0
949
971
  return .exit
950
972
  }
973
+ } else {
974
+ // In the hysteresis band (maxDist < distance <= exitDist) — do nothing
975
+ enterCtrs[identifier] = 0
976
+ exitCtrs[identifier] = 0
951
977
  }
952
978
  } else {
953
979
  enterCtrs[identifier] = (enterCtrs[identifier] ?? 0) + 1
@@ -1020,10 +1046,12 @@ public class ExpoBeaconModule: Module {
1020
1046
 
1021
1047
  // Distance-driven enter/exit synthesis with hysteresis
1022
1048
  if let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double {
1049
+ let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
1023
1050
  let action = evaluateDistanceHysteresis(
1024
1051
  identifier: identifier,
1025
1052
  distance: beacon.accuracy,
1026
1053
  maxDistance: maxDist,
1054
+ exitDistance: exitDist,
1027
1055
  entered: &enteredRegions,
1028
1056
  enterCtrs: &enterCounters,
1029
1057
  exitCtrs: &exitCounters
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",
@@ -100,6 +100,15 @@ export type MonitoringOptions = {
100
100
  * Exit events are always emitted when the region is lost.
101
101
  */
102
102
  maxDistance?: number;
103
+ /**
104
+ * Distance in metres at which exit events fire (must be ≥ maxDistance).
105
+ * Creates a hysteresis band between enter and exit thresholds to prevent
106
+ * rapid toggling near the boundary.
107
+ *
108
+ * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.
109
+ * Only used when `maxDistance` is set.
110
+ */
111
+ exitDistance?: number;
103
112
  /** Notification configuration overrides to apply for this monitoring session. */
104
113
  notifications?: NotificationConfig;
105
114
  };