expo-beacon 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -729,11 +729,20 @@ disconnectSub.remove();
729
729
  - Connect/disconnect events flow through the same SQLite event log and remote API forwarder as beacon events.
730
730
  - When the config plugin is installed, the auto-generated `BeaconGeoPlugin` also calls `BackgroundGeolocation.start()` on connect and `.stop()` on disconnect — no extra wiring required.
731
731
 
732
+ **Background detection**
733
+
734
+ CarPlay observation is **persistent** — the enabled flag is stored in native preferences and the observer is automatically re-attached after app kill or device reboot. `startMonitoring()` also enables CarPlay observation by default; calling `startCarPlayMonitoring()` explicitly is only required if you want CarPlay events without beacon monitoring.
735
+
736
+ - **Android:** the foreground service hosts the `CarConnection` observer. As long as the service runs (which it does whenever beacon monitoring or CarPlay monitoring is enabled, and is restarted on boot by `BootReceiver`), CarPlay events are captured even after the app process is killed. **Guaranteed background detection.**
737
+ - **iOS:** the observer auto-restarts in the module's `OnCreate`, including background-launches triggered by beacon region monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for guaranteed wake-from-suspension, also call `startMonitoring()` with at least one paired beacon (e.g. a beacon left in the vehicle). Region-wake events trigger a CarPlay state resync to reconcile any route changes that happened while the app was suspended.
738
+
732
739
  **Notes**
733
740
 
734
741
  - `startCarPlayMonitoring()` is idempotent. Calling it twice does not register a duplicate observer.
742
+ - `stopCarPlayMonitoring()` clears the persisted flag, so the observer will not auto-restart on next launch.
735
743
  - The iOS detector does not require the CarPlay entitlement because it only reads the active audio route; you do not need to ship a CarPlay app.
736
744
  - On iOS, if the JS bundle is suspended in the background, the JS event delivery is deferred until the app resumes, but the native lifecycle delegate (used by the geolocation plugin) fires immediately on connect.
745
+ - On Android, when CarPlay monitoring is enabled without beacon monitoring, the foreground service shows a generic "Connected device monitoring active" notification.
737
746
 
738
747
  ---
739
748
 
@@ -84,6 +84,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
84
84
  // Seconds of silence after last valid sighting before a disappearance-based exit fires.
85
85
  @Volatile private var exitTimeoutMs: Long = (DEFAULT_EXIT_TIMEOUT_SECONDS * 1000.0).toLong()
86
86
 
87
+ // CarPlay / Android Auto observer hosted by the foreground service so that
88
+ // CarConnection events are captured for as long as the service runs —
89
+ // independent of the JS bridge / module lifecycle. Survives app suspension
90
+ // and (via BootReceiver) device reboot.
91
+ @Volatile private var carPlayMonitor: CarPlayMonitor? = null
92
+
87
93
  override fun onCreate() {
88
94
  super.onCreate()
89
95
  activeService = this
@@ -106,6 +112,14 @@ class BeaconForegroundService : Service(), BeaconConsumer {
106
112
  // process and inherits the elevated priority. Calling enable/disable on the
107
113
  // shared singleton causes crashes when the ExpoBeaconModule has an active
108
114
  // scan bound to the same BeaconManager.
115
+
116
+ // Restore CarPlay observer if the user previously enabled CarPlay
117
+ // monitoring. Reading the flag here means cold-started services
118
+ // (e.g. via BootReceiver after reboot) automatically re-attach the
119
+ // observer with no JS interaction required.
120
+ if (isCarPlayEnabled(this)) {
121
+ startCarPlayObserverInternal()
122
+ }
109
123
  }
110
124
 
111
125
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -134,11 +148,32 @@ class BeaconForegroundService : Service(), BeaconConsumer {
134
148
  stopSelf()
135
149
  return START_NOT_STICKY
136
150
  }
151
+ // Handle CarPlay enable/disable actions. These can arrive after the
152
+ // service is already running for beacon monitoring, OR be the reason
153
+ // the service was started in the first place (CarPlay-only mode).
154
+ when (intent?.action) {
155
+ ACTION_ENABLE_CARPLAY -> {
156
+ setCarPlayEnabled(this, true)
157
+ startCarPlayObserverInternal()
158
+ }
159
+ ACTION_DISABLE_CARPLAY -> {
160
+ setCarPlayEnabled(this, false)
161
+ stopCarPlayObserverInternal()
162
+ // If the service is only alive for CarPlay (no beacon monitoring),
163
+ // shut it down so we don't keep an unnecessary foreground notification.
164
+ if (!isMonitoringActive(this)) {
165
+ stopSelf()
166
+ return START_NOT_STICKY
167
+ }
168
+ }
169
+ }
137
170
  if (serviceConnected) {
138
171
  // Already bound from a prior onStartCommand — reload regions directly
139
172
  // so that re-starting monitoring from JS always takes effect.
140
173
  loadAndMonitorRegions()
141
- } else {
174
+ } else if (isMonitoringActive(this)) {
175
+ // Only bind to AltBeacon when beacon monitoring is active.
176
+ // CarPlay-only mode keeps the service alive without scanning.
142
177
  beaconManager.bind(this)
143
178
  }
144
179
  return START_STICKY
@@ -765,6 +800,64 @@ class BeaconForegroundService : Service(), BeaconConsumer {
765
800
  }
766
801
  }
767
802
 
803
+ // MARK: - CarPlay observer (service-hosted)
804
+
805
+ /**
806
+ * Lazily instantiate and start the CarPlay observer. Idempotent —
807
+ * `CarPlayMonitor.start` itself is safe to call multiple times.
808
+ * Must be called from any thread; the monitor hops to main internally.
809
+ */
810
+ private fun startCarPlayObserverInternal() {
811
+ val monitor = carPlayMonitor ?: try {
812
+ CarPlayMonitor(applicationContext).also { carPlayMonitor = it }
813
+ } catch (e: Throwable) {
814
+ Log.w(TAG, "Failed to create CarPlayMonitor", e)
815
+ return
816
+ }
817
+ try {
818
+ monitor.start { eventName, payload ->
819
+ emitCarPlayEvent(eventName, payload)
820
+ }
821
+ } catch (e: Throwable) {
822
+ Log.w(TAG, "Failed to start CarPlayMonitor", e)
823
+ }
824
+ }
825
+
826
+ private fun stopCarPlayObserverInternal() {
827
+ carPlayMonitor?.stop()
828
+ // Keep the instance — it's idempotent, and recreating costs nothing
829
+ // beyond the `CarConnection` LiveData wrapper. Setting null here is
830
+ // also safe but unnecessary.
831
+ }
832
+
833
+ /**
834
+ * Fan out a CarPlay event to all sinks: SQLite log, remote API forwarder,
835
+ * native plugin registry, and (best-effort) the live JS bridge. Runs from
836
+ * the main thread (CarPlayMonitor's emit hop).
837
+ */
838
+ private fun emitCarPlayEvent(eventName: String, payload: Map<String, Any?>) {
839
+ // SQLite log (only if event logging is enabled).
840
+ try {
841
+ if (BeaconEventLogger.isLoggingEnabled(this)) {
842
+ val identifier = payload["identifier"] as? String
843
+ getOrCreateEventLogger().logEvent(eventName, identifier, payload)
844
+ }
845
+ } catch (e: Throwable) {
846
+ Log.w(TAG, "CarPlay log write failed", e)
847
+ }
848
+ // Remote API forwarder (no-op if unconfigured).
849
+ try { apiForwarder?.forwardEvent(payload) } catch (_: Throwable) {}
850
+ // Native plugin registry (BeaconGeoPlugin etc.).
851
+ when (eventName) {
852
+ "onCarPlayConnected" -> BeaconPluginRegistry.dispatchCarPlayConnected(
853
+ payload["transport"] as? String ?: "unknown"
854
+ )
855
+ "onCarPlayDisconnected" -> BeaconPluginRegistry.dispatchCarPlayDisconnected()
856
+ }
857
+ // Best-effort delivery to the live JS bridge if a module instance is bound.
858
+ try { boundModule?.get()?.forwardCarPlayEventFromService(eventName, payload) } catch (_: Throwable) {}
859
+ }
860
+
768
861
  private fun createNotificationChannel() {
769
862
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
770
863
  val config = readNotificationConfig()
@@ -814,13 +907,20 @@ class BeaconForegroundService : Service(), BeaconConsumer {
814
907
  const val DISTANCE_JUMP_FACTOR = 5.0
815
908
 
816
909
  private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
910
+ private const val PREF_CARPLAY_ENABLED = "expo.beacon.carplay_enabled"
817
911
  private const val EXTRA_RETRY_COUNT = "retryCount"
912
+ /** Intent action: enable CarPlay/Android Auto observation in the foreground service. */
913
+ const val ACTION_ENABLE_CARPLAY = "expo.modules.beacon.ENABLE_CARPLAY"
914
+ /** Intent action: disable CarPlay/Android Auto observation. Stops the service if no other reason to run. */
915
+ const val ACTION_DISABLE_CARPLAY = "expo.modules.beacon.DISABLE_CARPLAY"
818
916
  private const val MAX_STARTFOREGROUND_RETRIES = 3
819
917
  private const val RETRY_DELAY_MS = 10_000L
820
918
  private const val RETRY_SERVICE_REQUEST_CODE = 0x42454143 // "BEAC"
821
919
  /** Minimum milliseconds between consecutive loadAndMonitorRegions() calls. */
822
920
  private const val LOAD_REGIONS_DEBOUNCE_MS = 500L
823
921
  @Volatile private var activeService: BeaconForegroundService? = null
922
+ /** Weak reference to the live ExpoBeaconModule for best-effort JS bridge fan-out. */
923
+ @Volatile private var boundModule: java.lang.ref.WeakReference<ExpoBeaconModule>? = null
824
924
 
825
925
  fun start(context: Context) {
826
926
  context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
@@ -837,6 +937,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
837
937
  fun stop(context: Context) {
838
938
  context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
839
939
  .edit().putBoolean("active", false).apply()
940
+ // Keep the service alive if it's still needed for CarPlay observation;
941
+ // otherwise the user would silently lose CarPlay events when calling
942
+ // stopMonitoring() while CarPlay monitoring was independently enabled.
943
+ if (isCarPlayEnabled(context)) {
944
+ return
945
+ }
840
946
  context.stopService(Intent(context, BeaconForegroundService::class.java))
841
947
  }
842
948
 
@@ -845,6 +951,70 @@ class BeaconForegroundService : Service(), BeaconConsumer {
845
951
  .getBoolean("active", false)
846
952
  }
847
953
 
954
+ // MARK: - CarPlay public API
955
+
956
+ /**
957
+ * Persist whether the user wants CarPlay observation. Cold service starts
958
+ * (e.g. via [BootReceiver]) read this flag in [onCreate] and re-attach
959
+ * the observer automatically.
960
+ */
961
+ internal fun setCarPlayEnabled(context: Context, enabled: Boolean) {
962
+ context.getSharedPreferences(PREF_CARPLAY_ENABLED, Context.MODE_PRIVATE)
963
+ .edit().putBoolean("enabled", enabled).apply()
964
+ }
965
+
966
+ fun isCarPlayEnabled(context: Context): Boolean {
967
+ return context.getSharedPreferences(PREF_CARPLAY_ENABLED, Context.MODE_PRIVATE)
968
+ .getBoolean("enabled", false)
969
+ }
970
+
971
+ /**
972
+ * Enable CarPlay observation. Starts the foreground service if it's not
973
+ * already running so the observer survives app suspension and process death.
974
+ * Idempotent and safe to call from any context.
975
+ */
976
+ fun enableCarPlay(context: Context) {
977
+ setCarPlayEnabled(context, true)
978
+ ensureNotificationChannel(context)
979
+ val intent = Intent(context, BeaconForegroundService::class.java)
980
+ .setAction(ACTION_ENABLE_CARPLAY)
981
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
982
+ context.startForegroundService(intent)
983
+ } else {
984
+ context.startService(intent)
985
+ }
986
+ }
987
+
988
+ /**
989
+ * Disable CarPlay observation. The foreground service stops itself
990
+ * if no other monitoring reason remains.
991
+ */
992
+ fun disableCarPlay(context: Context) {
993
+ setCarPlayEnabled(context, false)
994
+ val intent = Intent(context, BeaconForegroundService::class.java)
995
+ .setAction(ACTION_DISABLE_CARPLAY)
996
+ // Best-effort: if the service isn't running, sending the intent will
997
+ // start it just to stop it. Skip startForegroundService unless beacon
998
+ // monitoring is also active (otherwise the start might fail with
999
+ // ForegroundServiceStartNotAllowedException on some Android versions).
1000
+ if (activeService != null || isMonitoringActive(context)) {
1001
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1002
+ context.startForegroundService(intent)
1003
+ } else {
1004
+ context.startService(intent)
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * Bind a live module instance for best-effort JS-bridge delivery of
1011
+ * service-emitted events. The reference is weak; pass `null` from the
1012
+ * module's OnDestroy to clear it.
1013
+ */
1014
+ fun bindModule(module: ExpoBeaconModule?) {
1015
+ boundModule = module?.let { java.lang.ref.WeakReference(it) }
1016
+ }
1017
+
848
1018
  fun getMonitoringRuntimeSnapshot(): Map<String, MonitoringRuntimeState> {
849
1019
  return activeService?.snapshotMonitoringRuntimeState() ?: emptyMap()
850
1020
  }
@@ -891,10 +1061,16 @@ class BeaconForegroundService : Service(), BeaconConsumer {
891
1061
  val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
892
1062
  val fgConfig = config.optJSONObject("foregroundService")
893
1063
 
894
- val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() }
895
- ?: "Beacon Monitoring Active"
896
- val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
897
- ?: "Monitoring for iBeacons in the background"
1064
+ // If beacon monitoring is not active and the service is alive only
1065
+ // for CarPlay observation, fall back to a generic notification so
1066
+ // users don't see "Monitoring for iBeacons" when no beacons are being
1067
+ // monitored.
1068
+ val carPlayOnly = !isMonitoringActive(context) && isCarPlayEnabled(context)
1069
+ val defaultTitle = if (carPlayOnly) "Connected device monitoring active" else "Beacon Monitoring Active"
1070
+ val defaultText = if (carPlayOnly) "Monitoring connected vehicle (CarPlay/Android Auto)" else "Monitoring for iBeacons in the background"
1071
+
1072
+ val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() } ?: defaultTitle
1073
+ val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() } ?: defaultText
898
1074
  val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
899
1075
  val iconResId = iconName?.let { name ->
900
1076
  try { context.resources.getIdentifier(name, "drawable", context.packageName).takeIf { it != 0 } }
@@ -921,6 +1097,13 @@ class BeaconForegroundService : Service(), BeaconConsumer {
921
1097
  if (activeService === this) {
922
1098
  activeService = null
923
1099
  }
1100
+ // Stop CarPlay observer first so the LiveData observer doesn't leak
1101
+ // past the service lifecycle. Safe even if it was never started.
1102
+ try {
1103
+ carPlayMonitor?.stop()
1104
+ } catch (_: Throwable) {}
1105
+ carPlayMonitor = null
1106
+ val wasBound = serviceConnected
924
1107
  serviceConnected = false
925
1108
  timeoutHandler.removeCallbacksAndMessages(null)
926
1109
  timeoutRunnables.clear()
@@ -944,7 +1127,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
944
1127
  monitoredRegions.forEach {
945
1128
  try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
946
1129
  }
947
- beaconManager.unbind(this)
1130
+ // Only unbind if we successfully bound — CarPlay-only service
1131
+ // instances skip beaconManager.bind() in onStartCommand.
1132
+ if (wasBound) {
1133
+ try { beaconManager.unbind(this) } catch (_: Throwable) {}
1134
+ }
948
1135
  super.onDestroy()
949
1136
  }
950
1137
 
@@ -29,11 +29,16 @@ class BootReceiver : BroadcastReceiver() {
29
29
  logMemoryKillDiagnostics(context)
30
30
  if (BeaconForegroundService.isMonitoringActive(context)) {
31
31
  tryStartService(context)
32
+ } else if (BeaconForegroundService.isCarPlayEnabled(context)) {
33
+ // CarPlay-only mode: re-attach the observer so it survives reboot.
34
+ tryEnableCarPlay(context)
32
35
  }
33
36
  }
34
37
  ACTION_RETRY_MONITORING -> {
35
38
  if (BeaconForegroundService.isMonitoringActive(context)) {
36
39
  tryStartService(context)
40
+ } else if (BeaconForegroundService.isCarPlayEnabled(context)) {
41
+ tryEnableCarPlay(context)
37
42
  }
38
43
  }
39
44
  }
@@ -54,6 +59,16 @@ class BootReceiver : BroadcastReceiver() {
54
59
  }
55
60
  }
56
61
 
62
+ private fun tryEnableCarPlay(context: Context) {
63
+ try {
64
+ BeaconForegroundService.enableCarPlay(context)
65
+ Log.d(TAG, "BootReceiver: CarPlay-only service started successfully")
66
+ } catch (e: Exception) {
67
+ Log.e(TAG, "BootReceiver: Failed to start CarPlay-only service — retrying in ${RETRY_DELAY_MS}ms", e)
68
+ scheduleRetry(context)
69
+ }
70
+ }
71
+
57
72
  private fun scheduleRetry(context: Context) {
58
73
  val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
59
74
  val retryIntent = Intent(context, BootReceiver::class.java).apply {
@@ -82,9 +82,6 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
82
82
  private var eventLogger: BeaconEventLogger? = null
83
83
  @Volatile private var loggingEnabled = false
84
84
 
85
- // CarPlay / Android Auto monitor (lazy because it requires reactContext)
86
- private var carPlayMonitor: CarPlayMonitor? = null
87
-
88
85
  override fun definition() = ModuleDefinition {
89
86
  Name("ExpoBeacon")
90
87
 
@@ -379,6 +376,10 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
379
376
  sendEvent("onBeaconError", mapOf("identifier" to "", "code" to "SERVICE_START_FAILED", "message" to "Failed to start monitoring service: ${e.message}"))
380
377
  return@AsyncFunction
381
378
  }
379
+ // Auto-enable CarPlay observation alongside beacon monitoring so
380
+ // the service captures CarPlay/Android Auto events for the same
381
+ // lifetime. Users can opt out at any time via stopCarPlayMonitoring().
382
+ try { BeaconForegroundService.enableCarPlay(ctx) } catch (_: Throwable) {}
382
383
  promise.resolve(null)
383
384
  }
384
385
 
@@ -462,6 +463,11 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
462
463
  loggingEnabled = false
463
464
  }
464
465
 
466
+ Function("isEventLoggingEnabled") {
467
+ val ctx = appContext.reactContext ?: return@Function false
468
+ BeaconEventLogger.isLoggingEnabled(ctx)
469
+ }
470
+
465
471
  Function("getEventLogs") { options: Map<String, Any?>? ->
466
472
  val logger = getOrCreateEventLogger() ?: return@Function emptyList<Map<String, Any?>>()
467
473
  val limit = (options?.get("limit") as? Number)?.toInt() ?: 1000
@@ -574,10 +580,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
574
580
  return@AsyncFunction
575
581
  }
576
582
  try {
577
- val monitor = carPlayMonitor ?: CarPlayMonitor(ctx).also { carPlayMonitor = it }
578
- monitor.start { eventName, payload ->
579
- emitCarPlayEvent(eventName, payload)
580
- }
583
+ BeaconForegroundService.enableCarPlay(ctx)
581
584
  promise.resolve(null)
582
585
  } catch (e: Throwable) {
583
586
  promise.reject("CARPLAY_START_FAILED", "Failed to start CarPlay monitoring: ${e.message}", e)
@@ -585,15 +588,35 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
585
588
  }
586
589
 
587
590
  AsyncFunction("stopCarPlayMonitoring") { promise: Promise ->
588
- carPlayMonitor?.stop()
589
- promise.resolve(null)
591
+ val ctx = appContext.reactContext
592
+ if (ctx == null) {
593
+ promise.reject("NO_CONTEXT", "React context is not available", null)
594
+ return@AsyncFunction
595
+ }
596
+ try {
597
+ BeaconForegroundService.disableCarPlay(ctx)
598
+ promise.resolve(null)
599
+ } catch (e: Throwable) {
600
+ promise.reject("CARPLAY_STOP_FAILED", "Failed to stop CarPlay monitoring: ${e.message}", e)
601
+ }
602
+ }
603
+
604
+ Function("isCarPlayMonitoringEnabled") {
605
+ val ctx = appContext.reactContext ?: return@Function false
606
+ BeaconForegroundService.isCarPlayEnabled(ctx)
607
+ }
608
+
609
+ OnCreate {
610
+ // Register this module instance for best-effort JS-bridge fan-out
611
+ // of CarPlay events emitted by the foreground service. The service
612
+ // holds a weak reference; cleared in OnDestroy.
613
+ BeaconForegroundService.bindModule(this@ExpoBeaconModule)
590
614
  }
591
615
 
592
616
  OnDestroy {
593
617
  with(this@ExpoBeaconModule) {
618
+ BeaconForegroundService.bindModule(null)
594
619
  unregisterEventReceiver()
595
- carPlayMonitor?.stop()
596
- carPlayMonitor = null
597
620
  loggingEnabled = false
598
621
  eventLogger?.close()
599
622
  eventLogger = null
@@ -1038,22 +1061,12 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
1038
1061
  }
1039
1062
 
1040
1063
  /**
1041
- * Emit a CarPlay/Android Auto event to JS, log it to SQLite, forward to the
1042
- * configured remote API, and dispatch to registered native plugins.
1064
+ * Called by [BeaconForegroundService] (via weak reference) when it emits a
1065
+ * CarPlay event from its own observer. Best-effort delivery to the JS bridge
1066
+ * — the service has already handled SQLite logging, API forwarding, and
1067
+ * native plugin dispatch, so this method only needs to relay to JS.
1043
1068
  */
1044
- private fun emitCarPlayEvent(eventName: String, payload: Map<String, Any?>) {
1045
- sendEvent(eventName, payload)
1046
- logBeaconEvent(eventName, payload)
1047
- appContext.reactContext?.let { ctx ->
1048
- try {
1049
- BeaconApiForwarder(ctx).forwardEvent(payload)
1050
- } catch (_: Throwable) { /* best-effort */ }
1051
- }
1052
- when (eventName) {
1053
- "onCarPlayConnected" -> BeaconPluginRegistry.dispatchCarPlayConnected(
1054
- payload["transport"] as? String ?: "unknown"
1055
- )
1056
- "onCarPlayDisconnected" -> BeaconPluginRegistry.dispatchCarPlayDisconnected()
1057
- }
1069
+ fun forwardCarPlayEventFromService(eventName: String, payload: Map<String, Any?>) {
1070
+ try { sendEvent(eventName, payload) } catch (_: Throwable) { /* JS bridge may be torn down */ }
1058
1071
  }
1059
1072
  }
@@ -93,6 +93,11 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
93
93
  enableEventLogging(): void;
94
94
  /** Disable event logging. Previously logged events are retained. */
95
95
  disableEventLogging(): void;
96
+ /**
97
+ * Returns whether SQLite event logging is currently enabled.
98
+ * Reads the persisted flag, so this stays accurate across app cold-starts.
99
+ */
100
+ isEventLoggingEnabled(): boolean;
96
101
  /**
97
102
  * Retrieve logged beacon events from the SQLite database.
98
103
  * @param options Optional filters (limit, eventType, sinceTimestamp).
@@ -143,7 +148,22 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
143
148
  * endpoint, and dispatched to native lifecycle plugins (used by the auto-
144
149
  * generated geolocation plugin to start/stop background-geolocation).
145
150
  *
146
- * Safe to call multiple times subsequent calls are no-ops.
151
+ * **Persistent.** The enabled state is stored in native preferences and
152
+ * survives app kill / device reboot. Call `stopCarPlayMonitoring()` to
153
+ * disable. `startMonitoring()` also enables CarPlay observation
154
+ * automatically — calling this method explicitly is only required if you
155
+ * want CarPlay events without beacon monitoring.
156
+ *
157
+ * **Background behavior:**
158
+ * - **Android**: the foreground service hosts the observer and continues to
159
+ * receive `CarConnection` events even after the app process is killed and
160
+ * restarted via `BootReceiver`. Guaranteed background detection.
161
+ * - **iOS**: the observer auto-restarts in `OnCreate` whenever the module
162
+ * is recreated, including background-launches triggered by beacon region
163
+ * monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for
164
+ * guaranteed wake-from-suspension, also call `startMonitoring()` with at
165
+ * least one paired beacon. Region wake events trigger a CarPlay state
166
+ * resync to reconcile any route changes that occurred during suspension.
147
167
  *
148
168
  * - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.
149
169
  * No CarPlay entitlement required.
@@ -152,8 +172,21 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
152
172
  * - Web: no-op (resolves immediately).
153
173
  */
154
174
  startCarPlayMonitoring(): Promise<void>;
155
- /** Stop CarPlay / Android Auto connection monitoring started by `startCarPlayMonitoring()`. */
175
+ /**
176
+ * Stop CarPlay / Android Auto connection monitoring and clear the persisted
177
+ * "enabled" flag so the observer is not auto-restarted on next launch / boot.
178
+ *
179
+ * On Android, if no beacon monitoring is active, the foreground service
180
+ * stops itself.
181
+ */
156
182
  stopCarPlayMonitoring(): Promise<void>;
183
+ /**
184
+ * Returns whether CarPlay / Android Auto observation is currently enabled.
185
+ * Reads the persisted flag, so the value survives app cold-starts and
186
+ * reflects the native source of truth (foreground service on Android,
187
+ * UserDefaults suite on iOS).
188
+ */
189
+ isCarPlayMonitoringEnabled(): boolean;
157
190
  }
158
191
  declare const _default: ExpoBeaconModule;
159
192
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAsNzD,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 MonitoringConfig,\r\n MonitoredDeviceState,\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-CSFR-Token header.\r\n * @param id Optional identifier appended to every forwarded event payload.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string, id?: string): void;\r\n\r\n /**\r\n * Return the current monitoring configuration and active state.\r\n * Option fields are undefined if not explicitly set.\r\n */\r\n getMonitoringConfig(): MonitoringConfig;\r\n\r\n /**\r\n * Return the current state snapshot for a paired monitored device.\r\n * Returns null when no paired device matches the identifier.\r\n */\r\n getMonitoredDeviceState(identifier: string): MonitoredDeviceState | null;\r\n\r\n /**\r\n * Return the current state snapshot for all paired monitored devices.\r\n */\r\n getMonitoredDeviceStates(): MonitoredDeviceState[];\r\n\r\n /**\r\n * Return the current API forwarding configuration.\r\n * Each field is `null` if not set.\r\n */\r\n getApiEndpoint(): { url: string | null; apiKey: string | null; id: string | null };\r\n\r\n /**\r\n * Start observing CarPlay (iOS) / Android Auto (Android) connection state.\r\n *\r\n * Emits `onCarPlayConnected` and `onCarPlayDisconnected` JS events. Events are\r\n * also written to the SQLite event log and forwarded to the configured API\r\n * endpoint, and dispatched to native lifecycle plugins (used by the auto-\r\n * generated geolocation plugin to start/stop background-geolocation).\r\n *\r\n * Safe to call multiple timessubsequent calls are no-ops.\r\n *\r\n * - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.\r\n * No CarPlay entitlement required.\r\n * - Android: observes `androidx.car.app.connection.CarConnection` LiveData.\r\n * No Android Auto certification required.\r\n * - Web: no-op (resolves immediately).\r\n */\r\n startCarPlayMonitoring(): Promise<void>;\r\n\r\n /** Stop CarPlay / Android Auto connection monitoring started by `startCarPlayMonitoring()`. */\r\n stopCarPlayMonitoring(): Promise<void>;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
1
+ {"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAyPzD,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 MonitoringConfig,\r\n MonitoredDeviceState,\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 * Returns whether SQLite event logging is currently enabled.\r\n * Reads the persisted flag, so this stays accurate across app cold-starts.\r\n */\r\n isEventLoggingEnabled(): boolean;\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-CSFR-Token header.\r\n * @param id Optional identifier appended to every forwarded event payload.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string, id?: string): void;\r\n\r\n /**\r\n * Return the current monitoring configuration and active state.\r\n * Option fields are undefined if not explicitly set.\r\n */\r\n getMonitoringConfig(): MonitoringConfig;\r\n\r\n /**\r\n * Return the current state snapshot for a paired monitored device.\r\n * Returns null when no paired device matches the identifier.\r\n */\r\n getMonitoredDeviceState(identifier: string): MonitoredDeviceState | null;\r\n\r\n /**\r\n * Return the current state snapshot for all paired monitored devices.\r\n */\r\n getMonitoredDeviceStates(): MonitoredDeviceState[];\r\n\r\n /**\r\n * Return the current API forwarding configuration.\r\n * Each field is `null` if not set.\r\n */\r\n getApiEndpoint(): { url: string | null; apiKey: string | null; id: string | null };\r\n\r\n /**\r\n * Start observing CarPlay (iOS) / Android Auto (Android) connection state.\r\n *\r\n * Emits `onCarPlayConnected` and `onCarPlayDisconnected` JS events. Events are\r\n * also written to the SQLite event log and forwarded to the configured API\r\n * endpoint, and dispatched to native lifecycle plugins (used by the auto-\r\n * generated geolocation plugin to start/stop background-geolocation).\r\n *\r\n * **Persistent.** The enabled state is stored in native preferences and\r\n * survives app kill / device reboot. Call `stopCarPlayMonitoring()` to\r\n * disable. `startMonitoring()` also enables CarPlay observation\r\n * automatically calling this method explicitly is only required if you\r\n * want CarPlay events without beacon monitoring.\r\n *\r\n * **Background behavior:**\r\n * - **Android**: the foreground service hosts the observer and continues to\r\n * receive `CarConnection` events even after the app process is killed and\r\n * restarted via `BootReceiver`. Guaranteed background detection.\r\n * - **iOS**: the observer auto-restarts in `OnCreate` whenever the module\r\n * is recreated, including background-launches triggered by beacon region\r\n * monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for\r\n * guaranteed wake-from-suspension, also call `startMonitoring()` with at\r\n * least one paired beacon. Region wake events trigger a CarPlay state\r\n * resync to reconcile any route changes that occurred during suspension.\r\n *\r\n * - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.\r\n * No CarPlay entitlement required.\r\n * - Android: observes `androidx.car.app.connection.CarConnection` LiveData.\r\n * No Android Auto certification required.\r\n * - Web: no-op (resolves immediately).\r\n */\r\n startCarPlayMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Stop CarPlay / Android Auto connection monitoring and clear the persisted\r\n * \"enabled\" flag so the observer is not auto-restarted on next launch / boot.\r\n *\r\n * On Android, if no beacon monitoring is active, the foreground service\r\n * stops itself.\r\n */\r\n stopCarPlayMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Returns whether CarPlay / Android Auto observation is currently enabled.\r\n * Reads the persisted flag, so the value survives app cold-starts and\r\n * reflects the native source of truth (foreground service on Android,\r\n * UserDefaults suite on iOS).\r\n */\r\n isCarPlayMonitoringEnabled(): boolean;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
@@ -17,6 +17,7 @@ declare const stub: {
17
17
  requestPermissionsAsync: () => Promise<boolean>;
18
18
  enableEventLogging: () => void;
19
19
  disableEventLogging: () => void;
20
+ isEventLoggingEnabled: () => boolean;
20
21
  getEventLogs: (_options?: EventLogQueryOptions) => EventLogEntry[];
21
22
  clearEventLogs: () => void;
22
23
  destroyEventLogs: () => void;
@@ -32,6 +33,7 @@ declare const stub: {
32
33
  requestBatteryOptimizationExemption: () => Promise<boolean>;
33
34
  startCarPlayMonitoring: () => Promise<void>;
34
35
  stopCarPlayMonitoring: () => Promise<void>;
36
+ isCarPlayMonitoringEnabled: () => boolean;
35
37
  addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => {
36
38
  remove: () => void;
37
39
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,QAAA,MAAM,IAAI;kCAEE,MAAM,EAAE,kBACA,MAAM,KACrB,OAAO,CAAC,gBAAgB,EAAE,CAAC;6CAEZ,MAAM,KACrB,OAAO,CAAC,mBAAmB,EAAE,CAAC;8BAElB,MAAM,SACZ,MAAM,UACL,MAAM,UACN,MAAM,KACb,IAAI;gCACqB,MAAM,KAAG,IAAI;4BACnB,YAAY,EAAE;iCAErB,MAAM,cACP,MAAM,aACP,MAAM,KAChB,IAAI;mCACwB,MAAM,KAAG,IAAI;+BACnB,eAAe,EAAE;2BACrB,OAAO,CAAC,IAAI,CAAC;0BACd,OAAO,CAAC,IAAI,CAAC;+BACR,IAAI;8BACL,IAAI;sBACZ,IAAI;qCACa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAAI;mCAClC,OAAO,CAAC,OAAO,CAAC;8BACrB,IAAI;+BACH,IAAI;8BACH,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;;2CAEa,MAAM,KAAG,oBAAoB,GAAG,IAAI;oCAC7C,oBAAoB,EAAE;0BAChC;QAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;uCACnD,OAAO;+CACC,OAAO,CAAC,OAAO,CAAC;kCAC7B,OAAO,CAAC,IAAI,CAAC;iCACd,OAAO,CAAC,IAAI,CAAC;8BACd,MAAM,sBAAsB,aAAa,GAAG;;;qCAGrC,MAAM,sBAAsB;CAC9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"ExpoBeaconModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,QAAA,MAAM,IAAI;kCAEE,MAAM,EAAE,kBACA,MAAM,KACrB,OAAO,CAAC,gBAAgB,EAAE,CAAC;6CAEZ,MAAM,KACrB,OAAO,CAAC,mBAAmB,EAAE,CAAC;8BAElB,MAAM,SACZ,MAAM,UACL,MAAM,UACN,MAAM,KACb,IAAI;gCACqB,MAAM,KAAG,IAAI;4BACnB,YAAY,EAAE;iCAErB,MAAM,cACP,MAAM,aACP,MAAM,KAChB,IAAI;mCACwB,MAAM,KAAG,IAAI;+BACnB,eAAe,EAAE;2BACrB,OAAO,CAAC,IAAI,CAAC;0BACd,OAAO,CAAC,IAAI,CAAC;+BACR,IAAI;8BACL,IAAI;sBACZ,IAAI;qCACa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAAI;mCAClC,OAAO,CAAC,OAAO,CAAC;8BACrB,IAAI;+BACH,IAAI;iCACF,OAAO;8BACR,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;;2CAEa,MAAM,KAAG,oBAAoB,GAAG,IAAI;oCAC7C,oBAAoB,EAAE;0BAChC;QAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;uCACnD,OAAO;+CACC,OAAO,CAAC,OAAO,CAAC;kCAC7B,OAAO,CAAC,IAAI,CAAC;iCACd,OAAO,CAAC,IAAI,CAAC;sCACR,OAAO;8BACb,MAAM,sBAAsB,aAAa,GAAG;;;qCAGrC,MAAM,sBAAsB;CAC9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -19,6 +19,7 @@ const stub = {
19
19
  requestPermissionsAsync: () => notSupported(),
20
20
  enableEventLogging: () => notSupported(),
21
21
  disableEventLogging: () => notSupported(),
22
+ isEventLoggingEnabled: () => false,
22
23
  getEventLogs: (_options) => notSupported(),
23
24
  clearEventLogs: () => notSupported(),
24
25
  destroyEventLogs: () => notSupported(),
@@ -30,6 +31,7 @@ const stub = {
30
31
  requestBatteryOptimizationExemption: () => Promise.resolve(true),
31
32
  startCarPlayMonitoring: () => Promise.resolve(),
32
33
  stopCarPlayMonitoring: () => Promise.resolve(),
34
+ isCarPlayMonitoringEnabled: () => false,
33
35
  addListener: (_eventName, _listener) => ({
34
36
  remove: () => { },
35
37
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAWA,MAAM,YAAY,GAAG,GAAU,EAAE;IAC/B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG;IACX,mBAAmB,EAAE,CACnB,MAAgB,EAChB,aAAsB,EACO,EAAE,CAAC,YAAY,EAAE;IAChD,sBAAsB,EAAE,CACtB,aAAsB,EACU,EAAE,CAAC,YAAY,EAAE;IACnD,UAAU,EAAE,CACV,WAAmB,EACnB,KAAa,EACb,MAAc,EACd,MAAc,EACR,EAAE,CAAC,YAAY,EAAE;IACzB,YAAY,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC3D,gBAAgB,EAAE,GAAmB,EAAE,CAAC,YAAY,EAAE;IACtD,aAAa,EAAE,CACb,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACX,EAAE,CAAC,YAAY,EAAE;IACzB,eAAe,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC9D,mBAAmB,EAAE,GAAsB,EAAE,CAAC,YAAY,EAAE;IAC5D,eAAe,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACpD,cAAc,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACnD,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,UAAU,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IACtC,qBAAqB,EAAE,CAAC,OAAgC,EAAQ,EAAE,CAAC,YAAY,EAAE;IACjF,uBAAuB,EAAE,GAAqB,EAAE,CAAC,YAAY,EAAE;IAC/D,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,YAAY,EAAE,CAAC,QAA+B,EAAmB,EAAE,CAAC,YAAY,EAAE;IAClF,cAAc,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC1C,gBAAgB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC5C,mBAAmB,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACzC,uBAAuB,EAAE,CAAC,WAAmB,EAA+B,EAAE,CAAC,YAAY,EAAE;IAC7F,wBAAwB,EAAE,GAA2B,EAAE,CAAC,YAAY,EAAE;IACtE,cAAc,EAAE,GAAqE,EAAE,CAAC,YAAY,EAAE;IACtG,2BAA2B,EAAE,GAAY,EAAE,CAAC,IAAI;IAChD,mCAAmC,EAAE,GAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IAClF,sBAAsB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC9D,qBAAqB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC7D,WAAW,EAAE,CAAC,UAAwC,EAAE,SAAc,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IACF,kBAAkB,EAAE,CAAC,UAAwC,EAAE,EAAE,GAAE,CAAC;CACrE,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n MonitoredDeviceState,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\nconst notSupported = (): never => {\r\n throw new Error(\"expo-beacon is not supported on web.\");\r\n};\r\n\r\nconst stub = {\r\n scanForBeaconsAsync: (\r\n _uuids: string[],\r\n _scanDuration?: number,\r\n ): Promise<BeaconScanResult[]> => notSupported(),\r\n scanForEddystonesAsync: (\r\n _scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]> => notSupported(),\r\n pairBeacon: (\r\n _identifier: string,\r\n _uuid: string,\r\n _major: number,\r\n _minor: number,\r\n ): void => notSupported(),\r\n unpairBeacon: (_identifier: string): void => notSupported(),\r\n getPairedBeacons: (): PairedBeacon[] => notSupported(),\r\n pairEddystone: (\r\n _identifier: string,\r\n _namespace: string,\r\n _instance: string,\r\n ): void => notSupported(),\r\n unpairEddystone: (_identifier: string): void => notSupported(),\r\n getPairedEddystones: (): PairedEddystone[] => notSupported(),\r\n startMonitoring: (): Promise<void> => notSupported(),\r\n stopMonitoring: (): Promise<void> => notSupported(),\r\n startContinuousScan: (): void => notSupported(),\r\n stopContinuousScan: (): void => notSupported(),\r\n cancelScan: (): void => notSupported(),\r\n setNotificationConfig: (_config: Record<string, unknown>): void => notSupported(),\r\n requestPermissionsAsync: (): Promise<boolean> => notSupported(),\r\n enableEventLogging: (): void => notSupported(),\r\n disableEventLogging: (): void => notSupported(),\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n getMonitoringConfig: () => notSupported(),\r\n getMonitoredDeviceState: (_identifier: string): MonitoredDeviceState | null => notSupported(),\r\n getMonitoredDeviceStates: (): MonitoredDeviceState[] => notSupported(),\r\n getApiEndpoint: (): { url: string | null; apiKey: string | null; id: string | null } => notSupported(),\r\n isBatteryOptimizationExempt: (): boolean => true,\r\n requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),\r\n startCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n stopCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({\r\n remove: () => {},\r\n }),\r\n removeAllListeners: (_eventName: keyof ExpoBeaconModuleEvents) => {},\r\n};\r\n\r\nexport default stub;\r\n"]}
1
+ {"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAWA,MAAM,YAAY,GAAG,GAAU,EAAE;IAC/B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG;IACX,mBAAmB,EAAE,CACnB,MAAgB,EAChB,aAAsB,EACO,EAAE,CAAC,YAAY,EAAE;IAChD,sBAAsB,EAAE,CACtB,aAAsB,EACU,EAAE,CAAC,YAAY,EAAE;IACnD,UAAU,EAAE,CACV,WAAmB,EACnB,KAAa,EACb,MAAc,EACd,MAAc,EACR,EAAE,CAAC,YAAY,EAAE;IACzB,YAAY,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC3D,gBAAgB,EAAE,GAAmB,EAAE,CAAC,YAAY,EAAE;IACtD,aAAa,EAAE,CACb,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACX,EAAE,CAAC,YAAY,EAAE;IACzB,eAAe,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC9D,mBAAmB,EAAE,GAAsB,EAAE,CAAC,YAAY,EAAE;IAC5D,eAAe,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACpD,cAAc,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACnD,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,UAAU,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IACtC,qBAAqB,EAAE,CAAC,OAAgC,EAAQ,EAAE,CAAC,YAAY,EAAE;IACjF,uBAAuB,EAAE,GAAqB,EAAE,CAAC,YAAY,EAAE;IAC/D,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,qBAAqB,EAAE,GAAY,EAAE,CAAC,KAAK;IAC3C,YAAY,EAAE,CAAC,QAA+B,EAAmB,EAAE,CAAC,YAAY,EAAE;IAClF,cAAc,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC1C,gBAAgB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC5C,mBAAmB,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACzC,uBAAuB,EAAE,CAAC,WAAmB,EAA+B,EAAE,CAAC,YAAY,EAAE;IAC7F,wBAAwB,EAAE,GAA2B,EAAE,CAAC,YAAY,EAAE;IACtE,cAAc,EAAE,GAAqE,EAAE,CAAC,YAAY,EAAE;IACtG,2BAA2B,EAAE,GAAY,EAAE,CAAC,IAAI;IAChD,mCAAmC,EAAE,GAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IAClF,sBAAsB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC9D,qBAAqB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC7D,0BAA0B,EAAE,GAAY,EAAE,CAAC,KAAK;IAChD,WAAW,EAAE,CAAC,UAAwC,EAAE,SAAc,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IACF,kBAAkB,EAAE,CAAC,UAAwC,EAAE,EAAE,GAAE,CAAC;CACrE,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n MonitoredDeviceState,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\nconst notSupported = (): never => {\r\n throw new Error(\"expo-beacon is not supported on web.\");\r\n};\r\n\r\nconst stub = {\r\n scanForBeaconsAsync: (\r\n _uuids: string[],\r\n _scanDuration?: number,\r\n ): Promise<BeaconScanResult[]> => notSupported(),\r\n scanForEddystonesAsync: (\r\n _scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]> => notSupported(),\r\n pairBeacon: (\r\n _identifier: string,\r\n _uuid: string,\r\n _major: number,\r\n _minor: number,\r\n ): void => notSupported(),\r\n unpairBeacon: (_identifier: string): void => notSupported(),\r\n getPairedBeacons: (): PairedBeacon[] => notSupported(),\r\n pairEddystone: (\r\n _identifier: string,\r\n _namespace: string,\r\n _instance: string,\r\n ): void => notSupported(),\r\n unpairEddystone: (_identifier: string): void => notSupported(),\r\n getPairedEddystones: (): PairedEddystone[] => notSupported(),\r\n startMonitoring: (): Promise<void> => notSupported(),\r\n stopMonitoring: (): Promise<void> => notSupported(),\r\n startContinuousScan: (): void => notSupported(),\r\n stopContinuousScan: (): void => notSupported(),\r\n cancelScan: (): void => notSupported(),\r\n setNotificationConfig: (_config: Record<string, unknown>): void => notSupported(),\r\n requestPermissionsAsync: (): Promise<boolean> => notSupported(),\r\n enableEventLogging: (): void => notSupported(),\r\n disableEventLogging: (): void => notSupported(),\r\n isEventLoggingEnabled: (): boolean => false,\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n getMonitoringConfig: () => notSupported(),\r\n getMonitoredDeviceState: (_identifier: string): MonitoredDeviceState | null => notSupported(),\r\n getMonitoredDeviceStates: (): MonitoredDeviceState[] => notSupported(),\r\n getApiEndpoint: (): { url: string | null; apiKey: string | null; id: string | null } => notSupported(),\r\n isBatteryOptimizationExempt: (): boolean => true,\r\n requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),\r\n startCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n stopCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n isCarPlayMonitoringEnabled: (): boolean => false,\r\n addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({\r\n remove: () => {},\r\n }),\r\n removeAllListeners: (_eventName: keyof ExpoBeaconModuleEvents) => {},\r\n};\r\n\r\nexport default stub;\r\n"]}
@@ -67,6 +67,27 @@ final class CarPlayMonitor {
67
67
  }
68
68
  }
69
69
 
70
+ /// Re-read the current audio route and emit a connect/disconnect event if
71
+ /// the state has changed since the last observed value. Cheap and idempotent;
72
+ /// safe to call from any background-wake hook (e.g. CLLocationManager region
73
+ /// callbacks) to reconcile CarPlay state changes that occurred while the app
74
+ /// was suspended and `AVAudioSession.routeChangeNotification` was not delivered.
75
+ func resyncIfNeeded() {
76
+ queue.async { [weak self] in
77
+ self?.handleRouteChange()
78
+ }
79
+ }
80
+
81
+ /// Whether `start(emit:)` has been called and `stop()` has not.
82
+ /// Use to decide whether to skip a redundant start during auto-start logic.
83
+ var isObserving: Bool {
84
+ // Synchronous read from main queue is safe — `observer` is only mutated on `queue`.
85
+ if Thread.isMainThread {
86
+ return observer != nil
87
+ }
88
+ return queue.sync { observer != nil }
89
+ }
90
+
70
91
  private func handleRouteChange() {
71
92
  let (connected, transport) = Self.currentCarPlayState()
72
93
  if connected == isConnected { return }
@@ -14,6 +14,7 @@ private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
14
14
  private let MIN_RSSI_KEY = "expo.beacon.min_rssi"
15
15
  private let EVENT_LEVEL_KEY = "expo.beacon.event_level"
16
16
  private let EXIT_TIMEOUT_SECONDS_KEY = "expo.beacon.exit_timeout_seconds"
17
+ private let CARPLAY_MONITORING_ENABLED_KEY = "expo.beacon.carplay_monitoring_enabled"
17
18
 
18
19
  /// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
19
20
  private let DEFAULT_MIN_RSSI: Int = -85
@@ -150,6 +151,14 @@ public class ExpoBeaconModule: Module {
150
151
 
151
152
  OnCreate {
152
153
  self.migrateUserDefaultsIfNeeded()
154
+ // If the user previously enabled CarPlay monitoring, restart it now —
155
+ // the module may have been recreated after the app was killed and
156
+ // background-launched (e.g. via a CLLocationManager region wake).
157
+ // Without this, CarPlay state changes that happened during suspension
158
+ // would be missed until JS calls startCarPlayMonitoring() again.
159
+ if self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
160
+ self.startCarPlayMonitoringInternal()
161
+ }
153
162
  }
154
163
 
155
164
  Events("onBeaconEnter", "onBeaconExit", "onBeaconDistance", "onBeaconTimeout", "onBeaconFound", "onEddystoneFound", "onEddystoneEnter", "onEddystoneExit", "onEddystoneDistance", "onEddystoneTimeout", "onBeaconError", "onCarPlayConnected", "onCarPlayDisconnected")
@@ -432,6 +441,11 @@ public class ExpoBeaconModule: Module {
432
441
  }
433
442
  self.requestNotificationPermission()
434
443
  self.startRegionMonitoring()
444
+ // Auto-enable CarPlay monitoring when beacon monitoring starts so
445
+ // CarPlay events are captured for the same lifetime as beacons.
446
+ // Users can opt out at any time via stopCarPlayMonitoring().
447
+ self.defaults.set(true, forKey: CARPLAY_MONITORING_ENABLED_KEY)
448
+ self.startCarPlayMonitoringInternal()
435
449
  promise.resolve(nil)
436
450
  }
437
451
  }
@@ -458,17 +472,21 @@ public class ExpoBeaconModule: Module {
458
472
  // MARK: - CarPlay
459
473
 
460
474
  AsyncFunction("startCarPlayMonitoring") { (promise: Promise) in
461
- CarPlayMonitor.shared.start { [weak self] eventName, payload in
462
- self?.sendLoggedEvent(eventName, payload)
463
- }
475
+ self.defaults.set(true, forKey: CARPLAY_MONITORING_ENABLED_KEY)
476
+ self.startCarPlayMonitoringInternal()
464
477
  promise.resolve(nil)
465
478
  }
466
479
 
467
480
  AsyncFunction("stopCarPlayMonitoring") { (promise: Promise) in
481
+ self.defaults.set(false, forKey: CARPLAY_MONITORING_ENABLED_KEY)
468
482
  CarPlayMonitor.shared.stop()
469
483
  promise.resolve(nil)
470
484
  }
471
485
 
486
+ Function("isCarPlayMonitoringEnabled") { () -> Bool in
487
+ return self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY)
488
+ }
489
+
472
490
  // MARK: - Continuous Scan
473
491
 
474
492
  Function("startContinuousScan") { () -> Void in
@@ -528,6 +546,10 @@ public class ExpoBeaconModule: Module {
528
546
  self.loggingEnabled = false
529
547
  }
530
548
 
549
+ Function("isEventLoggingEnabled") { () -> Bool in
550
+ return self.defaults.bool(forKey: EVENT_LOGGING_ENABLED_KEY)
551
+ }
552
+
531
553
  Function("getEventLogs") { (options: [String: Any]?) -> [[String: Any]] in
532
554
  let logger = self.getOrCreateEventLogger()
533
555
  let limit = (options?["limit"] as? Int) ?? 1000
@@ -613,7 +635,13 @@ public class ExpoBeaconModule: Module {
613
635
  self.eventLogger = nil
614
636
  self.stopRegionMonitoring()
615
637
  self.stopEddystoneMonitoring()
616
- CarPlayMonitor.shared.stop()
638
+ // Only tear down CarPlay observation when the user has explicitly
639
+ // disabled it. Otherwise leave `CarPlayMonitor.shared` running so
640
+ // that route changes continue to be observed across module recreations
641
+ // (e.g. background-launch wake → module re-init → OnDestroy on suspend).
642
+ if !self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
643
+ CarPlayMonitor.shared.stop()
644
+ }
617
645
  self.centralManager?.stopScan()
618
646
  self.centralManager = nil
619
647
  self.scanTimer?.cancel()
@@ -1080,6 +1108,16 @@ public class ExpoBeaconModule: Module {
1080
1108
  return distance
1081
1109
  }
1082
1110
 
1111
+ /// Starts the shared `CarPlayMonitor` and routes its events through the
1112
+ /// standard `sendLoggedEvent` pipeline (JS bridge + SQLite + API forwarder
1113
+ /// + lifecycle plugin registry). Idempotent — safe to call multiple times
1114
+ /// (see `CarPlayMonitor.start(emit:)` semantics).
1115
+ private func startCarPlayMonitoringInternal() {
1116
+ CarPlayMonitor.shared.start { [weak self] eventName, payload in
1117
+ self?.sendLoggedEvent(eventName, payload)
1118
+ }
1119
+ }
1120
+
1083
1121
  /// Sends an event to JS and logs it to SQLite if logging is enabled.
1084
1122
  private func sendLoggedEvent(_ eventName: String, _ params: [String: Any]) {
1085
1123
  if isEventLoggingEnabled() {
@@ -1739,12 +1777,22 @@ public class ExpoBeaconModule: Module {
1739
1777
  }
1740
1778
 
1741
1779
  fileprivate func handleDidEnterRegion(_ region: CLRegion) {
1780
+ // CLLocationManager region events are one of the few iOS background-wake
1781
+ // signals available to this app. Use this opportunity to reconcile
1782
+ // CarPlay state in case it changed during suspension.
1783
+ if defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
1784
+ CarPlayMonitor.shared.resyncIfNeeded()
1785
+ }
1742
1786
  // Region callbacks are suppressed — all enter/exit logic goes through
1743
1787
  // ranging-based hysteresis in handleDidRange for consistent behaviour
1744
1788
  // with ENTER_HYSTERESIS_COUNT / EXIT_HYSTERESIS_COUNT, regardless of whether maxDistance is set.
1745
1789
  }
1746
1790
 
1747
1791
  fileprivate func handleDidExitRegion(_ region: CLRegion) {
1792
+ // Reconcile CarPlay state on background wake — see handleDidEnterRegion.
1793
+ if defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
1794
+ CarPlayMonitor.shared.resyncIfNeeded()
1795
+ }
1748
1796
  guard let beaconRegion = region as? CLBeaconRegion else { return }
1749
1797
  let identifier = beaconRegion.identifier
1750
1798
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -144,6 +144,12 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
144
144
  /** Disable event logging. Previously logged events are retained. */
145
145
  disableEventLogging(): void;
146
146
 
147
+ /**
148
+ * Returns whether SQLite event logging is currently enabled.
149
+ * Reads the persisted flag, so this stays accurate across app cold-starts.
150
+ */
151
+ isEventLoggingEnabled(): boolean;
152
+
147
153
  /**
148
154
  * Retrieve logged beacon events from the SQLite database.
149
155
  * @param options Optional filters (limit, eventType, sinceTimestamp).
@@ -198,7 +204,22 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
198
204
  * endpoint, and dispatched to native lifecycle plugins (used by the auto-
199
205
  * generated geolocation plugin to start/stop background-geolocation).
200
206
  *
201
- * Safe to call multiple times subsequent calls are no-ops.
207
+ * **Persistent.** The enabled state is stored in native preferences and
208
+ * survives app kill / device reboot. Call `stopCarPlayMonitoring()` to
209
+ * disable. `startMonitoring()` also enables CarPlay observation
210
+ * automatically — calling this method explicitly is only required if you
211
+ * want CarPlay events without beacon monitoring.
212
+ *
213
+ * **Background behavior:**
214
+ * - **Android**: the foreground service hosts the observer and continues to
215
+ * receive `CarConnection` events even after the app process is killed and
216
+ * restarted via `BootReceiver`. Guaranteed background detection.
217
+ * - **iOS**: the observer auto-restarts in `OnCreate` whenever the module
218
+ * is recreated, including background-launches triggered by beacon region
219
+ * monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for
220
+ * guaranteed wake-from-suspension, also call `startMonitoring()` with at
221
+ * least one paired beacon. Region wake events trigger a CarPlay state
222
+ * resync to reconcile any route changes that occurred during suspension.
202
223
  *
203
224
  * - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.
204
225
  * No CarPlay entitlement required.
@@ -208,8 +229,22 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
208
229
  */
209
230
  startCarPlayMonitoring(): Promise<void>;
210
231
 
211
- /** Stop CarPlay / Android Auto connection monitoring started by `startCarPlayMonitoring()`. */
232
+ /**
233
+ * Stop CarPlay / Android Auto connection monitoring and clear the persisted
234
+ * "enabled" flag so the observer is not auto-restarted on next launch / boot.
235
+ *
236
+ * On Android, if no beacon monitoring is active, the foreground service
237
+ * stops itself.
238
+ */
212
239
  stopCarPlayMonitoring(): Promise<void>;
240
+
241
+ /**
242
+ * Returns whether CarPlay / Android Auto observation is currently enabled.
243
+ * Reads the persisted flag, so the value survives app cold-starts and
244
+ * reflects the native source of truth (foreground service on Android,
245
+ * UserDefaults suite on iOS).
246
+ */
247
+ isCarPlayMonitoringEnabled(): boolean;
213
248
  }
214
249
 
215
250
  export default requireNativeModule<ExpoBeaconModule>("ExpoBeacon");
@@ -45,6 +45,7 @@ const stub = {
45
45
  requestPermissionsAsync: (): Promise<boolean> => notSupported(),
46
46
  enableEventLogging: (): void => notSupported(),
47
47
  disableEventLogging: (): void => notSupported(),
48
+ isEventLoggingEnabled: (): boolean => false,
48
49
  getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),
49
50
  clearEventLogs: (): void => notSupported(),
50
51
  destroyEventLogs: (): void => notSupported(),
@@ -56,6 +57,7 @@ const stub = {
56
57
  requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),
57
58
  startCarPlayMonitoring: (): Promise<void> => Promise.resolve(),
58
59
  stopCarPlayMonitoring: (): Promise<void> => Promise.resolve(),
60
+ isCarPlayMonitoringEnabled: (): boolean => false,
59
61
  addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({
60
62
  remove: () => {},
61
63
  }),