expo-beacon 0.6.8 → 0.6.10

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.
@@ -0,0 +1,101 @@
1
+ package expo.modules.beacon
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.os.Build
6
+ import android.util.Log
7
+ import org.json.JSONObject
8
+ import java.io.OutputStreamWriter
9
+ import java.net.HttpURLConnection
10
+ import java.net.URL
11
+ import java.util.concurrent.Executors
12
+
13
+ private const val API_PREFS = "expo.beacon.api_config"
14
+ private const val API_URL_KEY = "api_url"
15
+ private const val API_KEY_KEY = "api_key"
16
+ private const val MAX_RETRIES = 3
17
+
18
+ /**
19
+ * Fire-and-forget HTTP event forwarder for beacon events.
20
+ * Sends enter/exit/timeout events to a configured API endpoint from native code,
21
+ * ensuring delivery even when the JS bridge is not active (app backgrounded).
22
+ */
23
+ internal class BeaconApiForwarder(private val context: Context) {
24
+
25
+ private val executor = Executors.newSingleThreadExecutor()
26
+ private val prefs: SharedPreferences by lazy {
27
+ context.getSharedPreferences(API_PREFS, Context.MODE_PRIVATE)
28
+ }
29
+
30
+ val isConfigured: Boolean
31
+ get() = prefs.getString(API_URL_KEY, null)?.isNotEmpty() == true
32
+
33
+ fun configure(url: String, apiKey: String?) {
34
+ prefs.edit().apply {
35
+ putString(API_URL_KEY, url)
36
+ if (apiKey != null) putString(API_KEY_KEY, apiKey)
37
+ else remove(API_KEY_KEY)
38
+ }.apply()
39
+ }
40
+
41
+ fun getConfig(): Map<String, String?> {
42
+ return mapOf(
43
+ "url" to prefs.getString(API_URL_KEY, null),
44
+ "apiKey" to prefs.getString(API_KEY_KEY, null)
45
+ )
46
+ }
47
+
48
+ /**
49
+ * Send a beacon event to the configured API endpoint.
50
+ * Fire-and-forget with simple retry (3 attempts, exponential backoff).
51
+ * No-op if no endpoint is configured.
52
+ */
53
+ fun forwardEvent(params: Map<String, Any?>) {
54
+ val url = prefs.getString(API_URL_KEY, null)
55
+ if (url.isNullOrEmpty()) return
56
+
57
+ val apiKey = prefs.getString(API_KEY_KEY, null)
58
+ val payload = JSONObject().apply {
59
+ for ((k, v) in params) {
60
+ put(k, v ?: JSONObject.NULL)
61
+ }
62
+ put("timestamp", System.currentTimeMillis())
63
+ put("platform", "android")
64
+ put("sdkVersion", Build.VERSION.SDK_INT)
65
+ }
66
+
67
+ executor.execute {
68
+ var lastException: Exception? = null
69
+ for (attempt in 1..MAX_RETRIES) {
70
+ try {
71
+ val conn = URL(url).openConnection() as HttpURLConnection
72
+ conn.apply {
73
+ requestMethod = "POST"
74
+ setRequestProperty("Content-Type", "application/json")
75
+ apiKey?.let { setRequestProperty("X-API-Key", it) }
76
+ connectTimeout = 10_000
77
+ readTimeout = 10_000
78
+ doOutput = true
79
+ }
80
+ OutputStreamWriter(conn.outputStream, Charsets.UTF_8).use { it.write(payload.toString()) }
81
+ val code = conn.responseCode
82
+ conn.disconnect()
83
+ if (code in 200..299) return@execute
84
+ // 4xx client errors — don't retry
85
+ if (code in 400..499) {
86
+ Log.w(TAG, "API forward failed with $code — not retrying")
87
+ return@execute
88
+ }
89
+ lastException = RuntimeException("HTTP $code")
90
+ } catch (e: Exception) {
91
+ lastException = e
92
+ }
93
+ // Exponential backoff: 1s, 2s, 4s
94
+ if (attempt < MAX_RETRIES) {
95
+ try { Thread.sleep(1000L * (1 shl (attempt - 1))) } catch (_: InterruptedException) {}
96
+ }
97
+ }
98
+ Log.w(TAG, "API forward failed after $MAX_RETRIES attempts: ${lastException?.message}")
99
+ }
100
+ }
101
+ }
@@ -47,6 +47,9 @@ internal const val HYSTERESIS_COUNT = 3
47
47
  */
48
48
  internal const val REGION_EXIT_PERIOD_MS = 60000L
49
49
 
50
+ /** Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable. */
51
+ internal const val DEFAULT_MIN_RSSI = -85
52
+
50
53
  /** Shared log tag for the expo-beacon module. */
51
54
  internal const val TAG = "ExpoBeacon"
52
55
 
@@ -24,6 +24,7 @@ class BeaconEventReceiver(
24
24
  val eventType = intent.getStringExtra("event") ?: return
25
25
  val beaconType = intent.getStringExtra("beaconType") ?: "ibeacon"
26
26
  val distance = intent.getDoubleExtra("distance", -1.0)
27
+ val rssi = intent.getIntExtra("rssi", 0)
27
28
 
28
29
  if (beaconType == "eddystone") {
29
30
  val namespace = intent.getStringExtra("namespace") ?: ""
@@ -34,7 +35,8 @@ class BeaconEventReceiver(
34
35
  "namespace" to namespace,
35
36
  "instance" to instance,
36
37
  "event" to eventType,
37
- "distance" to distance
38
+ "distance" to distance,
39
+ "rssi" to rssi
38
40
  )
39
41
 
40
42
  val eventName = when (eventType) {
@@ -56,7 +58,8 @@ class BeaconEventReceiver(
56
58
  "major" to major,
57
59
  "minor" to minor,
58
60
  "event" to eventType,
59
- "distance" to distance
61
+ "distance" to distance,
62
+ "rssi" to rssi
60
63
  )
61
64
 
62
65
  val eventName = when (eventType) {
@@ -38,6 +38,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
38
38
  // Distance filtering
39
39
  @Volatile private var maxDistance: Double? = null
40
40
  @Volatile private var exitDistance: Double? = null
41
+ @Volatile private var minRssiThreshold: Int = DEFAULT_MIN_RSSI
41
42
  private val monitoredRegionIds = java.util.concurrent.CopyOnWriteArraySet<String>()
42
43
  private val enteredRegions = java.util.concurrent.CopyOnWriteArraySet<String>()
43
44
  private val lastSeenAtMs = java.util.concurrent.ConcurrentHashMap<String, Long>()
@@ -48,6 +49,9 @@ class BeaconForegroundService : Service(), BeaconConsumer {
48
49
  private val exitCounters = java.util.concurrent.ConcurrentHashMap<String, Int>()
49
50
  private val missCounters = java.util.concurrent.ConcurrentHashMap<String, Int>()
50
51
 
52
+ // Distance smoothing (EMA)
53
+ private val smoothedDistances = java.util.concurrent.ConcurrentHashMap<String, Double>()
54
+
51
55
  // Notification ID counter for unique per-beacon notifications
52
56
  private val notifIdCounter = AtomicInteger(0)
53
57
  private val notifIdMap = java.util.concurrent.ConcurrentHashMap<String, Int>()
@@ -61,10 +65,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
61
65
  // Per-beacon timeout seconds lookup (identifier → seconds), loaded from paired data
62
66
  private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
63
67
  private var eventLogger: BeaconEventLogger? = null
68
+ private var apiForwarder: BeaconApiForwarder? = null
64
69
 
65
70
  override fun onCreate() {
66
71
  super.onCreate()
67
72
  createNotificationChannel()
73
+ apiForwarder = BeaconApiForwarder(this)
68
74
  beaconManager = BeaconManager.getInstanceForApplication(this).also { manager ->
69
75
  BeaconParsers.ensureRegistered(manager)
70
76
  try { manager.setEnableScheduledScanJobs(false) } catch (e: IllegalStateException) { Log.w(TAG, "setEnableScheduledScanJobs failed", e) }
@@ -114,10 +120,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
114
120
 
115
121
  override fun onBeaconServiceConnect() {
116
122
  serviceConnected = true
117
- // Read max distance and exit distance from options prefs
123
+ // Read max distance, exit distance, and min RSSI from options prefs
118
124
  val optPrefs = getSharedPreferences(MONITORING_OPTIONS_PREFS, Context.MODE_PRIVATE)
119
125
  maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
120
126
  exitDistance = optPrefs.getString("exit_distance", null)?.toDoubleOrNull()
127
+ minRssiThreshold = optPrefs.getInt("min_rssi", DEFAULT_MIN_RSSI)
121
128
 
122
129
  beaconManager.addMonitorNotifier(monitorNotifier)
123
130
  beaconManager.addRangeNotifier(rangeNotifier)
@@ -171,6 +178,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
171
178
  enterCounters.clear()
172
179
  exitCounters.clear()
173
180
  missCounters.clear()
181
+ smoothedDistances.clear()
174
182
  }
175
183
  // NOTE: enteredRegions is intentionally NOT cleared here.
176
184
  // Clearing it on every reload (e.g. START_STICKY restart or repeated
@@ -249,10 +257,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
249
257
  // Distance logging only — emits distance broadcasts. Enter/exit logic lives in rangeNotifier.
250
258
  private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
251
259
  if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
252
- val closest = beacons.filter { it.distance >= 0 }.minByOrNull { it.distance }
260
+ val closest = beacons.filter { it.distance >= 0 && it.rssi >= minRssiThreshold }.minByOrNull { it.distance }
253
261
  if (closest != null) {
254
262
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
255
- sendBeaconBroadcast(region, "distance", closest.distance)
263
+ sendBeaconBroadcast(region, "distance", closest.distance, closest.rssi)
256
264
  }
257
265
  }
258
266
 
@@ -299,7 +307,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
299
307
  if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
300
308
 
301
309
  val beacon = beacons
302
- .filter { it.distance >= 0 }
310
+ .filter { it.distance >= 0 && it.rssi >= minRssiThreshold }
303
311
  .minByOrNull { it.distance }
304
312
 
305
313
  synchronized(distanceLock) {
@@ -308,18 +316,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
308
316
  lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
309
317
  missCounters[region.uniqueId] = 0
310
318
 
311
- val action = evaluateDistanceHysteresis(region.uniqueId, beacon.distance, maxDist)
319
+ // Apply EMA smoothing; jump guard returns null for outliers
320
+ 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
+ val action = evaluateDistanceHysteresis(region.uniqueId, smoothed, maxDist)
312
327
  when (action) {
313
328
  HysteresisAction.ENTER -> {
314
329
  enteredRegions.add(region.uniqueId)
315
- sendBeaconBroadcast(region, "enter", beacon.distance)
330
+ sendBeaconBroadcast(region, "enter", beacon.distance, beacon.rssi)
316
331
  showEnterExitNotification(region, "enter")
317
332
  scheduleTimeoutIfConfigured(region)
318
333
  }
319
334
  HysteresisAction.EXIT -> {
320
335
  cancelTimeout(region.uniqueId)
321
336
  enteredRegions.remove(region.uniqueId)
322
- sendBeaconBroadcast(region, "exit", beacon.distance)
337
+ sendBeaconBroadcast(region, "exit", beacon.distance, beacon.rssi)
323
338
  showEnterExitNotification(region, "exit")
324
339
  }
325
340
  HysteresisAction.NONE -> {}
@@ -333,7 +348,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
333
348
  missCounters[region.uniqueId] = count
334
349
 
335
350
  if (enteredRegions.contains(region.uniqueId) && count >= EXIT_MISS_THRESHOLD) {
336
- cancelTimeout(region.uniqueId)
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.
337
357
  enteredRegions.remove(region.uniqueId)
338
358
  missCounters[region.uniqueId] = 0
339
359
  enterCounters[region.uniqueId] = 0
@@ -349,6 +369,26 @@ class BeaconForegroundService : Service(), BeaconConsumer {
349
369
 
350
370
  private enum class HysteresisAction { NONE, ENTER, EXIT }
351
371
 
372
+ /**
373
+ * 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).
375
+ */
376
+ private fun smoothDistance(regionId: String, rawDistance: Double): Double? {
377
+ val prev = smoothedDistances[regionId]
378
+ if (prev == null) {
379
+ smoothedDistances[regionId] = rawDistance
380
+ return rawDistance
381
+ }
382
+ // Jump guard: if the raw value is wildly different, treat as outlier
383
+ val ratio = if (prev > 0.001) rawDistance / prev else rawDistance
384
+ if (ratio > DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / DISTANCE_JUMP_FACTOR)) {
385
+ return null
386
+ }
387
+ val smoothed = DISTANCE_EMA_ALPHA * rawDistance + (1 - DISTANCE_EMA_ALPHA) * prev
388
+ smoothedDistances[regionId] = smoothed
389
+ return smoothed
390
+ }
391
+
352
392
  /**
353
393
  * Computes the effective exit distance from maxDistance and an optional explicit exitDistance.
354
394
  * Default: maxDistance + min(maxDistance × 0.5, 2.5).
@@ -423,10 +463,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
423
463
  cancelTimeout(region.uniqueId)
424
464
  val runnable = Runnable {
425
465
  timeoutRunnables.remove(region.uniqueId)
426
- // Only fire if the beacon is still in range
427
- if (enteredRegions.contains(region.uniqueId)) {
428
- sendBeaconBroadcast(region, "timeout", -1.0)
429
- }
466
+ // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
467
+ // the timer elapsed (BLE gaps can cause false exits at ~21 s), but the beacon
468
+ // may still be physically present. Distance-based exits call cancelTimeout() so
469
+ // this runnable is never queued when the beacon has genuinely moved away.
470
+ sendBeaconBroadcast(region, "timeout", -1.0)
430
471
  }
431
472
  timeoutRunnables[region.uniqueId] = runnable
432
473
  timeoutHandler.postDelayed(runnable, seconds * 1000L)
@@ -436,7 +477,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
436
477
  timeoutRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
437
478
  }
438
479
 
439
- private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double) {
480
+ private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double, rssi: Int = 0) {
440
481
  // Determine if this is an Eddystone region based on identifier format
441
482
  // Eddystone regions have id1 as a hex namespace (not a UUID)
442
483
  val id1Str = region.id1?.toString() ?: ""
@@ -449,6 +490,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
449
490
  put("instance", region.id2?.toString()?.removePrefix("0x") ?: "")
450
491
  put("event", eventType)
451
492
  put("distance", distance)
493
+ put("rssi", rssi)
452
494
  }
453
495
  } else {
454
496
  buildMap<String, Any?> {
@@ -458,14 +500,21 @@ class BeaconForegroundService : Service(), BeaconConsumer {
458
500
  put("minor", region.id3?.toInt() ?: 0)
459
501
  put("event", eventType)
460
502
  put("distance", distance)
503
+ put("rssi", rssi)
461
504
  }
462
505
  }
463
506
  monitoringEventName(isEddystone, eventType)?.let { logBeaconEvent(it, params) }
464
507
 
508
+ // Forward enter/exit/timeout events to remote API (skip distance — too frequent)
509
+ if (eventType != "distance") {
510
+ apiForwarder?.forwardEvent(params)
511
+ }
512
+
465
513
  val intent = Intent(ACTION_BEACON_EVENT).apply {
466
514
  putExtra("identifier", region.uniqueId)
467
515
  putExtra("event", eventType)
468
516
  putExtra("distance", distance)
517
+ putExtra("rssi", rssi)
469
518
  if (isEddystone) {
470
519
  putExtra("beaconType", "eddystone")
471
520
  putExtra("namespace", id1Str.removePrefix("0x"))
@@ -587,6 +636,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
587
636
  }
588
637
 
589
638
  companion object {
639
+ /** EMA weight for new readings. 0.4 balances responsiveness vs noise rejection. */
640
+ const val DISTANCE_EMA_ALPHA = 0.4
641
+ /** If raw distance differs from smoothed by more than this factor, treat as outlier. */
642
+ const val DISTANCE_JUMP_FACTOR = 5.0
643
+
590
644
  private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
591
645
 
592
646
  fun start(context: Context) {
@@ -281,6 +281,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
281
281
  }
282
282
  var maxDistance: Double? = null
283
283
  var exitDistance: Double? = null
284
+ var minRssi: Int? = null
284
285
  when (options) {
285
286
  is Double -> maxDistance = options
286
287
  is Map<*, *> -> {
@@ -288,6 +289,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
288
289
  val map = options as Map<String, Any?>
289
290
  maxDistance = (map["maxDistance"] as? Number)?.toDouble()
290
291
  exitDistance = (map["exitDistance"] as? Number)?.toDouble()
292
+ minRssi = (map["minRssi"] as? Number)?.toInt()
291
293
  val notifications = map["notifications"]
292
294
  if (notifications is Map<*, *>) {
293
295
  @Suppress("UNCHECKED_CAST")
@@ -318,6 +320,8 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
318
320
  else remove("max_distance")
319
321
  if (exitDistance != null) putString("exit_distance", exitDistance.toString())
320
322
  else remove("exit_distance")
323
+ if (minRssi != null) putInt("min_rssi", minRssi)
324
+ else remove("min_rssi")
321
325
  }.apply()
322
326
  // Verify we have the permissions needed for background monitoring
323
327
  val hasLocation = ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
@@ -449,6 +453,14 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
449
453
  BeaconEventLogger.deleteLogDatabase(ctx)
450
454
  }
451
455
 
456
+ // MARK: - API Forwarding
457
+
458
+ Function("setApiEndpoint") { url: String, apiKey: String? ->
459
+ val ctx = appContext.reactContext
460
+ ?: throw IllegalStateException("React context is not available")
461
+ BeaconApiForwarder(ctx).configure(url, apiKey)
462
+ }
463
+
452
464
  // MARK: - Battery Optimization
453
465
 
454
466
  Function("isBatteryOptimizationExempt") {
@@ -38,6 +38,8 @@ export type BeaconRegionEvent = {
38
38
  event: "enter" | "exit";
39
39
  /** Measured distance in metres at the time of the event (–1 if unavailable). */
40
40
  distance: number;
41
+ /** Signal strength in dBm at the time of the event (0 if unavailable). */
42
+ rssi?: number;
41
43
  };
42
44
  /** Payload for periodic distance update events during monitoring. */
43
45
  export type BeaconDistanceEvent = {
@@ -46,6 +48,8 @@ export type BeaconDistanceEvent = {
46
48
  major: number;
47
49
  minor: number;
48
50
  distance: number;
51
+ /** Signal strength in dBm (0 if unavailable). */
52
+ rssi?: number;
49
53
  };
50
54
  /** Payload for beacon timeout events (beacon in range for configured duration). */
51
55
  export type BeaconTimeoutEvent = {
@@ -120,6 +124,14 @@ export type MonitoringOptions = {
120
124
  * Only used when `maxDistance` is set.
121
125
  */
122
126
  exitDistance?: number;
127
+ /**
128
+ * Minimum RSSI (dBm) for a beacon reading to be considered valid.
129
+ * Readings below this threshold are discarded as unreliable, preventing
130
+ * false detections from reflected or distant signals.
131
+ *
132
+ * Default: -85. Typical range: -100 (very permissive) to -70 (strict).
133
+ */
134
+ minRssi?: number;
123
135
  /** Notification configuration overrides to apply for this monitoring session. */
124
136
  notifications?: NotificationConfig;
125
137
  };
@@ -169,6 +181,8 @@ export type EddystoneRegionEvent = {
169
181
  event: "enter" | "exit";
170
182
  /** Measured distance in metres at the time of the event (–1 if unavailable). */
171
183
  distance: number;
184
+ /** Signal strength in dBm at the time of the event (0 if unavailable). */
185
+ rssi?: number;
172
186
  };
173
187
  /** Payload for periodic Eddystone distance update events during monitoring. */
174
188
  export type EddystoneDistanceEvent = {
@@ -176,6 +190,8 @@ export type EddystoneDistanceEvent = {
176
190
  namespace: string;
177
191
  instance: string;
178
192
  distance: number;
193
+ /** Signal strength in dBm (0 if unavailable). */
194
+ rssi?: number;
179
195
  };
180
196
  /** Payload for Eddystone timeout events (beacon in range for configured duration). */
181
197
  export type EddystoneTimeoutEvent = {
@@ -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};\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/** 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 /** 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};\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/** 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 /** 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"]}
@@ -102,6 +102,15 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
102
102
  clearEventLogs(): void;
103
103
  /** Delete the entire event log database. Also disables logging. */
104
104
  destroyEventLogs(): void;
105
+ /**
106
+ * Configure a remote API endpoint for native event forwarding.
107
+ * Once set, enter/exit/timeout events are POSTed directly from native code,
108
+ * ensuring delivery even when the JS bridge is not active (app backgrounded).
109
+ *
110
+ * @param url The API endpoint URL to POST events to.
111
+ * @param apiKey Optional API key sent as X-API-Key header.
112
+ */
113
+ setApiEndpoint(url: string, apiKey?: string): void;
105
114
  }
106
115
  declare const _default: ExpoBeaconModule;
107
116
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI;IAElB,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAE3C;;;OAGG;IACH,2BAA2B,IAAI,OAAO;IAEtC;;;;;OAKG;IACH,mCAAmC,IAAI,OAAO,CAAC,OAAO,CAAC;IAEvD,4FAA4F;IAC5F,kBAAkB,IAAI,IAAI;IAE1B,oEAAoE;IACpE,mBAAmB,IAAI,IAAI;IAE3B;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;CACzB;;AAED,wBAAmE"}
1
+ {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI;IAElB,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAE3C;;;OAGG;IACH,2BAA2B,IAAI,OAAO;IAEtC;;;;;OAKG;IACH,mCAAmC,IAAI,OAAO,CAAC,OAAO,CAAC;IAEvD,4FAA4F;IAC5F,kBAAkB,IAAI,IAAI;IAE1B,oEAAoE;IACpE,mBAAmB,IAAI,IAAI;IAE3B;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;IAExB;;;;;;;OAOG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CACnD;;AAED,wBAAmE"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA6JzD,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\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, 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"]}
@@ -0,0 +1,104 @@
1
+ import Foundation
2
+ import os.log
3
+
4
+ private let API_URL_KEY = "expo.beacon.api_url"
5
+ private let API_KEY_KEY = "expo.beacon.api_key"
6
+ private let MAX_RETRIES = 3
7
+
8
+ /// Fire-and-forget HTTP event forwarder for beacon events.
9
+ /// Sends enter/exit/timeout events to a configured API endpoint from native code,
10
+ /// ensuring delivery even when the JS bridge is not active (app backgrounded).
11
+ final class BeaconApiForwarder {
12
+
13
+ private let defaults: UserDefaults
14
+ private let session: URLSession
15
+
16
+ var isConfigured: Bool {
17
+ guard let url = defaults.string(forKey: API_URL_KEY) else { return false }
18
+ return !url.isEmpty
19
+ }
20
+
21
+ init(defaults: UserDefaults? = nil) {
22
+ self.defaults = defaults ?? (UserDefaults(suiteName: "expo.modules.beacon") ?? .standard)
23
+ let config = URLSessionConfiguration.default
24
+ config.timeoutIntervalForRequest = 10
25
+ config.timeoutIntervalForResource = 30
26
+ // Use background-safe waitsForConnectivity so requests survive brief connectivity gaps
27
+ config.waitsForConnectivity = true
28
+ self.session = URLSession(configuration: config)
29
+ }
30
+
31
+ func configure(url: String, apiKey: String?) {
32
+ defaults.set(url, forKey: API_URL_KEY)
33
+ if let key = apiKey {
34
+ defaults.set(key, forKey: API_KEY_KEY)
35
+ } else {
36
+ defaults.removeObject(forKey: API_KEY_KEY)
37
+ }
38
+ }
39
+
40
+ func getConfig() -> [String: String?] {
41
+ return [
42
+ "url": defaults.string(forKey: API_URL_KEY),
43
+ "apiKey": defaults.string(forKey: API_KEY_KEY)
44
+ ]
45
+ }
46
+
47
+ /// Send a beacon event to the configured API endpoint.
48
+ /// Fire-and-forget with simple retry (3 attempts, exponential backoff).
49
+ /// No-op if no endpoint is configured.
50
+ func forwardEvent(_ params: [String: Any]) {
51
+ guard let urlString = defaults.string(forKey: API_URL_KEY),
52
+ !urlString.isEmpty,
53
+ let url = URL(string: urlString) else { return }
54
+
55
+ let apiKey = defaults.string(forKey: API_KEY_KEY)
56
+
57
+ var payload = params
58
+ payload["timestamp"] = Int64(Date().timeIntervalSince1970 * 1000)
59
+ payload["platform"] = "ios"
60
+ payload["sdkVersion"] = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
61
+
62
+ guard let body = try? JSONSerialization.data(withJSONObject: payload) else {
63
+ os_log(.error, "BeaconApiForwarder: failed to serialize payload")
64
+ return
65
+ }
66
+
67
+ sendWithRetry(url: url, body: body, apiKey: apiKey, attempt: 1)
68
+ }
69
+
70
+ private func sendWithRetry(url: URL, body: Data, apiKey: String?, attempt: Int) {
71
+ var request = URLRequest(url: url)
72
+ request.httpMethod = "POST"
73
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
74
+ if let key = apiKey {
75
+ request.setValue(key, forHTTPHeaderField: "X-API-Key")
76
+ }
77
+ request.httpBody = body
78
+
79
+ let task = session.dataTask(with: request) { [weak self] _, response, error in
80
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
81
+
82
+ if statusCode >= 200 && statusCode < 300 {
83
+ return // Success
84
+ }
85
+
86
+ // 4xx client errors — don't retry
87
+ if statusCode >= 400 && statusCode < 500 {
88
+ os_log(.error, "BeaconApiForwarder: HTTP %{public}d — not retrying", statusCode)
89
+ return
90
+ }
91
+
92
+ if attempt < MAX_RETRIES {
93
+ let delay = pow(2.0, Double(attempt - 1)) // 1s, 2s, 4s
94
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + delay) {
95
+ self?.sendWithRetry(url: url, body: body, apiKey: apiKey, attempt: attempt + 1)
96
+ }
97
+ } else {
98
+ let msg = error?.localizedDescription ?? "HTTP \(statusCode)"
99
+ os_log(.error, "BeaconApiForwarder: failed after %d attempts: %{public}@", MAX_RETRIES, msg)
100
+ }
101
+ }
102
+ task.resume()
103
+ }
104
+ }
@@ -11,12 +11,17 @@ private let MAX_DISTANCE_KEY = "expo.beacon.max_distance"
11
11
  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
+ private let MIN_RSSI_KEY = "expo.beacon.min_rssi"
15
+
16
+ /// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
17
+ private let DEFAULT_MIN_RSSI: Int = -85
14
18
 
15
19
  /// Number of consecutive ranging misses before emitting a distance-based exit event.
16
- /// With ~1 s CoreLocation ranging cycles, 10 misses 10 s of silence before
17
- /// declaring exit — tolerant of brief ranging gaps while still responsive.
18
- /// NOTE: Android uses the same value but with a different scan cycle (~2.1 s).
19
- private let EXIT_MISS_THRESHOLD = 10
20
+ /// With ~1 s CoreLocation ranging cycles (iBeacon) or ~2 s Eddystone monitoring ticks,
21
+ /// 20 misses ≈ 20–40 s of silence before declaring exit — tolerant of iOS background
22
+ /// BLE throttling while still responsive to actual departures.
23
+ /// NOTE: Android uses 10 with a ~2.1 s scan cycle (≈21 s effective).
24
+ private let EXIT_MISS_THRESHOLD = 20
20
25
  /// Number of consecutive readings required to confirm a distance-based enter or exit transition.
21
26
  /// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
22
27
  private let HYSTERESIS_COUNT = 3
@@ -24,8 +29,9 @@ private let HYSTERESIS_COUNT = 3
24
29
  /// Eddystone monitoring timer interval in seconds.
25
30
  private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
26
31
  /// Maximum age (in seconds) before a beacon is considered "not recently seen".
27
- /// Set higher than a few tick intervals to tolerate brief BLE advertisement gaps.
28
- private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 8.0
32
+ /// Set high enough to tolerate iOS background CoreBluetooth throttling which
33
+ /// can cause 10-12 s gaps between Eddystone advertisements.
34
+ private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 15.0
29
35
  /// Minimum interval between consecutive distance event emissions per identifier.
30
36
  private let DISTANCE_EVENT_THROTTLE_INTERVAL: TimeInterval = 1.0
31
37
 
@@ -89,6 +95,16 @@ public class ExpoBeaconModule: Module {
89
95
  private var eddystoneExitCounters: [String: Int] = [:]
90
96
  private var eddystoneLastDistanceEmit: [String: Date] = [:]
91
97
 
98
+ /// Minimum RSSI threshold — readings below this are treated as unreliable.
99
+ private var minRssiThreshold: Int = DEFAULT_MIN_RSSI
100
+
101
+ /// Distance smoothing (EMA) state per identifier.
102
+ private var smoothedDistances: [String: Double] = [:]
103
+ /// EMA weight for new readings. 0.4 balances responsiveness vs noise rejection.
104
+ private static let DISTANCE_EMA_ALPHA = 0.4
105
+ /// If raw distance differs from smoothed by more than this factor, treat as outlier.
106
+ private static let DISTANCE_JUMP_FACTOR = 5.0
107
+
92
108
  // Permission callback
93
109
  private var permissionCompletion: ((Bool) -> Void)?
94
110
 
@@ -100,6 +116,9 @@ public class ExpoBeaconModule: Module {
100
116
  private var eventLogger: BeaconEventLogger?
101
117
  private var loggingEnabled = false
102
118
 
119
+ // Native API forwarder (fire-and-forget HTTP)
120
+ private lazy var apiForwarder = BeaconApiForwarder(defaults: defaults)
121
+
103
122
  // Timeout timers — fire once after beacon stays in range for configured duration
104
123
  private var beaconTimeoutTimers: [String: DispatchWorkItem] = [:]
105
124
  private var eddystoneTimeoutTimers: [String: DispatchWorkItem] = [:]
@@ -305,11 +324,13 @@ public class ExpoBeaconModule: Module {
305
324
  AsyncFunction("startMonitoring") { (options: Either<Double, [String: Any]>?, promise: Promise) in
306
325
  var maxDistance: Double? = nil
307
326
  var exitDistance: Double? = nil
327
+ var minRssi: Int? = nil
308
328
  if let dist: Double = options?.get() {
309
329
  maxDistance = dist
310
330
  } else if let map: [String: Any] = options?.get() {
311
331
  maxDistance = map["maxDistance"] as? Double
312
332
  exitDistance = map["exitDistance"] as? Double
333
+ minRssi = map["minRssi"] as? Int
313
334
  if let notifications = map["notifications"] as? [String: Any],
314
335
  let data = try? JSONSerialization.data(withJSONObject: notifications),
315
336
  let json = String(data: data, encoding: .utf8) {
@@ -342,6 +363,13 @@ public class ExpoBeaconModule: Module {
342
363
  } else {
343
364
  self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
344
365
  }
366
+ if let rssi = minRssi {
367
+ self.defaults.set(rssi, forKey: MIN_RSSI_KEY)
368
+ self.minRssiThreshold = rssi
369
+ } else {
370
+ self.defaults.removeObject(forKey: MIN_RSSI_KEY)
371
+ self.minRssiThreshold = DEFAULT_MIN_RSSI
372
+ }
345
373
  self.defaults.set(true, forKey: IS_MONITORING_KEY)
346
374
  self.requestLocationPermission { granted in
347
375
  guard granted else {
@@ -454,6 +482,12 @@ public class ExpoBeaconModule: Module {
454
482
  self.eventLogger = nil
455
483
  }
456
484
 
485
+ // MARK: - API Forwarding
486
+
487
+ Function("setApiEndpoint") { (url: String, apiKey: String?) -> Void in
488
+ self.apiForwarder.configure(url: url, apiKey: apiKey)
489
+ }
490
+
457
491
  // MARK: - Battery Optimization (Android-only; no-op on iOS)
458
492
 
459
493
  Function("isBatteryOptimizationExempt") { () -> Bool in
@@ -537,6 +571,10 @@ public class ExpoBeaconModule: Module {
537
571
  private func startRegionMonitoring() {
538
572
  stopRegionMonitoring()
539
573
 
574
+ // Restore persisted min RSSI threshold (survives app restarts)
575
+ let storedRssi = defaults.object(forKey: MIN_RSSI_KEY) as? Int
576
+ minRssiThreshold = storedRssi ?? DEFAULT_MIN_RSSI
577
+
540
578
  let beacons = loadPairedBeaconsRaw()
541
579
 
542
580
  // CLLocationManager supports a maximum of 20 monitored regions.
@@ -599,6 +637,7 @@ public class ExpoBeaconModule: Module {
599
637
  missCounters.removeAll()
600
638
  enterCounters.removeAll()
601
639
  exitCounters.removeAll()
640
+ smoothedDistances.removeAll()
602
641
 
603
642
  for timer in beaconTimeoutTimers.values { timer.cancel() }
604
643
  beaconTimeoutTimers.removeAll()
@@ -738,6 +777,10 @@ public class ExpoBeaconModule: Module {
738
777
 
739
778
  guard let beacon = Self.parseEddystoneFrame(data: data, rssi: rssi.intValue) else { return }
740
779
 
780
+ // Discard weak signals that produce unreliable distance estimates
781
+ let beaconRssi = rssi.intValue
782
+ guard beaconRssi >= minRssiThreshold else { return }
783
+
741
784
  // Augment with the BLE advertising device name if present
742
785
  var beaconInfo = beacon
743
786
  if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
@@ -775,9 +818,19 @@ public class ExpoBeaconModule: Module {
775
818
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
776
819
  let hasValidDistance = distance.isFinite && distance >= 0
777
820
  if hasValidDistance || maxDist == nil {
821
+ // Apply EMA smoothing; jump guard returns nil for outliers
822
+ 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
828
+ } else {
829
+ effectiveDistance = distance
830
+ }
778
831
  let action = evaluateDistanceHysteresis(
779
832
  identifier: identifier,
780
- distance: distance,
833
+ distance: effectiveDistance,
781
834
  maxDistance: maxDist,
782
835
  exitDistance: exitDist,
783
836
  entered: &eddystoneEnteredRegions,
@@ -791,7 +844,8 @@ public class ExpoBeaconModule: Module {
791
844
  "namespace": ns,
792
845
  "instance": inst,
793
846
  "event": "enter",
794
- "distance": distance
847
+ "distance": distance,
848
+ "rssi": beaconRssi
795
849
  ])
796
850
  postBeaconNotification(identifier: identifier, eventType: "enter")
797
851
  scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
@@ -802,7 +856,8 @@ public class ExpoBeaconModule: Module {
802
856
  "namespace": ns,
803
857
  "instance": inst,
804
858
  "event": "exit",
805
- "distance": distance
859
+ "distance": distance,
860
+ "rssi": beaconRssi
806
861
  ])
807
862
  postBeaconNotification(identifier: identifier, eventType: "exit")
808
863
  case .none:
@@ -825,7 +880,8 @@ public class ExpoBeaconModule: Module {
825
880
  "identifier": identifier,
826
881
  "namespace": ns,
827
882
  "instance": inst,
828
- "distance": distance
883
+ "distance": distance,
884
+ "rssi": beaconRssi
829
885
  ])
830
886
  break
831
887
  }
@@ -901,6 +957,10 @@ public class ExpoBeaconModule: Module {
901
957
  let identifier = params["identifier"] as? String
902
958
  getOrCreateEventLogger().logEvent(eventType: eventName, identifier: identifier, data: params)
903
959
  }
960
+ // Forward enter/exit/timeout events to remote API (skip distance — too frequent)
961
+ if !eventName.lowercased().contains("distance") {
962
+ apiForwarder.forwardEvent(params)
963
+ }
904
964
  sendEvent(eventName, params)
905
965
  }
906
966
 
@@ -980,6 +1040,8 @@ public class ExpoBeaconModule: Module {
980
1040
  eddystoneEnterCounters.removeAll()
981
1041
  eddystoneExitCounters.removeAll()
982
1042
  eddystoneLastDistanceEmit.removeAll()
1043
+ // Eddystone smoothed distances are in the shared smoothedDistances map;
1044
+ // they are cleaned up when stopRegionMonitoring clears the entire map.
983
1045
 
984
1046
  for timer in eddystoneTimeoutTimers.values { timer.cancel() }
985
1047
  eddystoneTimeoutTimers.removeAll()
@@ -1001,9 +1063,9 @@ public class ExpoBeaconModule: Module {
1001
1063
  continue
1002
1064
  }
1003
1065
 
1004
- // Not seen recently — break distance hysteresis streaks and, if the
1005
- // beacon was already entered, increment miss counter toward exit.
1006
- eddystoneEnterCounters[identifier] = 0
1066
+ // Not seen recently — reset exit counter (miss counter handles exit
1067
+ // separately) but preserve enter counter so that background BLE
1068
+ // throttling gaps don't force re-accumulating HYSTERESIS_COUNT reads.
1007
1069
  eddystoneExitCounters[identifier] = 0
1008
1070
  guard eddystoneEnteredRegions.contains(identifier) else { continue }
1009
1071
 
@@ -1130,7 +1192,24 @@ public class ExpoBeaconModule: Module {
1130
1192
  return dict
1131
1193
  }
1132
1194
 
1133
- // MARK: - Distance-based enter/exit hysteresis
1195
+ // MARK: - Distance smoothing + enter/exit hysteresis
1196
+
1197
+ /// Apply exponential moving average (EMA) smoothing to a raw distance reading.
1198
+ /// Returns nil if the reading is a jump outlier (raw differs from smoothed by > DISTANCE_JUMP_FACTOR).
1199
+ private func smoothDistance(identifier: String, rawDistance: Double) -> Double? {
1200
+ guard let prev = smoothedDistances[identifier] else {
1201
+ smoothedDistances[identifier] = rawDistance
1202
+ return rawDistance
1203
+ }
1204
+ // Jump guard: if the raw value is wildly different, treat as outlier
1205
+ let ratio = prev > 0.001 ? rawDistance / prev : rawDistance
1206
+ if ratio > Self.DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / Self.DISTANCE_JUMP_FACTOR) {
1207
+ return nil
1208
+ }
1209
+ let smoothed = Self.DISTANCE_EMA_ALPHA * rawDistance + (1 - Self.DISTANCE_EMA_ALPHA) * prev
1210
+ smoothedDistances[identifier] = smoothed
1211
+ return smoothed
1212
+ }
1134
1213
 
1135
1214
  private enum HysteresisAction {
1136
1215
  case none, enter, exit
@@ -1204,7 +1283,8 @@ public class ExpoBeaconModule: Module {
1204
1283
  "uuid": (beacon?.uuid ?? region?.uuid)?.uuidString.uppercased() ?? "",
1205
1284
  "major": beacon?.major.intValue ?? region?.major?.intValue ?? 0,
1206
1285
  "minor": beacon?.minor.intValue ?? region?.minor?.intValue ?? 0,
1207
- "distance": beacon != nil ? beacon!.accuracy : distance
1286
+ "distance": beacon != nil ? beacon!.accuracy : distance,
1287
+ "rssi": beacon?.rssi ?? 0
1208
1288
  ]
1209
1289
  if let event = event {
1210
1290
  params["event"] = event
@@ -1237,7 +1317,7 @@ public class ExpoBeaconModule: Module {
1237
1317
 
1238
1318
  // 2. Distance-ranging for monitored beacons
1239
1319
  if let (identifier, _) = distanceRangingConstraints.first(where: { $0.value == constraint }) {
1240
- let validBeacon = beacons.first(where: { $0.accuracy >= 0 })
1320
+ let validBeacon = beacons.first(where: { $0.accuracy >= 0 && $0.rssi >= minRssiThreshold })
1241
1321
 
1242
1322
  if let beacon = validBeacon {
1243
1323
  // Got a valid reading — reset miss counter
@@ -1252,9 +1332,16 @@ public class ExpoBeaconModule: Module {
1252
1332
  // (HYSTERESIS_COUNT consecutive readings to confirm enter).
1253
1333
  let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
1254
1334
  let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
1335
+
1336
+ // Apply EMA smoothing; jump guard returns nil for outliers
1337
+ guard let smoothed = smoothDistance(identifier: identifier, rawDistance: beacon.accuracy) else {
1338
+ // Jump outlier — skip this cycle without resetting counters
1339
+ return
1340
+ }
1341
+
1255
1342
  let action = evaluateDistanceHysteresis(
1256
1343
  identifier: identifier,
1257
- distance: beacon.accuracy,
1344
+ distance: smoothed,
1258
1345
  maxDistance: maxDist,
1259
1346
  exitDistance: exitDist,
1260
1347
  entered: &enteredRegions,
@@ -1278,8 +1365,9 @@ public class ExpoBeaconModule: Module {
1278
1365
  // UUID-only constraints in check 3 below, not here, to avoid
1279
1366
  // duplicate events when both monitoring and continuous scan are active.
1280
1367
  } else {
1281
- // No valid beacon reading — beacon may have disappeared
1282
- enterCounters[identifier] = 0
1368
+ // No valid beacon reading — beacon may have disappeared.
1369
+ // Preserve enter counter so background accuracy=-1 gaps don't
1370
+ // force re-accumulating HYSTERESIS_COUNT reads from scratch.
1283
1371
  exitCounters[identifier] = 0
1284
1372
  let count = (missCounters[identifier] ?? 0) + 1
1285
1373
  missCounters[identifier] = count
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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",
@@ -40,6 +40,8 @@ export type BeaconRegionEvent = {
40
40
  event: "enter" | "exit";
41
41
  /** Measured distance in metres at the time of the event (–1 if unavailable). */
42
42
  distance: number;
43
+ /** Signal strength in dBm at the time of the event (0 if unavailable). */
44
+ rssi?: number;
43
45
  };
44
46
 
45
47
  /** Payload for periodic distance update events during monitoring. */
@@ -49,6 +51,8 @@ export type BeaconDistanceEvent = {
49
51
  major: number;
50
52
  minor: number;
51
53
  distance: number;
54
+ /** Signal strength in dBm (0 if unavailable). */
55
+ rssi?: number;
52
56
  };
53
57
 
54
58
  /** Payload for beacon timeout events (beacon in range for configured duration). */
@@ -129,6 +133,14 @@ export type MonitoringOptions = {
129
133
  * Only used when `maxDistance` is set.
130
134
  */
131
135
  exitDistance?: number;
136
+ /**
137
+ * Minimum RSSI (dBm) for a beacon reading to be considered valid.
138
+ * Readings below this threshold are discarded as unreliable, preventing
139
+ * false detections from reflected or distant signals.
140
+ *
141
+ * Default: -85. Typical range: -100 (very permissive) to -70 (strict).
142
+ */
143
+ minRssi?: number;
132
144
  /** Notification configuration overrides to apply for this monitoring session. */
133
145
  notifications?: NotificationConfig;
134
146
  };
@@ -182,6 +194,8 @@ export type EddystoneRegionEvent = {
182
194
  event: "enter" | "exit";
183
195
  /** Measured distance in metres at the time of the event (–1 if unavailable). */
184
196
  distance: number;
197
+ /** Signal strength in dBm at the time of the event (0 if unavailable). */
198
+ rssi?: number;
185
199
  };
186
200
 
187
201
  /** Payload for periodic Eddystone distance update events during monitoring. */
@@ -190,6 +204,8 @@ export type EddystoneDistanceEvent = {
190
204
  namespace: string;
191
205
  instance: string;
192
206
  distance: number;
207
+ /** Signal strength in dBm (0 if unavailable). */
208
+ rssi?: number;
193
209
  };
194
210
 
195
211
  /** Payload for Eddystone timeout events (beacon in range for configured duration). */
@@ -153,6 +153,16 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
153
153
 
154
154
  /** Delete the entire event log database. Also disables logging. */
155
155
  destroyEventLogs(): void;
156
+
157
+ /**
158
+ * Configure a remote API endpoint for native event forwarding.
159
+ * Once set, enter/exit/timeout events are POSTed directly from native code,
160
+ * ensuring delivery even when the JS bridge is not active (app backgrounded).
161
+ *
162
+ * @param url The API endpoint URL to POST events to.
163
+ * @param apiKey Optional API key sent as X-API-Key header.
164
+ */
165
+ setApiEndpoint(url: string, apiKey?: string): void;
156
166
  }
157
167
 
158
168
  export default requireNativeModule<ExpoBeaconModule>("ExpoBeacon");