expo-beacon 0.6.12 → 0.6.13

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.
@@ -288,9 +288,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
288
288
  missCounters.remove(region.uniqueId)
289
289
  }
290
290
  if (wasEntered) {
291
- cancelTimeout(region.uniqueId)
292
291
  sendBeaconBroadcast(region, "exit", -1.0)
293
292
  showEnterExitNotification(region, "exit")
293
+ // OS-level exit safety net — start the timeout clock.
294
+ scheduleTimeoutIfConfigured(region)
294
295
  }
295
296
  }
296
297
 
@@ -329,13 +330,15 @@ class BeaconForegroundService : Service(), BeaconConsumer {
329
330
  enteredRegions.add(region.uniqueId)
330
331
  sendBeaconBroadcast(region, "enter", beacon.distance, beacon.rssi)
331
332
  showEnterExitNotification(region, "enter")
332
- scheduleTimeoutIfConfigured(region)
333
+ // Beacon returned — cancel any running timeout timer.
334
+ cancelTimeout(region.uniqueId)
333
335
  }
334
336
  HysteresisAction.EXIT -> {
335
- cancelTimeout(region.uniqueId)
336
337
  enteredRegions.remove(region.uniqueId)
337
338
  sendBeaconBroadcast(region, "exit", beacon.distance, beacon.rssi)
338
339
  showEnterExitNotification(region, "exit")
340
+ // Beacon left — start the timeout clock.
341
+ scheduleTimeoutIfConfigured(region)
339
342
  }
340
343
  HysteresisAction.NONE -> {}
341
344
  }
@@ -348,18 +351,14 @@ class BeaconForegroundService : Service(), BeaconConsumer {
348
351
  missCounters[region.uniqueId] = count
349
352
 
350
353
  if (enteredRegions.contains(region.uniqueId) && count >= EXIT_MISS_THRESHOLD) {
351
- // Do NOT cancel the timeout here. A miss-based exit is triggered by BLE
352
- // scan gaps (unreliable signal disappearance), not a confirmed physical
353
- // departure. Cancelling the timeout here would prevent it from ever firing
354
- // when the configured timeout (e.g. 25 s) exceeds the miss window (~21 s).
355
- // The timeout runnable fires unconditionally; distance-based exits still
356
- // call cancelTimeout() reliably when the beacon moves out of range.
357
354
  enteredRegions.remove(region.uniqueId)
358
355
  missCounters[region.uniqueId] = 0
359
356
  enterCounters[region.uniqueId] = 0
360
357
  exitCounters[region.uniqueId] = 0
361
358
  sendBeaconBroadcast(region, "exit", -1.0)
362
359
  showEnterExitNotification(region, "exit")
360
+ // Beacon disappeared — start the timeout clock.
361
+ scheduleTimeoutIfConfigured(region)
363
362
  }
364
363
  }
365
364
  }
@@ -459,16 +458,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
459
458
 
460
459
  private fun scheduleTimeoutIfConfigured(region: Region) {
461
460
  val seconds = beaconTimeouts[region.uniqueId] ?: return
462
- // If a timer is already running, don't reset it. Miss-based exits clear
463
- // enteredRegions without cancelling the timer, so a subsequent re-entry
464
- // must not restart the clock — doing so would defer the timeout indefinitely.
465
- if (timeoutRunnables.containsKey(region.uniqueId)) return
461
+ // Cancel any existing timer so each exit resets the clock.
462
+ cancelTimeout(region.uniqueId)
466
463
  val runnable = Runnable {
467
464
  timeoutRunnables.remove(region.uniqueId)
468
- // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
469
- // the timer elapsed (BLE gaps can cause false exits at ~21 s), but the beacon
470
- // may still be physically present. Distance-based exits call cancelTimeout() so
471
- // this runnable is never queued when the beacon has genuinely moved away.
472
465
  sendBeaconBroadcast(region, "timeout", -1.0)
473
466
  }
474
467
  timeoutRunnables[region.uniqueId] = runnable
@@ -848,10 +848,10 @@ public class ExpoBeaconModule: Module {
848
848
  "rssi": beaconRssi
849
849
  ])
850
850
  postBeaconNotification(identifier: identifier, eventType: "enter")
851
- scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
851
+ // Beacon returned cancel any running timeout timer.
852
+ cancelEddystoneTimeout(identifier: identifier)
852
853
  case .exit:
853
854
  smoothedDistances.removeValue(forKey: identifier)
854
- cancelEddystoneTimeout(identifier: identifier)
855
855
  sendLoggedEvent("onEddystoneExit", [
856
856
  "identifier": identifier,
857
857
  "namespace": ns,
@@ -861,6 +861,8 @@ public class ExpoBeaconModule: Module {
861
861
  "rssi": beaconRssi
862
862
  ])
863
863
  postBeaconNotification(identifier: identifier, eventType: "exit")
864
+ // Beacon left — start the timeout clock.
865
+ scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
864
866
  case .none:
865
867
  break
866
868
  }
@@ -1080,7 +1082,6 @@ public class ExpoBeaconModule: Module {
1080
1082
  eddystoneExitCounters[identifier] = 0
1081
1083
  eddystoneLatestSeen.removeValue(forKey: identifier)
1082
1084
  smoothedDistances.removeValue(forKey: identifier)
1083
- // Do NOT cancel the timeout here — same reason as iBeacon miss-based exit.
1084
1085
 
1085
1086
  let ns = paired["namespace"] as? String ?? ""
1086
1087
  let inst = paired["instance"] as? String ?? ""
@@ -1093,17 +1094,17 @@ public class ExpoBeaconModule: Module {
1093
1094
  ]
1094
1095
  sendLoggedEvent("onEddystoneExit", params)
1095
1096
  postBeaconNotification(identifier: identifier, eventType: "exit")
1097
+ // Beacon disappeared — start the timeout clock.
1098
+ scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
1096
1099
  }
1097
1100
  }
1098
1101
  }
1099
1102
 
1100
1103
  // MARK: - Timeout timer helpers
1101
1104
 
1102
- private func scheduleBeaconTimeout(identifier: String, beacon: CLBeacon) {
1103
- // If a timer is already running, don't reset it. Miss-based exits clear
1104
- // enteredRegions without cancelling the timer, so a subsequent re-entry
1105
- // must not restart the clock — doing so would defer the timeout indefinitely.
1106
- guard beaconTimeoutTimers[identifier] == nil else { return }
1105
+ private func scheduleBeaconTimeout(identifier: String, beacon: CLBeacon? = nil, region: CLBeaconRegion? = nil) {
1106
+ // Cancel any existing timer so each exit resets the clock.
1107
+ cancelBeaconTimeout(identifier: identifier)
1107
1108
 
1108
1109
  let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
1109
1110
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1111,11 +1112,7 @@ public class ExpoBeaconModule: Module {
1111
1112
  let work = DispatchWorkItem { [weak self] in
1112
1113
  guard let self = self else { return }
1113
1114
  self.beaconTimeoutTimers.removeValue(forKey: identifier)
1114
- // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
1115
- // the timer elapsed (ranging gaps can cause false exits), but the beacon may still
1116
- // be physically present. Distance-based exits call cancelBeaconTimeout() so this
1117
- // work item is cancelled before it runs on genuine out-of-range departures.
1118
- self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon))
1115
+ self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon, region: region))
1119
1116
  }
1120
1117
  beaconTimeoutTimers[identifier] = work
1121
1118
  DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds), execute: work)
@@ -1126,8 +1123,8 @@ public class ExpoBeaconModule: Module {
1126
1123
  }
1127
1124
 
1128
1125
  private func scheduleEddystoneTimeout(identifier: String, namespace: String, instance: String) {
1129
- // If a timer is already running, don't reset it — same reason as iBeacon timeout.
1130
- guard eddystoneTimeoutTimers[identifier] == nil else { return }
1126
+ // Cancel any existing timer so each exit resets the clock.
1127
+ cancelEddystoneTimeout(identifier: identifier)
1131
1128
 
1132
1129
  let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
1133
1130
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1135,7 +1132,6 @@ public class ExpoBeaconModule: Module {
1135
1132
  let work = DispatchWorkItem { [weak self] in
1136
1133
  guard let self = self else { return }
1137
1134
  self.eddystoneTimeoutTimers.removeValue(forKey: identifier)
1138
- // Fire unconditionally — same reason as iBeacon timeout.
1139
1135
  self.sendLoggedEvent("onEddystoneTimeout", [
1140
1136
  "identifier": identifier,
1141
1137
  "namespace": namespace,
@@ -1362,12 +1358,14 @@ public class ExpoBeaconModule: Module {
1362
1358
  case .enter:
1363
1359
  sendLoggedEvent("onBeaconEnter", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "enter"))
1364
1360
  postBeaconNotification(identifier: identifier, eventType: "enter")
1365
- scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1361
+ // Beacon returned — cancel any running timeout timer.
1362
+ cancelBeaconTimeout(identifier: identifier)
1366
1363
  case .exit:
1367
1364
  smoothedDistances.removeValue(forKey: identifier)
1368
- cancelBeaconTimeout(identifier: identifier)
1369
1365
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
1370
1366
  postBeaconNotification(identifier: identifier, eventType: "exit")
1367
+ // Beacon left — start the timeout clock.
1368
+ scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1371
1369
  case .none:
1372
1370
  break
1373
1371
  }
@@ -1389,16 +1387,13 @@ public class ExpoBeaconModule: Module {
1389
1387
  enterCounters[identifier] = 0
1390
1388
  exitCounters[identifier] = 0
1391
1389
  smoothedDistances.removeValue(forKey: identifier)
1392
- // Do NOT cancel the timeout here. A miss-based exit is triggered by
1393
- // ranging gaps (e.g. accuracy == -1), not a confirmed physical departure.
1394
- // Cancelling here prevents the timeout from firing when it is longer than
1395
- // the miss window. Distance-based exits call cancelBeaconTimeout() so the
1396
- // timer is still cancelled on genuine out-of-range events.
1397
1390
 
1398
1391
  // Look up region info for the exit event payload
1399
1392
  let region = monitoredRegions.first { $0.identifier == identifier }
1400
1393
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: region, event: "exit"))
1401
1394
  postBeaconNotification(identifier: identifier, eventType: "exit")
1395
+ // Beacon disappeared — start the timeout clock.
1396
+ scheduleBeaconTimeout(identifier: identifier, region: region)
1402
1397
  }
1403
1398
  }
1404
1399
  return
@@ -1441,9 +1436,10 @@ public class ExpoBeaconModule: Module {
1441
1436
  missCounters.removeValue(forKey: identifier)
1442
1437
  smoothedDistances.removeValue(forKey: identifier)
1443
1438
  if wasEntered {
1444
- cancelBeaconTimeout(identifier: identifier)
1445
1439
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
1446
1440
  postBeaconNotification(identifier: identifier, eventType: "exit")
1441
+ // OS-level exit safety net — start the timeout clock.
1442
+ scheduleBeaconTimeout(identifier: identifier, region: beaconRegion)
1447
1443
  }
1448
1444
  }
1449
1445
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
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",