expo-beacon 0.6.14 → 0.6.16
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 +43 -8
- package/build/ExpoBeacon.types.d.ts +8 -0
- package/build/ExpoBeacon.types.d.ts.map +1 -1
- package/build/ExpoBeacon.types.js.map +1 -1
- package/ios/ExpoBeaconModule.swift +81 -8
- package/package.json +1 -1
- package/src/ExpoBeacon.types.ts +8 -0
|
@@ -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,6 +64,8 @@ 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
|
|
69
71
|
// Event level: "all" emits distance + enter/exit/timeout; "events" suppresses distance.
|
|
@@ -177,6 +179,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
177
179
|
lastSeenAtMs.clear()
|
|
178
180
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
179
181
|
timeoutRunnables.clear()
|
|
182
|
+
inactivityRunnables.clear()
|
|
180
183
|
synchronized(distanceLock) {
|
|
181
184
|
enterCounters.clear()
|
|
182
185
|
exitCounters.clear()
|
|
@@ -264,6 +267,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
264
267
|
val closest = beacons.filter { it.distance >= 0 && it.rssi >= minRssiThreshold }.minByOrNull { it.distance }
|
|
265
268
|
if (closest != null) {
|
|
266
269
|
lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
|
|
270
|
+
// Valid BLE reading — reset inactivity timer.
|
|
271
|
+
rescheduleInactivity(region)
|
|
267
272
|
sendBeaconBroadcast(region, "distance", closest.distance, closest.rssi)
|
|
268
273
|
}
|
|
269
274
|
}
|
|
@@ -294,7 +299,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
294
299
|
if (wasEntered) {
|
|
295
300
|
sendBeaconBroadcast(region, "exit", -1.0)
|
|
296
301
|
showEnterExitNotification(region, "exit")
|
|
297
|
-
// 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)
|
|
298
304
|
scheduleTimeoutIfConfigured(region)
|
|
299
305
|
}
|
|
300
306
|
}
|
|
@@ -320,6 +326,8 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
320
326
|
// Got a valid reading — reset miss counter
|
|
321
327
|
lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
|
|
322
328
|
missCounters[region.uniqueId] = 0
|
|
329
|
+
// Valid BLE reading — reset inactivity timer.
|
|
330
|
+
rescheduleInactivity(region)
|
|
323
331
|
|
|
324
332
|
// Apply EMA smoothing; jump resets EMA to the new value
|
|
325
333
|
val smoothed = smoothDistance(region.uniqueId, beacon.distance)
|
|
@@ -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
|
}
|
|
@@ -468,6 +478,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
468
478
|
val runnable = Runnable {
|
|
469
479
|
timeoutRunnables.remove(region.uniqueId)
|
|
470
480
|
sendBeaconBroadcast(region, "timeout", -1.0)
|
|
481
|
+
showEnterExitNotification(region, "timeout")
|
|
471
482
|
}
|
|
472
483
|
timeoutRunnables[region.uniqueId] = runnable
|
|
473
484
|
timeoutHandler.postDelayed(runnable, seconds * 1000L)
|
|
@@ -477,6 +488,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
477
488
|
timeoutRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
|
|
478
489
|
}
|
|
479
490
|
|
|
491
|
+
// MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
|
|
492
|
+
|
|
493
|
+
private fun rescheduleInactivity(region: Region) {
|
|
494
|
+
val regionId = region.uniqueId
|
|
495
|
+
if (!beaconTimeouts.containsKey(regionId)) return
|
|
496
|
+
cancelInactivity(regionId)
|
|
497
|
+
val runnable = Runnable {
|
|
498
|
+
inactivityRunnables.remove(regionId)
|
|
499
|
+
// No BLE readings for 60 s — start the configured timeout countdown.
|
|
500
|
+
scheduleTimeoutIfConfigured(region)
|
|
501
|
+
}
|
|
502
|
+
inactivityRunnables[regionId] = runnable
|
|
503
|
+
timeoutHandler.postDelayed(runnable, DISTANCE_INACTIVITY_MS)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private fun cancelInactivity(regionId: String) {
|
|
507
|
+
inactivityRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
|
|
508
|
+
}
|
|
509
|
+
|
|
480
510
|
private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double, rssi: Int = 0) {
|
|
481
511
|
// Determine if this is an Eddystone region based on identifier format
|
|
482
512
|
// Eddystone regions have id1 as a hex namespace (not a UUID)
|
|
@@ -566,11 +596,15 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
566
596
|
// Respect the enabled flag (defaults to true)
|
|
567
597
|
if (eventsConfig != null && !eventsConfig.optBoolean("enabled", true)) return
|
|
568
598
|
|
|
569
|
-
val defaultTitle =
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
599
|
+
val defaultTitle = when (eventType) {
|
|
600
|
+
"enter" -> "Beacon Entered"
|
|
601
|
+
"timeout" -> "Beacon Timeout"
|
|
602
|
+
else -> "Beacon Exited"
|
|
603
|
+
}
|
|
604
|
+
val title = when (eventType) {
|
|
605
|
+
"enter" -> eventsConfig?.optString("enterTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
|
|
606
|
+
"timeout" -> eventsConfig?.optString("timeoutTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
|
|
607
|
+
else -> eventsConfig?.optString("exitTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
|
|
574
608
|
}
|
|
575
609
|
|
|
576
610
|
val bodyTemplate = eventsConfig?.optString("body")?.takeIf { it.isNotEmpty() }
|
|
@@ -736,6 +770,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
736
770
|
serviceConnected = false
|
|
737
771
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
738
772
|
timeoutRunnables.clear()
|
|
773
|
+
inactivityRunnables.clear()
|
|
739
774
|
beaconTimeouts.clear()
|
|
740
775
|
lastSeenAtMs.clear()
|
|
741
776
|
monitoredRegionIds.clear()
|
|
@@ -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
|
};
|
|
@@ -68,6 +71,8 @@ export type BeaconNotificationConfig = {
|
|
|
68
71
|
enterTitle?: string;
|
|
69
72
|
/** Notification title on beacon exit. Default: "Beacon Exited". */
|
|
70
73
|
exitTitle?: string;
|
|
74
|
+
/** Notification title on beacon timeout. Default: "Beacon Timeout". */
|
|
75
|
+
timeoutTitle?: string;
|
|
71
76
|
/**
|
|
72
77
|
* Notification body template. Supports {identifier} and {event} placeholders.
|
|
73
78
|
* Default: "{identifier} region {event}ed".
|
|
@@ -177,6 +182,9 @@ export type PairedEddystone = {
|
|
|
177
182
|
* Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
|
|
178
183
|
* after the beacon has been continuously in range for this duration.
|
|
179
184
|
* The timer resets if the beacon exits and re-enters range.
|
|
185
|
+
*
|
|
186
|
+
* The timeout countdown also starts if no BLE readings are received
|
|
187
|
+
* for 60 seconds (e.g. due to Doze mode or background throttling).
|
|
180
188
|
*/
|
|
181
189
|
timeoutSeconds?: number;
|
|
182
190
|
};
|
|
@@ -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 /**\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 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 /** Notification title on beacon timeout. Default: \"Beacon Timeout\". */\r\n timeoutTitle?: 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"]}
|
|
@@ -35,6 +35,9 @@ private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
|
|
|
35
35
|
private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 15.0
|
|
36
36
|
/// Minimum interval between consecutive distance event emissions per identifier.
|
|
37
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
|
|
38
41
|
|
|
39
42
|
public class ExpoBeaconModule: Module {
|
|
40
43
|
|
|
@@ -126,6 +129,9 @@ public class ExpoBeaconModule: Module {
|
|
|
126
129
|
// Timeout timers — fire once after beacon stays in range for configured duration
|
|
127
130
|
private var beaconTimeoutTimers: [String: DispatchWorkItem] = [:]
|
|
128
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] = [:]
|
|
129
135
|
|
|
130
136
|
// Custom UserDefaults suite to isolate beacon data from the host app's .standard
|
|
131
137
|
private lazy var defaults: UserDefaults = {
|
|
@@ -658,6 +664,9 @@ public class ExpoBeaconModule: Module {
|
|
|
658
664
|
for timer in beaconTimeoutTimers.values { timer.cancel() }
|
|
659
665
|
beaconTimeoutTimers.removeAll()
|
|
660
666
|
|
|
667
|
+
for timer in beaconInactivityTimers.values { timer.cancel() }
|
|
668
|
+
beaconInactivityTimers.removeAll()
|
|
669
|
+
|
|
661
670
|
stopEddystoneMonitoring()
|
|
662
671
|
}
|
|
663
672
|
|
|
@@ -826,6 +835,8 @@ public class ExpoBeaconModule: Module {
|
|
|
826
835
|
|
|
827
836
|
eddystoneLatestSeen[identifier] = Date()
|
|
828
837
|
eddystoneMissCounters[identifier] = 0
|
|
838
|
+
// Valid BLE reading — reset inactivity timer.
|
|
839
|
+
rescheduleEddystoneInactivity(identifier: identifier, namespace: ns, instance: inst)
|
|
829
840
|
|
|
830
841
|
// Distance-driven enter/exit with hysteresis — evaluated on every
|
|
831
842
|
// BLE callback (not throttled) so the hysteresis counters advance
|
|
@@ -875,7 +886,8 @@ public class ExpoBeaconModule: Module {
|
|
|
875
886
|
"rssi": beaconRssi
|
|
876
887
|
])
|
|
877
888
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
878
|
-
// Beacon left — start the timeout clock.
|
|
889
|
+
// Beacon left — cancel inactivity timer and start the timeout clock.
|
|
890
|
+
cancelEddystoneInactivity(identifier: identifier)
|
|
879
891
|
scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
|
|
880
892
|
case .none:
|
|
881
893
|
break
|
|
@@ -1062,6 +1074,9 @@ public class ExpoBeaconModule: Module {
|
|
|
1062
1074
|
for timer in eddystoneTimeoutTimers.values { timer.cancel() }
|
|
1063
1075
|
eddystoneTimeoutTimers.removeAll()
|
|
1064
1076
|
|
|
1077
|
+
for timer in eddystoneInactivityTimers.values { timer.cancel() }
|
|
1078
|
+
eddystoneInactivityTimers.removeAll()
|
|
1079
|
+
|
|
1065
1080
|
stopBleScanIfUnneeded()
|
|
1066
1081
|
}
|
|
1067
1082
|
|
|
@@ -1108,7 +1123,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1108
1123
|
sendLoggedEvent("onEddystoneExit", params)
|
|
1109
1124
|
print("[ExpoBeacon] DEBUG: Eddystone miss-based EXIT for \(identifier)")
|
|
1110
1125
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1111
|
-
// Beacon disappeared — start the timeout clock.
|
|
1126
|
+
// Beacon disappeared — cancel inactivity timer and start the timeout clock.
|
|
1127
|
+
cancelEddystoneInactivity(identifier: identifier)
|
|
1112
1128
|
scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
|
|
1113
1129
|
}
|
|
1114
1130
|
}
|
|
@@ -1127,6 +1143,7 @@ public class ExpoBeaconModule: Module {
|
|
|
1127
1143
|
guard let self = self else { return }
|
|
1128
1144
|
self.beaconTimeoutTimers.removeValue(forKey: identifier)
|
|
1129
1145
|
self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon, region: region))
|
|
1146
|
+
self.postBeaconNotification(identifier: identifier, eventType: "timeout")
|
|
1130
1147
|
}
|
|
1131
1148
|
beaconTimeoutTimers[identifier] = work
|
|
1132
1149
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds), execute: work)
|
|
@@ -1152,6 +1169,7 @@ public class ExpoBeaconModule: Module {
|
|
|
1152
1169
|
"instance": instance,
|
|
1153
1170
|
"distance": -1
|
|
1154
1171
|
])
|
|
1172
|
+
self.postBeaconNotification(identifier: identifier, eventType: "timeout")
|
|
1155
1173
|
}
|
|
1156
1174
|
eddystoneTimeoutTimers[identifier] = work
|
|
1157
1175
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(seconds), execute: work)
|
|
@@ -1161,6 +1179,48 @@ public class ExpoBeaconModule: Module {
|
|
|
1161
1179
|
eddystoneTimeoutTimers.removeValue(forKey: identifier)?.cancel()
|
|
1162
1180
|
}
|
|
1163
1181
|
|
|
1182
|
+
// MARK: - Inactivity timer helpers (no BLE readings → start timeout countdown)
|
|
1183
|
+
|
|
1184
|
+
private func rescheduleBeaconInactivity(identifier: String, beacon: CLBeacon? = nil, region: CLBeaconRegion? = nil) {
|
|
1185
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1186
|
+
|
|
1187
|
+
let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
|
|
1188
|
+
guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
|
|
1189
|
+
|
|
1190
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1191
|
+
guard let self = self else { return }
|
|
1192
|
+
self.beaconInactivityTimers.removeValue(forKey: identifier)
|
|
1193
|
+
// No BLE readings for 60 s — start the configured timeout countdown.
|
|
1194
|
+
self.scheduleBeaconTimeout(identifier: identifier, beacon: beacon, region: region)
|
|
1195
|
+
}
|
|
1196
|
+
beaconInactivityTimers[identifier] = work
|
|
1197
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private func rescheduleEddystoneInactivity(identifier: String, namespace: String, instance: String) {
|
|
1201
|
+
cancelEddystoneInactivity(identifier: identifier)
|
|
1202
|
+
|
|
1203
|
+
let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
|
|
1204
|
+
guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
|
|
1205
|
+
|
|
1206
|
+
let work = DispatchWorkItem { [weak self] in
|
|
1207
|
+
guard let self = self else { return }
|
|
1208
|
+
self.eddystoneInactivityTimers.removeValue(forKey: identifier)
|
|
1209
|
+
// No BLE readings for 60 s — start the configured timeout countdown.
|
|
1210
|
+
self.scheduleEddystoneTimeout(identifier: identifier, namespace: namespace, instance: instance)
|
|
1211
|
+
}
|
|
1212
|
+
eddystoneInactivityTimers[identifier] = work
|
|
1213
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + DISTANCE_INACTIVITY_SECONDS, execute: work)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private func cancelBeaconInactivity(identifier: String) {
|
|
1217
|
+
beaconInactivityTimers.removeValue(forKey: identifier)?.cancel()
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private func cancelEddystoneInactivity(identifier: String) {
|
|
1221
|
+
eddystoneInactivityTimers.removeValue(forKey: identifier)?.cancel()
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1164
1224
|
private func postBeaconNotification(identifier: String, eventType: String) {
|
|
1165
1225
|
let cfg = loadNotificationConfig()
|
|
1166
1226
|
let eventsCfg = cfg["beaconEvents"] as? [String: Any]
|
|
@@ -1168,11 +1228,19 @@ public class ExpoBeaconModule: Module {
|
|
|
1168
1228
|
// Respect the enabled flag (defaults to true)
|
|
1169
1229
|
if let enabled = eventsCfg?["enabled"] as? Bool, !enabled { return }
|
|
1170
1230
|
|
|
1171
|
-
let defaultTitle
|
|
1231
|
+
let defaultTitle: String
|
|
1232
|
+
switch eventType {
|
|
1233
|
+
case "enter": defaultTitle = "Beacon Entered"
|
|
1234
|
+
case "timeout": defaultTitle = "Beacon Timeout"
|
|
1235
|
+
default: defaultTitle = "Beacon Exited"
|
|
1236
|
+
}
|
|
1172
1237
|
let title: String
|
|
1173
|
-
|
|
1238
|
+
switch eventType {
|
|
1239
|
+
case "enter":
|
|
1174
1240
|
title = (eventsCfg?["enterTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
|
|
1175
|
-
|
|
1241
|
+
case "timeout":
|
|
1242
|
+
title = (eventsCfg?["timeoutTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
|
|
1243
|
+
default:
|
|
1176
1244
|
title = (eventsCfg?["exitTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
|
|
1177
1245
|
}
|
|
1178
1246
|
|
|
@@ -1342,6 +1410,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1342
1410
|
if let beacon = validBeacon {
|
|
1343
1411
|
// Got a valid reading — reset miss counter
|
|
1344
1412
|
missCounters[identifier] = 0
|
|
1413
|
+
// Valid BLE reading — reset inactivity timer.
|
|
1414
|
+
rescheduleBeaconInactivity(identifier: identifier, beacon: beacon)
|
|
1345
1415
|
|
|
1346
1416
|
// Emit distance event every ranging cycle (~1 s) if level allows
|
|
1347
1417
|
if self.eventLevel == "all" {
|
|
@@ -1377,7 +1447,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1377
1447
|
smoothedDistances.removeValue(forKey: identifier)
|
|
1378
1448
|
sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
|
|
1379
1449
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1380
|
-
// Beacon left — start the timeout clock.
|
|
1450
|
+
// Beacon left — cancel inactivity timer and start the timeout clock.
|
|
1451
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1381
1452
|
scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
|
|
1382
1453
|
case .none:
|
|
1383
1454
|
break
|
|
@@ -1405,7 +1476,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1405
1476
|
let region = monitoredRegions.first { $0.identifier == identifier }
|
|
1406
1477
|
sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: region, event: "exit"))
|
|
1407
1478
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1408
|
-
// Beacon disappeared — start the timeout clock.
|
|
1479
|
+
// Beacon disappeared — cancel inactivity timer and start the timeout clock.
|
|
1480
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1409
1481
|
scheduleBeaconTimeout(identifier: identifier, region: region)
|
|
1410
1482
|
}
|
|
1411
1483
|
}
|
|
@@ -1451,7 +1523,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1451
1523
|
if wasEntered {
|
|
1452
1524
|
sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
|
|
1453
1525
|
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
1454
|
-
// OS-level exit safety net — start the timeout clock.
|
|
1526
|
+
// OS-level exit safety net — cancel inactivity timer and start the timeout clock.
|
|
1527
|
+
cancelBeaconInactivity(identifier: identifier)
|
|
1455
1528
|
scheduleBeaconTimeout(identifier: identifier, region: beaconRegion)
|
|
1456
1529
|
}
|
|
1457
1530
|
}
|
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
|
};
|
|
@@ -73,6 +76,8 @@ export type BeaconNotificationConfig = {
|
|
|
73
76
|
enterTitle?: string;
|
|
74
77
|
/** Notification title on beacon exit. Default: "Beacon Exited". */
|
|
75
78
|
exitTitle?: string;
|
|
79
|
+
/** Notification title on beacon timeout. Default: "Beacon Timeout". */
|
|
80
|
+
timeoutTitle?: string;
|
|
76
81
|
/**
|
|
77
82
|
* Notification body template. Supports {identifier} and {event} placeholders.
|
|
78
83
|
* Default: "{identifier} region {event}ed".
|
|
@@ -189,6 +194,9 @@ export type PairedEddystone = {
|
|
|
189
194
|
* Timeout in seconds. When set, the module fires `onEddystoneTimeout` once
|
|
190
195
|
* after the beacon has been continuously in range for this duration.
|
|
191
196
|
* The timer resets if the beacon exits and re-enters range.
|
|
197
|
+
*
|
|
198
|
+
* The timeout countdown also starts if no BLE readings are received
|
|
199
|
+
* for 60 seconds (e.g. due to Doze mode or background throttling).
|
|
192
200
|
*/
|
|
193
201
|
timeoutSeconds?: number;
|
|
194
202
|
};
|