expo-beacon 0.6.13 → 0.6.15
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/android/src/main/java/expo/modules/beacon/BeaconConstants.kt +6 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +49 -16
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +3 -0
- package/build/ExpoBeacon.types.d.ts +13 -0
- package/build/ExpoBeacon.types.d.ts.map +1 -1
- package/build/ExpoBeacon.types.js.map +1 -1
- package/build/ExpoBeaconModule.d.ts +1 -1
- package/build/ExpoBeaconModule.js.map +1 -1
- package/ios/ExpoBeaconModule.swift +98 -22
- package/package.json +1 -1
- package/src/ExpoBeacon.types.ts +13 -0
- package/src/ExpoBeaconModule.ts +1 -1
|
@@ -37,6 +37,12 @@ internal const val RECENT_RANGING_SIGHTING_GRACE_MS = 25000L
|
|
|
37
37
|
*/
|
|
38
38
|
internal const val EXIT_MISS_THRESHOLD = 10
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Milliseconds of no valid BLE readings before starting the timeout countdown.
|
|
42
|
+
* Acts as a safety net when ranging cycles stop entirely (e.g. Doze mode).
|
|
43
|
+
*/
|
|
44
|
+
internal const val DISTANCE_INACTIVITY_MS = 60_000L
|
|
45
|
+
|
|
40
46
|
/** Number of consecutive readings required to confirm a distance-based enter or exit transition. */
|
|
41
47
|
internal const val HYSTERESIS_COUNT = 3
|
|
42
48
|
|
|
@@ -64,8 +64,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
64
64
|
private val timeoutRunnables = java.util.concurrent.ConcurrentHashMap<String, Runnable>()
|
|
65
65
|
// Per-beacon timeout seconds lookup (identifier → seconds), loaded from paired data
|
|
66
66
|
private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
|
67
|
+
// Inactivity timers — start timeout countdown when no BLE readings for 60 s
|
|
68
|
+
private val inactivityRunnables = java.util.concurrent.ConcurrentHashMap<String, Runnable>()
|
|
67
69
|
private var eventLogger: BeaconEventLogger? = null
|
|
68
70
|
private var apiForwarder: BeaconApiForwarder? = null
|
|
71
|
+
// Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
|
|
72
|
+
@Volatile private var eventLevel: String = "all"
|
|
69
73
|
|
|
70
74
|
override fun onCreate() {
|
|
71
75
|
super.onCreate()
|
|
@@ -125,6 +129,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
125
129
|
maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
|
|
126
130
|
exitDistance = optPrefs.getString("exit_distance", null)?.toDoubleOrNull()
|
|
127
131
|
minRssiThreshold = optPrefs.getInt("min_rssi", DEFAULT_MIN_RSSI)
|
|
132
|
+
eventLevel = optPrefs.getString("level", "all") ?: "all"
|
|
128
133
|
|
|
129
134
|
beaconManager.addMonitorNotifier(monitorNotifier)
|
|
130
135
|
beaconManager.addRangeNotifier(rangeNotifier)
|
|
@@ -174,6 +179,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
174
179
|
lastSeenAtMs.clear()
|
|
175
180
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
176
181
|
timeoutRunnables.clear()
|
|
182
|
+
inactivityRunnables.clear()
|
|
177
183
|
synchronized(distanceLock) {
|
|
178
184
|
enterCounters.clear()
|
|
179
185
|
exitCounters.clear()
|
|
@@ -256,10 +262,13 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
256
262
|
|
|
257
263
|
// Distance logging only — emits distance broadcasts. Enter/exit logic lives in rangeNotifier.
|
|
258
264
|
private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
|
|
265
|
+
if (eventLevel != "all") return@RangeNotifier
|
|
259
266
|
if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
|
|
260
267
|
val closest = beacons.filter { it.distance >= 0 && it.rssi >= minRssiThreshold }.minByOrNull { it.distance }
|
|
261
268
|
if (closest != null) {
|
|
262
269
|
lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
|
|
270
|
+
// Valid BLE reading — reset inactivity timer.
|
|
271
|
+
rescheduleInactivity(region)
|
|
263
272
|
sendBeaconBroadcast(region, "distance", closest.distance, closest.rssi)
|
|
264
273
|
}
|
|
265
274
|
}
|
|
@@ -290,7 +299,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
290
299
|
if (wasEntered) {
|
|
291
300
|
sendBeaconBroadcast(region, "exit", -1.0)
|
|
292
301
|
showEnterExitNotification(region, "exit")
|
|
293
|
-
// OS-level exit safety net — start the timeout clock.
|
|
302
|
+
// OS-level exit safety net — cancel inactivity timer and start the timeout clock.
|
|
303
|
+
cancelInactivity(region.uniqueId)
|
|
294
304
|
scheduleTimeoutIfConfigured(region)
|
|
295
305
|
}
|
|
296
306
|
}
|
|
@@ -316,13 +326,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
316
326
|
// Got a valid reading — reset miss counter
|
|
317
327
|
lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
|
|
318
328
|
missCounters[region.uniqueId] = 0
|
|
329
|
+
// Valid BLE reading — reset inactivity timer.
|
|
330
|
+
rescheduleInactivity(region)
|
|
319
331
|
|
|
320
|
-
// Apply EMA smoothing; jump
|
|
332
|
+
// Apply EMA smoothing; jump resets EMA to the new value
|
|
321
333
|
val smoothed = smoothDistance(region.uniqueId, beacon.distance)
|
|
322
|
-
if (smoothed == null) {
|
|
323
|
-
// Outlier — treat as miss without resetting enter counter
|
|
324
|
-
return@RangeNotifier
|
|
325
|
-
}
|
|
326
334
|
|
|
327
335
|
val action = evaluateDistanceHysteresis(region.uniqueId, smoothed, maxDist)
|
|
328
336
|
when (action) {
|
|
@@ -337,7 +345,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
337
345
|
enteredRegions.remove(region.uniqueId)
|
|
338
346
|
sendBeaconBroadcast(region, "exit", beacon.distance, beacon.rssi)
|
|
339
347
|
showEnterExitNotification(region, "exit")
|
|
340
|
-
// Beacon left — start the timeout clock.
|
|
348
|
+
// Beacon left — cancel inactivity timer and start the timeout clock.
|
|
349
|
+
cancelInactivity(region.uniqueId)
|
|
341
350
|
scheduleTimeoutIfConfigured(region)
|
|
342
351
|
}
|
|
343
352
|
HysteresisAction.NONE -> {}
|
|
@@ -357,7 +366,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
357
366
|
exitCounters[region.uniqueId] = 0
|
|
358
367
|
sendBeaconBroadcast(region, "exit", -1.0)
|
|
359
368
|
showEnterExitNotification(region, "exit")
|
|
360
|
-
// Beacon disappeared — start the timeout clock.
|
|
369
|
+
// Beacon disappeared — cancel inactivity timer and start the timeout clock.
|
|
370
|
+
cancelInactivity(region.uniqueId)
|
|
361
371
|
scheduleTimeoutIfConfigured(region)
|
|
362
372
|
}
|
|
363
373
|
}
|
|
@@ -370,18 +380,23 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
370
380
|
|
|
371
381
|
/**
|
|
372
382
|
* Apply exponential moving average (EMA) smoothing to a raw distance reading.
|
|
373
|
-
*
|
|
383
|
+
* If the reading is a large jump (> DISTANCE_JUMP_FACTOR), resets the EMA to the new
|
|
384
|
+
* value instead of rejecting it — this ensures the hysteresis pipeline keeps receiving
|
|
385
|
+
* data and can fire exit events when the user moves away from a beacon, rather than
|
|
386
|
+
* freezing because the EMA is stuck at the old close-range value.
|
|
374
387
|
*/
|
|
375
|
-
private fun smoothDistance(regionId: String, rawDistance: Double): Double
|
|
388
|
+
private fun smoothDistance(regionId: String, rawDistance: Double): Double {
|
|
376
389
|
val prev = smoothedDistances[regionId]
|
|
377
390
|
if (prev == null) {
|
|
378
391
|
smoothedDistances[regionId] = rawDistance
|
|
379
392
|
return rawDistance
|
|
380
393
|
}
|
|
381
|
-
// Jump guard: if the raw value is wildly different,
|
|
394
|
+
// Jump guard: if the raw value is wildly different, reset EMA to the new reading
|
|
395
|
+
// so the hysteresis pipeline keeps receiving data and can fire the exit event.
|
|
382
396
|
val ratio = if (prev > 0.001) rawDistance / prev else rawDistance
|
|
383
397
|
if (ratio > DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / DISTANCE_JUMP_FACTOR)) {
|
|
384
|
-
|
|
398
|
+
smoothedDistances[regionId] = rawDistance
|
|
399
|
+
return rawDistance
|
|
385
400
|
}
|
|
386
401
|
val smoothed = DISTANCE_EMA_ALPHA * rawDistance + (1 - DISTANCE_EMA_ALPHA) * prev
|
|
387
402
|
smoothedDistances[regionId] = smoothed
|
|
@@ -472,6 +487,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
472
487
|
timeoutRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
|
|
473
488
|
}
|
|
474
489
|
|
|
490
|
+
// MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
|
|
491
|
+
|
|
492
|
+
private fun rescheduleInactivity(region: Region) {
|
|
493
|
+
val regionId = region.uniqueId
|
|
494
|
+
if (!beaconTimeouts.containsKey(regionId)) return
|
|
495
|
+
cancelInactivity(regionId)
|
|
496
|
+
val runnable = Runnable {
|
|
497
|
+
inactivityRunnables.remove(regionId)
|
|
498
|
+
// No BLE readings for 60 s — start the configured timeout countdown.
|
|
499
|
+
scheduleTimeoutIfConfigured(region)
|
|
500
|
+
}
|
|
501
|
+
inactivityRunnables[regionId] = runnable
|
|
502
|
+
timeoutHandler.postDelayed(runnable, DISTANCE_INACTIVITY_MS)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private fun cancelInactivity(regionId: String) {
|
|
506
|
+
inactivityRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
|
|
507
|
+
}
|
|
508
|
+
|
|
475
509
|
private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double, rssi: Int = 0) {
|
|
476
510
|
// Determine if this is an Eddystone region based on identifier format
|
|
477
511
|
// Eddystone regions have id1 as a hex namespace (not a UUID)
|
|
@@ -500,10 +534,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
500
534
|
}
|
|
501
535
|
monitoringEventName(isEddystone, eventType)?.let { logBeaconEvent(it, params) }
|
|
502
536
|
|
|
503
|
-
// Forward
|
|
504
|
-
|
|
505
|
-
apiForwarder?.forwardEvent(params)
|
|
506
|
-
}
|
|
537
|
+
// Forward all produced events to remote API
|
|
538
|
+
apiForwarder?.forwardEvent(params)
|
|
507
539
|
|
|
508
540
|
val intent = Intent(ACTION_BEACON_EVENT).apply {
|
|
509
541
|
putExtra("identifier", region.uniqueId)
|
|
@@ -733,6 +765,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
733
765
|
serviceConnected = false
|
|
734
766
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
735
767
|
timeoutRunnables.clear()
|
|
768
|
+
inactivityRunnables.clear()
|
|
736
769
|
beaconTimeouts.clear()
|
|
737
770
|
lastSeenAtMs.clear()
|
|
738
771
|
monitoredRegionIds.clear()
|
|
@@ -282,6 +282,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
282
282
|
var maxDistance: Double? = null
|
|
283
283
|
var exitDistance: Double? = null
|
|
284
284
|
var minRssi: Int? = null
|
|
285
|
+
var level: String = "all"
|
|
285
286
|
when (options) {
|
|
286
287
|
is Double -> maxDistance = options
|
|
287
288
|
is Map<*, *> -> {
|
|
@@ -290,6 +291,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
290
291
|
maxDistance = (map["maxDistance"] as? Number)?.toDouble()
|
|
291
292
|
exitDistance = (map["exitDistance"] as? Number)?.toDouble()
|
|
292
293
|
minRssi = (map["minRssi"] as? Number)?.toInt()
|
|
294
|
+
level = (map["level"] as? String) ?: "all"
|
|
293
295
|
val notifications = map["notifications"]
|
|
294
296
|
if (notifications is Map<*, *>) {
|
|
295
297
|
@Suppress("UNCHECKED_CAST")
|
|
@@ -322,6 +324,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
322
324
|
else remove("exit_distance")
|
|
323
325
|
if (minRssi != null) putInt("min_rssi", minRssi)
|
|
324
326
|
else remove("min_rssi")
|
|
327
|
+
putString("level", level)
|
|
325
328
|
}.apply()
|
|
326
329
|
// Verify we have the permissions needed for background monitoring
|
|
327
330
|
val hasLocation = ContextCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
@@ -26,6 +26,9 @@ export type PairedBeacon = {
|
|
|
26
26
|
* Timeout in seconds. When set, the module fires `onBeaconTimeout` once
|
|
27
27
|
* after the beacon has been continuously in range for this duration.
|
|
28
28
|
* The timer resets if the beacon exits and re-enters range.
|
|
29
|
+
*
|
|
30
|
+
* The timeout countdown also starts if no BLE readings are received
|
|
31
|
+
* for 60 seconds (e.g. due to Doze mode or background throttling).
|
|
29
32
|
*/
|
|
30
33
|
timeoutSeconds?: number;
|
|
31
34
|
};
|
|
@@ -132,6 +135,13 @@ export type MonitoringOptions = {
|
|
|
132
135
|
* Default: -85. Typical range: -100 (very permissive) to -70 (strict).
|
|
133
136
|
*/
|
|
134
137
|
minRssi?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Controls which event types are emitted, logged, and forwarded to the API.
|
|
140
|
+
*
|
|
141
|
+
* - `'all'` (default): distance + enter + exit + timeout events.
|
|
142
|
+
* - `'events'`: enter + exit + timeout only (no distance events).
|
|
143
|
+
*/
|
|
144
|
+
level?: 'all' | 'events';
|
|
135
145
|
/** Notification configuration overrides to apply for this monitoring session. */
|
|
136
146
|
notifications?: NotificationConfig;
|
|
137
147
|
};
|
|
@@ -170,6 +180,9 @@ export type PairedEddystone = {
|
|
|
170
180
|
* Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
|
|
171
181
|
* after the beacon has been continuously in range for this duration.
|
|
172
182
|
* The timer resets if the beacon exits and re-enters range.
|
|
183
|
+
*
|
|
184
|
+
* The timeout countdown also starts if no BLE readings are received
|
|
185
|
+
* for 60 seconds (e.g. due to Doze mode or background throttling).
|
|
173
186
|
*/
|
|
174
187
|
timeoutSeconds?: number;
|
|
175
188
|
};
|
|
@@ -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;IAChB,0GAA0G;IAC1G,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,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;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd
|
|
1
|
+
{"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for beacon timeout events (beacon in range for configured duration). */\r\nexport type BeaconTimeoutEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Configuration for beacon enter/exit event notifications. */\r\nexport type BeaconNotificationConfig = {\r\n /** Whether to show enter/exit notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on beacon enter. Default: \"Beacon Entered\". */\r\n enterTitle?: string;\r\n /** Notification title on beacon exit. Default: \"Beacon Exited\". */\r\n exitTitle?: string;\r\n /**\r\n * Notification body template. Supports {identifier} and {event} placeholders.\r\n * Default: \"{identifier} region {event}ed\".\r\n */\r\n body?: string;\r\n /** Play a sound with the notification (iOS only). Default: true. */\r\n sound?: boolean;\r\n /** Android drawable resource name for the notification icon (e.g. \"ic_notification\"). */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android foreground service notification (persistent status bar entry). */\r\nexport type ForegroundServiceConfig = {\r\n /** Title of the persistent notification. Default: \"Beacon Monitoring Active\". */\r\n title?: string;\r\n /** Body text of the persistent notification. Default: \"Monitoring for iBeacons in the background\". */\r\n text?: string;\r\n /** Android drawable resource name for the notification icon. */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android notification channel. */\r\nexport type NotificationChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"Beacon Monitoring\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"Used for background iBeacon region monitoring\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'low'.\r\n * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.\r\n */\r\n importance?: \"low\" | \"default\" | \"high\";\r\n};\r\n\r\n/** Combined notification configuration for all notification types. */\r\nexport type NotificationConfig = {\r\n /** Settings for beacon enter/exit event notifications. */\r\n beaconEvents?: BeaconNotificationConfig;\r\n /** Settings for the persistent foreground service notification (Android only). */\r\n foregroundService?: ForegroundServiceConfig;\r\n /** Settings for the Android notification channel (Android only). */\r\n channel?: NotificationChannelConfig;\r\n};\r\n\r\n/** Options accepted by startMonitoring(). */\r\nexport type MonitoringOptions = {\r\n /**\r\n * Maximum distance in metres for distance-based enter events.\r\n * Exit events are always emitted when the region is lost.\r\n */\r\n maxDistance?: number;\r\n /**\r\n * Distance in metres at which exit events fire (must be ≥ maxDistance).\r\n * Creates a hysteresis band between enter and exit thresholds to prevent\r\n * rapid toggling near the boundary.\r\n *\r\n * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.\r\n * Only used when `maxDistance` is set.\r\n */\r\n exitDistance?: number;\r\n /**\r\n * Minimum RSSI (dBm) for a beacon reading to be considered valid.\r\n * Readings below this threshold are discarded as unreliable, preventing\r\n * false detections from reflected or distant signals.\r\n *\r\n * Default: -85. Typical range: -100 (very permissive) to -70 (strict).\r\n */\r\n minRssi?: number;\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for Eddystone enter/exit region events. */\r\nexport type EddystoneRegionEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic Eddystone distance update events during monitoring. */\r\nexport type EddystoneDistanceEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for Eddystone timeout events (beacon in range for configured duration). */\r\nexport type EddystoneTimeoutEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired once after a paired beacon has been continuously in range for its configured `timeoutSeconds`. */\r\n onBeaconTimeout: (params: BeaconTimeoutEvent) => void;\r\n /** Fired continuously during a live scan as each iBeacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n /** Fired continuously during a live scan as each Eddystone beacon is detected. */\r\n onEddystoneFound: (params: EddystoneScanResult) => void;\r\n onEddystoneEnter: (params: EddystoneRegionEvent) => void;\r\n onEddystoneExit: (params: EddystoneRegionEvent) => void;\r\n onEddystoneDistance: (params: EddystoneDistanceEvent) => void;\r\n /** Fired once after a paired Eddystone has been continuously in range for its configured `timeoutSeconds`. */\r\n onEddystoneTimeout: (params: EddystoneTimeoutEvent) => void;\r\n};\r\n\r\n/** Options for filtering event logs. */\r\nexport type EventLogQueryOptions = {\r\n /** Maximum number of log entries to return (default: 1000, max: 10000). */\r\n limit?: number;\r\n /** Filter by event type (e.g. \"onBeaconEnter\", \"onBeaconExit\"). */\r\n eventType?: string;\r\n /** Only return events with timestamp >= this value (ms since epoch). */\r\n sinceTimestamp?: number;\r\n};\r\n\r\n/** A single logged beacon event entry. */\r\nexport type EventLogEntry = {\r\n id: number;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** The event type that was logged (e.g. \"onBeaconEnter\"). */\r\n eventType: string;\r\n /** Beacon identifier, if available. */\r\n identifier?: string;\r\n /** The full event payload that was sent to JS. */\r\n data: Record<string, unknown>;\r\n};\r\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * The timeout countdown also starts if no BLE readings are received\r\n * for 60 seconds (e.g. due to Doze mode or background throttling).\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for enter/exit region events. */\r\nexport type BeaconRegionEvent = {\r\n identifier: string; // Matches PairedBeacon.identifier\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic distance update events during monitoring. */\r\nexport type BeaconDistanceEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for beacon timeout events (beacon in range for configured duration). */\r\nexport type BeaconTimeoutEvent = {\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Configuration for beacon enter/exit event notifications. */\r\nexport type BeaconNotificationConfig = {\r\n /** Whether to show enter/exit notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on beacon enter. Default: \"Beacon Entered\". */\r\n enterTitle?: string;\r\n /** Notification title on beacon exit. Default: \"Beacon Exited\". */\r\n exitTitle?: string;\r\n /**\r\n * Notification body template. Supports {identifier} and {event} placeholders.\r\n * Default: \"{identifier} region {event}ed\".\r\n */\r\n body?: string;\r\n /** Play a sound with the notification (iOS only). Default: true. */\r\n sound?: boolean;\r\n /** Android drawable resource name for the notification icon (e.g. \"ic_notification\"). */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android foreground service notification (persistent status bar entry). */\r\nexport type ForegroundServiceConfig = {\r\n /** Title of the persistent notification. Default: \"Beacon Monitoring Active\". */\r\n title?: string;\r\n /** Body text of the persistent notification. Default: \"Monitoring for iBeacons in the background\". */\r\n text?: string;\r\n /** Android drawable resource name for the notification icon. */\r\n icon?: string;\r\n};\r\n\r\n/** Configuration for the Android notification channel. */\r\nexport type NotificationChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"Beacon Monitoring\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"Used for background iBeacon region monitoring\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'low'.\r\n * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.\r\n */\r\n importance?: \"low\" | \"default\" | \"high\";\r\n};\r\n\r\n/** Combined notification configuration for all notification types. */\r\nexport type NotificationConfig = {\r\n /** Settings for beacon enter/exit event notifications. */\r\n beaconEvents?: BeaconNotificationConfig;\r\n /** Settings for the persistent foreground service notification (Android only). */\r\n foregroundService?: ForegroundServiceConfig;\r\n /** Settings for the Android notification channel (Android only). */\r\n channel?: NotificationChannelConfig;\r\n};\r\n\r\n/** Options accepted by startMonitoring(). */\r\nexport type MonitoringOptions = {\r\n /**\r\n * Maximum distance in metres for distance-based enter events.\r\n * Exit events are always emitted when the region is lost.\r\n */\r\n maxDistance?: number;\r\n /**\r\n * Distance in metres at which exit events fire (must be ≥ maxDistance).\r\n * Creates a hysteresis band between enter and exit thresholds to prevent\r\n * rapid toggling near the boundary.\r\n *\r\n * Default when omitted: `maxDistance + min(maxDistance × 0.5, 2.5)`.\r\n * Only used when `maxDistance` is set.\r\n */\r\n exitDistance?: number;\r\n /**\r\n * Minimum RSSI (dBm) for a beacon reading to be considered valid.\r\n * Readings below this threshold are discarded as unreliable, preventing\r\n * false detections from reflected or distant signals.\r\n *\r\n * Default: -85. Typical range: -100 (very permissive) to -70 (strict).\r\n */\r\n minRssi?: number;\r\n /**\r\n * Controls which event types are emitted, logged, and forwarded to the API.\r\n *\r\n * - `'all'` (default): distance + enter + exit + timeout events.\r\n * - `'events'`: enter + exit + timeout only (no distance events).\r\n */\r\n level?: 'all' | 'events';\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * The timeout countdown also starts if no BLE readings are received\r\n * for 60 seconds (e.g. due to Doze mode or background throttling).\r\n */\r\n timeoutSeconds?: number;\r\n};\r\n\r\n/** Payload for Eddystone enter/exit region events. */\r\nexport type EddystoneRegionEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n event: \"enter\" | \"exit\";\r\n /** Measured distance in metres at the time of the event (–1 if unavailable). */\r\n distance: number;\r\n /** Signal strength in dBm at the time of the event (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for periodic Eddystone distance update events during monitoring. */\r\nexport type EddystoneDistanceEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n distance: number;\r\n /** Signal strength in dBm (0 if unavailable). */\r\n rssi?: number;\r\n};\r\n\r\n/** Payload for Eddystone timeout events (beacon in range for configured duration). */\r\nexport type EddystoneTimeoutEvent = {\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n /** Current distance in metres at the time the timeout fired. */\r\n distance: number;\r\n};\r\n\r\n/** Module event map. */\r\nexport type ExpoBeaconModuleEvents = {\r\n onBeaconEnter: (params: BeaconRegionEvent) => void;\r\n onBeaconExit: (params: BeaconRegionEvent) => void;\r\n onBeaconDistance: (params: BeaconDistanceEvent) => void;\r\n /** Fired once after a paired beacon has been continuously in range for its configured `timeoutSeconds`. */\r\n onBeaconTimeout: (params: BeaconTimeoutEvent) => void;\r\n /** Fired continuously during a live scan as each iBeacon is detected. */\r\n onBeaconFound: (params: BeaconScanResult) => void;\r\n /** Fired continuously during a live scan as each Eddystone beacon is detected. */\r\n onEddystoneFound: (params: EddystoneScanResult) => void;\r\n onEddystoneEnter: (params: EddystoneRegionEvent) => void;\r\n onEddystoneExit: (params: EddystoneRegionEvent) => void;\r\n onEddystoneDistance: (params: EddystoneDistanceEvent) => void;\r\n /** Fired once after a paired Eddystone has been continuously in range for its configured `timeoutSeconds`. */\r\n onEddystoneTimeout: (params: EddystoneTimeoutEvent) => void;\r\n};\r\n\r\n/** Options for filtering event logs. */\r\nexport type EventLogQueryOptions = {\r\n /** Maximum number of log entries to return (default: 1000, max: 10000). */\r\n limit?: number;\r\n /** Filter by event type (e.g. \"onBeaconEnter\", \"onBeaconExit\"). */\r\n eventType?: string;\r\n /** Only return events with timestamp >= this value (ms since epoch). */\r\n sinceTimestamp?: number;\r\n};\r\n\r\n/** A single logged beacon event entry. */\r\nexport type EventLogEntry = {\r\n id: number;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** The event type that was logged (e.g. \"onBeaconEnter\"). */\r\n eventType: string;\r\n /** Beacon identifier, if available. */\r\n identifier?: string;\r\n /** The full event payload that was sent to JS. */\r\n data: Record<string, unknown>;\r\n};\r\n"]}
|
|
@@ -104,7 +104,7 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
104
104
|
destroyEventLogs(): void;
|
|
105
105
|
/**
|
|
106
106
|
* Configure a remote API endpoint for native event forwarding.
|
|
107
|
-
* Once set,
|
|
107
|
+
* Once set, beacon events are POSTed directly from native code,
|
|
108
108
|
* ensuring delivery even when the JS bridge is not active (app backgrounded).
|
|
109
109
|
*
|
|
110
110
|
* @param url The API endpoint URL to POST events to.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAuKzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /**\r\n * Check whether the app is exempt from Android battery optimizations.\r\n * Always returns true on iOS and web (no equivalent concept).\r\n */\r\n isBatteryOptimizationExempt(): boolean;\r\n\r\n /**\r\n * Request exemption from Android battery optimizations.\r\n * Opens the system dialog asking the user to whitelist this app.\r\n * Returns true if the dialog was shown (or already exempt), false on failure.\r\n * Always resolves true on iOS and web.\r\n */\r\n requestBatteryOptimizationExemption(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n\r\n /**\r\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set,
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAuKzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /**\r\n * Check whether the app is exempt from Android battery optimizations.\r\n * Always returns true on iOS and web (no equivalent concept).\r\n */\r\n isBatteryOptimizationExempt(): boolean;\r\n\r\n /**\r\n * Request exemption from Android battery optimizations.\r\n * Opens the system dialog asking the user to whitelist this app.\r\n * Returns true if the dialog was shown (or already exempt), false on failure.\r\n * Always resolves true on iOS and web.\r\n */\r\n requestBatteryOptimizationExemption(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n\r\n /**\r\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set, beacon events are POSTed directly from native code,\r\n * ensuring delivery even when the JS bridge is not active (app backgrounded).\r\n *\r\n * @param url The API endpoint URL to POST events to.\r\n * @param apiKey Optional API key sent as X-API-Key header.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
|
|
@@ -12,6 +12,7 @@ private let EXIT_DISTANCE_KEY = "expo.beacon.exit_distance"
|
|
|
12
12
|
private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
|
|
13
13
|
private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
|
|
14
14
|
private let MIN_RSSI_KEY = "expo.beacon.min_rssi"
|
|
15
|
+
private let EVENT_LEVEL_KEY = "expo.beacon.event_level"
|
|
15
16
|
|
|
16
17
|
/// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
|
|
17
18
|
private let DEFAULT_MIN_RSSI: Int = -85
|
|
@@ -34,6 +35,9 @@ private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
|
|
|
34
35
|
private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 15.0
|
|
35
36
|
/// Minimum interval between consecutive distance event emissions per identifier.
|
|
36
37
|
private let DISTANCE_EVENT_THROTTLE_INTERVAL: TimeInterval = 1.0
|
|
38
|
+
/// Seconds of no valid BLE readings before starting the timeout countdown.
|
|
39
|
+
/// Acts as a safety net when ranging cycles stop entirely (e.g. Doze mode).
|
|
40
|
+
private let DISTANCE_INACTIVITY_SECONDS: TimeInterval = 60.0
|
|
37
41
|
|
|
38
42
|
public class ExpoBeaconModule: Module {
|
|
39
43
|
|
|
@@ -98,6 +102,9 @@ public class ExpoBeaconModule: Module {
|
|
|
98
102
|
/// Minimum RSSI threshold — readings below this are treated as unreliable.
|
|
99
103
|
private var minRssiThreshold: Int = DEFAULT_MIN_RSSI
|
|
100
104
|
|
|
105
|
+
/// Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
|
|
106
|
+
private var eventLevel: String = "all"
|
|
107
|
+
|
|
101
108
|
/// Distance smoothing (EMA) state per identifier.
|
|
102
109
|
private var smoothedDistances: [String: Double] = [:]
|
|
103
110
|
/// EMA weight for new readings. 0.4 balances responsiveness vs noise rejection.
|
|
@@ -122,6 +129,9 @@ public class ExpoBeaconModule: Module {
|
|
|
122
129
|
// Timeout timers — fire once after beacon stays in range for configured duration
|
|
123
130
|
private var beaconTimeoutTimers: [String: DispatchWorkItem] = [:]
|
|
124
131
|
private var eddystoneTimeoutTimers: [String: DispatchWorkItem] = [:]
|
|
132
|
+
// Inactivity timers — start timeout countdown when no BLE readings for 60 s
|
|
133
|
+
private var beaconInactivityTimers: [String: DispatchWorkItem] = [:]
|
|
134
|
+
private var eddystoneInactivityTimers: [String: DispatchWorkItem] = [:]
|
|
125
135
|
|
|
126
136
|
// Custom UserDefaults suite to isolate beacon data from the host app's .standard
|
|
127
137
|
private lazy var defaults: UserDefaults = {
|
|
@@ -331,6 +341,13 @@ public class ExpoBeaconModule: Module {
|
|
|
331
341
|
maxDistance = map["maxDistance"] as? Double
|
|
332
342
|
exitDistance = map["exitDistance"] as? Double
|
|
333
343
|
minRssi = map["minRssi"] as? Int
|
|
344
|
+
if let lvl = map["level"] as? String, lvl == "events" || lvl == "all" {
|
|
345
|
+
self.eventLevel = lvl
|
|
346
|
+
self.defaults.set(lvl, forKey: EVENT_LEVEL_KEY)
|
|
347
|
+
} else {
|
|
348
|
+
self.eventLevel = "all"
|
|
349
|
+
self.defaults.set("all", forKey: EVENT_LEVEL_KEY)
|
|
350
|
+
}
|
|
334
351
|
if let notifications = map["notifications"] as? [String: Any],
|
|
335
352
|
let data = try? JSONSerialization.data(withJSONObject: notifications),
|
|
336
353
|
let json = String(data: data, encoding: .utf8) {
|
|
@@ -392,6 +409,8 @@ public class ExpoBeaconModule: Module {
|
|
|
392
409
|
self.defaults.set(false, forKey: IS_MONITORING_KEY)
|
|
393
410
|
self.defaults.removeObject(forKey: MAX_DISTANCE_KEY)
|
|
394
411
|
self.defaults.removeObject(forKey: EXIT_DISTANCE_KEY)
|
|
412
|
+
self.defaults.removeObject(forKey: EVENT_LEVEL_KEY)
|
|
413
|
+
self.eventLevel = "all"
|
|
395
414
|
self.stopRegionMonitoring()
|
|
396
415
|
promise.resolve(nil)
|
|
397
416
|
}
|
|
@@ -575,6 +594,9 @@ public class ExpoBeaconModule: Module {
|
|
|
575
594
|
let storedRssi = defaults.object(forKey: MIN_RSSI_KEY) as? Int
|
|
576
595
|
minRssiThreshold = storedRssi ?? DEFAULT_MIN_RSSI
|
|
577
596
|
|
|
597
|
+
// Restore persisted event level (survives app restarts)
|
|
598
|
+
eventLevel = defaults.string(forKey: EVENT_LEVEL_KEY) ?? "all"
|
|
599
|
+
|
|
578
600
|
let beacons = loadPairedBeaconsRaw()
|
|
579
601
|
|
|
580
602
|
// CLLocationManager supports a maximum of 20 monitored regions.
|
|
@@ -642,6 +664,9 @@ public class ExpoBeaconModule: Module {
|
|
|
642
664
|
for timer in beaconTimeoutTimers.values { timer.cancel() }
|
|
643
665
|
beaconTimeoutTimers.removeAll()
|
|
644
666
|
|
|
667
|
+
for timer in beaconInactivityTimers.values { timer.cancel() }
|
|
668
|
+
beaconInactivityTimers.removeAll()
|
|
669
|
+
|
|
645
670
|
stopEddystoneMonitoring()
|
|
646
671
|
}
|
|
647
672
|
|
|
@@ -810,6 +835,8 @@ public class ExpoBeaconModule: Module {
|
|
|
810
835
|
|
|
811
836
|
eddystoneLatestSeen[identifier] = Date()
|
|
812
837
|
eddystoneMissCounters[identifier] = 0
|
|
838
|
+
// Valid BLE reading — reset inactivity timer.
|
|
839
|
+
rescheduleEddystoneInactivity(identifier: identifier, namespace: ns, instance: inst)
|
|
813
840
|
|
|
814
841
|
// Distance-driven enter/exit with hysteresis — evaluated on every
|
|
815
842
|
// BLE callback (not throttled) so the hysteresis counters advance
|
|
@@ -818,13 +845,10 @@ public class ExpoBeaconModule: Module {
|
|
|
818
845
|
let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
|
|
819
846
|
let hasValidDistance = distance.isFinite && distance >= 0
|
|
820
847
|
if hasValidDistance || maxDist == nil {
|
|
821
|
-
// Apply EMA smoothing; jump
|
|
848
|
+
// Apply EMA smoothing; jump resets EMA to the new value
|
|
822
849
|
let effectiveDistance: Double
|
|
823
|
-
if hasValidDistance
|
|
824
|
-
effectiveDistance =
|
|
825
|
-
} else if hasValidDistance {
|
|
826
|
-
// Jump outlier — skip this cycle without resetting counters
|
|
827
|
-
break
|
|
850
|
+
if hasValidDistance {
|
|
851
|
+
effectiveDistance = smoothDistance(identifier: identifier, rawDistance: distance)!
|
|
828
852
|
} else {
|
|
829
853
|
effectiveDistance = distance
|
|
830
854
|
}
|
|
@@ -852,6 +876,7 @@ public class ExpoBeaconModule: Module {
|
|
|
852
876
|
cancelEddystoneTimeout(identifier: identifier)
|
|
853
877
|
case .exit:
|
|
854
878
|
smoothedDistances.removeValue(forKey: identifier)
|
|
879
|
+
print("[ExpoBeacon] DEBUG: Eddystone distance-based EXIT for \(identifier)")
|
|
855
880
|
sendLoggedEvent("onEddystoneExit", [
|
|
856
881
|
"identifier": identifier,
|
|
857
882
|
"namespace": ns,
|
|
@@ -861,7 +886,8 @@ public class ExpoBeaconModule: Module {
|
|
|
861
886
|
"rssi": beaconRssi
|
|
862
887
|
])
|
|
863
888
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
864
|
-
// Beacon left — start the timeout clock.
|
|
889
|
+
// Beacon left — cancel inactivity timer and start the timeout clock.
|
|
890
|
+
cancelEddystoneInactivity(identifier: identifier)
|
|
865
891
|
scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
|
|
866
892
|
case .none:
|
|
867
893
|
break
|
|
@@ -869,6 +895,7 @@ public class ExpoBeaconModule: Module {
|
|
|
869
895
|
}
|
|
870
896
|
|
|
871
897
|
guard hasValidDistance else { break }
|
|
898
|
+
guard self.eventLevel == "all" else { break }
|
|
872
899
|
|
|
873
900
|
// Throttle distance events — enter/exit above is evaluated on every
|
|
874
901
|
// callback, but distance events are rate-limited to avoid flooding JS.
|
|
@@ -960,10 +987,8 @@ public class ExpoBeaconModule: Module {
|
|
|
960
987
|
let identifier = params["identifier"] as? String
|
|
961
988
|
getOrCreateEventLogger().logEvent(eventType: eventName, identifier: identifier, data: params)
|
|
962
989
|
}
|
|
963
|
-
// Forward
|
|
964
|
-
|
|
965
|
-
apiForwarder.forwardEvent(params)
|
|
966
|
-
}
|
|
990
|
+
// Forward all produced events to remote API
|
|
991
|
+
apiForwarder.forwardEvent(params)
|
|
967
992
|
sendEvent(eventName, params)
|
|
968
993
|
}
|
|
969
994
|
|
|
@@ -1049,6 +1074,9 @@ public class ExpoBeaconModule: Module {
|
|
|
1049
1074
|
for timer in eddystoneTimeoutTimers.values { timer.cancel() }
|
|
1050
1075
|
eddystoneTimeoutTimers.removeAll()
|
|
1051
1076
|
|
|
1077
|
+
for timer in eddystoneInactivityTimers.values { timer.cancel() }
|
|
1078
|
+
eddystoneInactivityTimers.removeAll()
|
|
1079
|
+
|
|
1052
1080
|
stopBleScanIfUnneeded()
|
|
1053
1081
|
}
|
|
1054
1082
|
|
|
@@ -1093,8 +1121,10 @@ public class ExpoBeaconModule: Module {
|
|
|
1093
1121
|
"distance": -1
|
|
1094
1122
|
]
|
|
1095
1123
|
sendLoggedEvent("onEddystoneExit", params)
|
|
1124
|
+
print("[ExpoBeacon] DEBUG: Eddystone miss-based EXIT for \(identifier)")
|
|
1096
1125
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1097
|
-
// Beacon disappeared — start the timeout clock.
|
|
1126
|
+
// Beacon disappeared — cancel inactivity timer and start the timeout clock.
|
|
1127
|
+
cancelEddystoneInactivity(identifier: identifier)
|
|
1098
1128
|
scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
|
|
1099
1129
|
}
|
|
1100
1130
|
}
|
|
@@ -1147,6 +1177,48 @@ public class ExpoBeaconModule: Module {
|
|
|
1147
1177
|
eddystoneTimeoutTimers.removeValue(forKey: identifier)?.cancel()
|
|
1148
1178
|
}
|
|
1149
1179
|
|
|
1180
|
+
// MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
|
|
1181
|
+
|
|
1182
|
+
private func rescheduleBeaconInactivity(identifier: String, beacon: CLBeacon? = nil, region: CLBeaconRegion? = nil) {
|
|
1183
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1184
|
+
|
|
1185
|
+
let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
|
|
1186
|
+
guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
|
|
1187
|
+
|
|
1188
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1189
|
+
guard let self = self else { return }
|
|
1190
|
+
self.beaconInactivityTimers.removeValue(forKey: identifier)
|
|
1191
|
+
// No BLE readings for 60 s — start the configured timeout countdown.
|
|
1192
|
+
self.scheduleBeaconTimeout(identifier: identifier, beacon: beacon, region: region)
|
|
1193
|
+
}
|
|
1194
|
+
beaconInactivityTimers[identifier] = work
|
|
1195
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private func rescheduleEddystoneInactivity(identifier: String, namespace: String, instance: String) {
|
|
1199
|
+
cancelEddystoneInactivity(identifier: identifier)
|
|
1200
|
+
|
|
1201
|
+
let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
|
|
1202
|
+
guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
|
|
1203
|
+
|
|
1204
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1205
|
+
guard let self = self else { return }
|
|
1206
|
+
self.eddystoneInactivityTimers.removeValue(forKey: identifier)
|
|
1207
|
+
// No BLE readings for 60 s — start the configured timeout countdown.
|
|
1208
|
+
self.scheduleEddystoneTimeout(identifier: identifier, namespace: namespace, instance: instance)
|
|
1209
|
+
}
|
|
1210
|
+
eddystoneInactivityTimers[identifier] = work
|
|
1211
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private func cancelBeaconInactivity(identifier: String) {
|
|
1215
|
+
beaconInactivityTimers.removeValue(forKey: identifier)?.cancel()
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
private func cancelEddystoneInactivity(identifier: String) {
|
|
1219
|
+
eddystoneInactivityTimers.removeValue(forKey: identifier)?.cancel()
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1150
1222
|
private func postBeaconNotification(identifier: String, eventType: String) {
|
|
1151
1223
|
let cfg = loadNotificationConfig()
|
|
1152
1224
|
let eventsCfg = cfg["beaconEvents"] as? [String: Any]
|
|
@@ -1328,9 +1400,13 @@ public class ExpoBeaconModule: Module {
|
|
|
1328
1400
|
if let beacon = validBeacon {
|
|
1329
1401
|
// Got a valid reading — reset miss counter
|
|
1330
1402
|
missCounters[identifier] = 0
|
|
1403
|
+
// Valid BLE reading — reset inactivity timer.
|
|
1404
|
+
rescheduleBeaconInactivity(identifier: identifier, beacon: beacon)
|
|
1331
1405
|
|
|
1332
|
-
// Emit distance event every ranging cycle (~1 s)
|
|
1333
|
-
|
|
1406
|
+
// Emit distance event every ranging cycle (~1 s) if level allows
|
|
1407
|
+
if self.eventLevel == "all" {
|
|
1408
|
+
sendLoggedEvent("onBeaconDistance", makeBeaconEventParams(identifier: identifier, beacon: beacon))
|
|
1409
|
+
}
|
|
1334
1410
|
|
|
1335
1411
|
// Enter/exit synthesis with hysteresis — always applied.
|
|
1336
1412
|
// When maxDistance is set, distance thresholds control transitions.
|
|
@@ -1339,11 +1415,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1339
1415
|
let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
|
|
1340
1416
|
let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
|
|
1341
1417
|
|
|
1342
|
-
// Apply EMA smoothing; jump
|
|
1343
|
-
|
|
1344
|
-
// Jump outlier — skip this cycle without resetting counters
|
|
1345
|
-
return
|
|
1346
|
-
}
|
|
1418
|
+
// Apply EMA smoothing; jump resets EMA to the new value
|
|
1419
|
+
let smoothed = smoothDistance(identifier: identifier, rawDistance: beacon.accuracy)!
|
|
1347
1420
|
|
|
1348
1421
|
let action = evaluateDistanceHysteresis(
|
|
1349
1422
|
identifier: identifier,
|
|
@@ -1364,7 +1437,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1364
1437
|
smoothedDistances.removeValue(forKey: identifier)
|
|
1365
1438
|
sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
|
|
1366
1439
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1367
|
-
// Beacon left — start the timeout clock.
|
|
1440
|
+
// Beacon left — cancel inactivity timer and start the timeout clock.
|
|
1441
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1368
1442
|
scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
|
|
1369
1443
|
case .none:
|
|
1370
1444
|
break
|
|
@@ -1392,7 +1466,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1392
1466
|
let region = monitoredRegions.first { $0.identifier == identifier }
|
|
1393
1467
|
sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: region, event: "exit"))
|
|
1394
1468
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1395
|
-
// Beacon disappeared — start the timeout clock.
|
|
1469
|
+
// Beacon disappeared — cancel inactivity timer and start the timeout clock.
|
|
1470
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1396
1471
|
scheduleBeaconTimeout(identifier: identifier, region: region)
|
|
1397
1472
|
}
|
|
1398
1473
|
}
|
|
@@ -1438,7 +1513,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1438
1513
|
if wasEntered {
|
|
1439
1514
|
sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
|
|
1440
1515
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1441
|
-
// OS-level exit safety net — start the timeout clock.
|
|
1516
|
+
// OS-level exit safety net — cancel inactivity timer and start the timeout clock.
|
|
1517
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1442
1518
|
scheduleBeaconTimeout(identifier: identifier, region: beaconRegion)
|
|
1443
1519
|
}
|
|
1444
1520
|
}
|
package/package.json
CHANGED
package/src/ExpoBeacon.types.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type PairedBeacon = {
|
|
|
27
27
|
* Timeout in seconds. When set, the module fires `onBeaconTimeout` once
|
|
28
28
|
* after the beacon has been continuously in range for this duration.
|
|
29
29
|
* The timer resets if the beacon exits and re-enters range.
|
|
30
|
+
*
|
|
31
|
+
* The timeout countdown also starts if no BLE readings are received
|
|
32
|
+
* for 60 seconds (e.g. due to Doze mode or background throttling).
|
|
30
33
|
*/
|
|
31
34
|
timeoutSeconds?: number;
|
|
32
35
|
};
|
|
@@ -141,6 +144,13 @@ export type MonitoringOptions = {
|
|
|
141
144
|
* Default: -85. Typical range: -100 (very permissive) to -70 (strict).
|
|
142
145
|
*/
|
|
143
146
|
minRssi?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Controls which event types are emitted, logged, and forwarded to the API.
|
|
149
|
+
*
|
|
150
|
+
* - `'all'` (default): distance + enter + exit + timeout events.
|
|
151
|
+
* - `'events'`: enter + exit + timeout only (no distance events).
|
|
152
|
+
*/
|
|
153
|
+
level?: 'all' | 'events';
|
|
144
154
|
/** Notification configuration overrides to apply for this monitoring session. */
|
|
145
155
|
notifications?: NotificationConfig;
|
|
146
156
|
};
|
|
@@ -182,6 +192,9 @@ export type PairedEddystone = {
|
|
|
182
192
|
* Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
|
|
183
193
|
* after the beacon has been continuously in range for this duration.
|
|
184
194
|
* The timer resets if the beacon exits and re-enters range.
|
|
195
|
+
*
|
|
196
|
+
* The timeout countdown also starts if no BLE readings are received
|
|
197
|
+
* for 60 seconds (e.g. due to Doze mode or background throttling).
|
|
185
198
|
*/
|
|
186
199
|
timeoutSeconds?: number;
|
|
187
200
|
};
|
package/src/ExpoBeaconModule.ts
CHANGED
|
@@ -156,7 +156,7 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* Configure a remote API endpoint for native event forwarding.
|
|
159
|
-
* Once set,
|
|
159
|
+
* Once set, beacon events are POSTed directly from native code,
|
|
160
160
|
* ensuring delivery even when the JS bridge is not active (app backgrounded).
|
|
161
161
|
*
|
|
162
162
|
* @param url The API endpoint URL to POST events to.
|