expo-beacon 0.5.0 → 0.5.1

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":"AAAA,2CAA2C;AAC3C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qEAAqE;AACrE,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,MAAM,wBAAwB,GAAG;IACrC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,mGAAmG;AACnG,MAAM,MAAM,uBAAuB,GAAG;IACpC,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sGAAsG;IACtG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,yBAAyB,GAAG;IACtC,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CACzC,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,0DAA0D;IAC1D,YAAY,CAAC,EAAE,wBAAwB,CAAC;IACxC,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,oEAAoE;IACpE,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACrC,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC,CAAC;AAEF,4BAA4B;AAC5B,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,KAAK,CAAC;AAE/C,qDAAqD;AACrD,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAwB;AACxB,MAAM,MAAM,sBAAsB,GAAG;IACnC,aAAa,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACxD,yEAAyE;IACzE,aAAa,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,kFAAkF;IAClF,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACxD,gBAAgB,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACzD,eAAe,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACxD,mBAAmB,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC/D,CAAC"}
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.
@@ -282,10 +283,12 @@ public class ExpoBeaconModule: Module {
282
283
 
283
284
  AsyncFunction("startMonitoring") { (options: Either<Double, [String: Any]>?, promise: Promise) in
284
285
  var maxDistance: Double? = nil
286
+ var exitDistance: Double? = nil
285
287
  if let dist: Double = options?.get() {
286
288
  maxDistance = dist
287
289
  } else if let map: [String: Any] = options?.get() {
288
290
  maxDistance = map["maxDistance"] as? Double
291
+ exitDistance = map["exitDistance"] as? Double
289
292
  if let notifications = map["notifications"] as? [String: Any],
290
293
  let data = try? JSONSerialization.data(withJSONObject: notifications),
291
294
  let json = String(data: data, encoding: .utf8) {
@@ -297,6 +300,11 @@ public class ExpoBeaconModule: Module {
297
300
  } else {
298
301
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
299
302
  }
303
+ if let exitDist = exitDistance {
304
+ self.defaults.set(exitDist, forKey: EXIT_DISTANCE_KEY)
305
+ } else {
306
+ self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
307
+ }
300
308
  self.defaults.set(true, forKey: IS_MONITORING_KEY)
301
309
  self.requestLocationPermission(requireAlways: true) { granted in
302
310
  guard granted else {
@@ -312,6 +320,7 @@ public class ExpoBeaconModule: Module {
312
320
  AsyncFunction("stopMonitoring") { (promise: Promise) in
313
321
  self.defaults.set(false, forKey: IS_MONITORING_KEY)
314
322
  self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
323
+ self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
315
324
  self.stopRegionMonitoring()
316
325
  promise.resolve(nil)
317
326
  }
@@ -674,10 +683,12 @@ public class ExpoBeaconModule: Module {
674
683
 
675
684
  // Distance-driven enter/exit with hysteresis
676
685
  let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
686
+ let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
677
687
  let action = evaluateDistanceHysteresis(
678
688
  identifier: identifier,
679
689
  distance: distance,
680
690
  maxDistance: maxDist,
691
+ exitDistance: exitDist,
681
692
  entered: &eddystoneEnteredRegions,
682
693
  enterCtrs: &eddystoneEnterCounters,
683
694
  exitCtrs: &eddystoneExitCounters
@@ -921,17 +932,26 @@ public class ExpoBeaconModule: Module {
921
932
  case none, enter, exit
922
933
  }
923
934
 
935
+ /// Computes the effective exit distance from maxDistance and an optional explicit exitDistance.
936
+ /// Default: maxDistance + min(maxDistance × 0.5, 2.5).
937
+ private static func effectiveExitDistance(maxDistance: Double, exitDistance: Double?) -> Double {
938
+ if let explicit = exitDistance { return explicit }
939
+ return maxDistance + min(maxDistance * 0.5, 2.5)
940
+ }
941
+
924
942
  /// Shared distance-based enter/exit evaluation with hysteresis.
925
943
  /// Used by both iBeacon (handleDidRange) and Eddystone (handleEddystoneDiscovery) paths.
926
944
  private func evaluateDistanceHysteresis(
927
945
  identifier: String,
928
946
  distance: Double,
929
947
  maxDistance: Double?,
948
+ exitDistance: Double?,
930
949
  entered: inout Set<String>,
931
950
  enterCtrs: inout [String: Int],
932
951
  exitCtrs: inout [String: Int]
933
952
  ) -> HysteresisAction {
934
953
  if let maxDist = maxDistance {
954
+ let exitDist = Self.effectiveExitDistance(maxDistance: maxDist, exitDistance: exitDistance)
935
955
  if distance <= maxDist {
936
956
  exitCtrs[identifier] = 0
937
957
  enterCtrs[identifier] = (enterCtrs[identifier] ?? 0) + 1
@@ -940,7 +960,7 @@ public class ExpoBeaconModule: Module {
940
960
  enterCtrs[identifier] = 0
941
961
  return .enter
942
962
  }
943
- } else {
963
+ } else if distance > exitDist {
944
964
  enterCtrs[identifier] = 0
945
965
  exitCtrs[identifier] = (exitCtrs[identifier] ?? 0) + 1
946
966
  if entered.contains(identifier) && (exitCtrs[identifier] ?? 0) >= HYSTERESIS_COUNT {
@@ -948,6 +968,10 @@ public class ExpoBeaconModule: Module {
948
968
  exitCtrs[identifier] = 0
949
969
  return .exit
950
970
  }
971
+ } else {
972
+ // In the hysteresis band (maxDist < distance <= exitDist) — do nothing
973
+ enterCtrs[identifier] = 0
974
+ exitCtrs[identifier] = 0
951
975
  }
952
976
  } else {
953
977
  enterCtrs[identifier] = (enterCtrs[identifier] ?? 0) + 1
@@ -1020,10 +1044,12 @@ public class ExpoBeaconModule: Module {
1020
1044
 
1021
1045
  // Distance-driven enter/exit synthesis with hysteresis
1022
1046
  if let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double {
1047
+ let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
1023
1048
  let action = evaluateDistanceHysteresis(
1024
1049
  identifier: identifier,
1025
1050
  distance: beacon.accuracy,
1026
1051
  maxDistance: maxDist,
1052
+ exitDistance: exitDist,
1027
1053
  entered: &enteredRegions,
1028
1054
  enterCtrs: &enterCounters,
1029
1055
  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.1",
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
  };