expo-beacon 0.8.2 → 0.8.4

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.
@@ -34,6 +34,21 @@
34
34
 
35
35
  <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
36
36
 
37
+ <!--
38
+ Package-visibility declaration required by Android 11+ so that
39
+ androidx.car.app's CarConnection can resolve the Android Auto /
40
+ Automotive OS content provider. Without this, CarConnection.type
41
+ silently reports CONNECTION_TYPE_NOT_CONNECTED on freshly-started
42
+ processes (e.g. after the foreground service is revived via
43
+ START_STICKY following a swipe-away), causing spurious disconnect
44
+ events.
45
+ -->
46
+ <queries>
47
+ <intent>
48
+ <action android:name="androidx.car.app.connection.action.CAR_PROVIDER" />
49
+ </intent>
50
+ </queries>
51
+
37
52
  <application>
38
53
  <!-- AltBeacon background scanning service -->
39
54
  <service
@@ -19,6 +19,7 @@ import org.altbeacon.beacon.*
19
19
  import org.json.JSONArray
20
20
 
21
21
  private const val CHANNEL_ID = "expo_beacon_channel"
22
+ private const val CARPLAY_CHANNEL_ID = "expo_beacon_carplay_channel"
22
23
  internal const val FOREGROUND_NOTIF_ID = 1001
23
24
  /**
24
25
  * Base ID for per-beacon enter/exit notifications; incremented per unique region.
@@ -26,6 +27,14 @@ internal const val FOREGROUND_NOTIF_ID = 1001
26
27
  * before ID collision. Sufficient for real-world beacon deployments.
27
28
  */
28
29
  private const val ENTER_EXIT_NOTIF_BASE_ID = 2000
30
+ /**
31
+ * Fixed notification IDs for CarPlay connect / disconnect events. Each event
32
+ * type uses a single ID so repeated events of the same type replace the prior
33
+ * notification rather than stacking, while connect and disconnect remain
34
+ * independently visible.
35
+ */
36
+ private const val CARPLAY_CONNECTED_NOTIF_ID = 3000
37
+ private const val CARPLAY_DISCONNECTED_NOTIF_ID = 3001
29
38
 
30
39
  class BeaconForegroundService : Service(), BeaconConsumer {
31
40
 
@@ -94,6 +103,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
94
103
  super.onCreate()
95
104
  activeService = this
96
105
  createNotificationChannel()
106
+ createCarPlayNotificationChannel()
97
107
  apiForwarder = BeaconApiForwarder(this)
98
108
  beaconManager = BeaconManager.getInstanceForApplication(this).also { manager ->
99
109
  BeaconParsers.ensureRegistered(manager)
@@ -856,6 +866,60 @@ class BeaconForegroundService : Service(), BeaconConsumer {
856
866
  }
857
867
  // Best-effort delivery to the live JS bridge if a module instance is bound.
858
868
  try { boundModule?.get()?.forwardCarPlayEventFromService(eventName, payload) } catch (_: Throwable) {}
869
+ // Local notification for connect/disconnect (config-gated).
870
+ try {
871
+ when (eventName) {
872
+ "onCarPlayConnected" -> showCarPlayNotification(
873
+ "connected",
874
+ payload["transport"] as? String,
875
+ )
876
+ "onCarPlayDisconnected" -> showCarPlayNotification("disconnected", null)
877
+ }
878
+ } catch (e: Throwable) {
879
+ Log.w(TAG, "CarPlay notification post failed", e)
880
+ }
881
+ }
882
+
883
+ private fun showCarPlayNotification(eventType: String, transport: String?) {
884
+ val config = readNotificationConfig()
885
+ val eventsConfig = config.optJSONObject("carPlayEvents")
886
+
887
+ // Respect the enabled flag (defaults to true)
888
+ if (eventsConfig != null && !eventsConfig.optBoolean("enabled", true)) return
889
+
890
+ val defaultTitle = if (eventType == "connected") "CarPlay Connected" else "CarPlay Disconnected"
891
+ val title = when (eventType) {
892
+ "connected" -> eventsConfig?.optString("connectedTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
893
+ else -> eventsConfig?.optString("disconnectedTitle")?.takeIf { it.isNotEmpty() } ?: defaultTitle
894
+ }
895
+
896
+ val bodyTemplate = eventsConfig?.optString("body")?.takeIf { it.isNotEmpty() }
897
+ ?: "CarPlay session {event}"
898
+ val message = bodyTemplate
899
+ .replace("{event}", eventType)
900
+ .replace("{transport}", transport ?: "")
901
+
902
+ val iconName = eventsConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
903
+ val iconResId = iconName?.let { name ->
904
+ try { resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 } }
905
+ catch (_: Exception) { null }
906
+ } ?: android.R.drawable.ic_dialog_info
907
+
908
+ val notification = NotificationCompat.Builder(this, CARPLAY_CHANNEL_ID)
909
+ .setSmallIcon(iconResId)
910
+ .setContentTitle(title)
911
+ .setContentText(message)
912
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
913
+ .setAutoCancel(true)
914
+ .build()
915
+
916
+ val notifId = if (eventType == "connected") CARPLAY_CONNECTED_NOTIF_ID
917
+ else CARPLAY_DISCONNECTED_NOTIF_ID
918
+ try {
919
+ NotificationManagerCompat.from(this).notify(notifId, notification)
920
+ } catch (_: SecurityException) {
921
+ // POST_NOTIFICATIONS not granted — silently skip notification
922
+ }
859
923
  }
860
924
 
861
925
  private fun createNotificationChannel() {
@@ -884,6 +948,31 @@ class BeaconForegroundService : Service(), BeaconConsumer {
884
948
  }
885
949
  }
886
950
 
951
+ private fun createCarPlayNotificationChannel() {
952
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
953
+ val config = readNotificationConfig()
954
+ val channelConfig = config.optJSONObject("carPlayChannel")
955
+
956
+ val channelName = channelConfig?.optString("name")?.takeIf { it.isNotEmpty() }
957
+ ?: "CarPlay / Android Auto"
958
+ val channelDesc = channelConfig?.optString("description")?.takeIf { it.isNotEmpty() }
959
+ ?: "CarPlay and Android Auto connect/disconnect notifications"
960
+ val importance = when (channelConfig?.optString("importance")) {
961
+ "high" -> NotificationManager.IMPORTANCE_HIGH
962
+ "low" -> NotificationManager.IMPORTANCE_LOW
963
+ else -> NotificationManager.IMPORTANCE_DEFAULT
964
+ }
965
+
966
+ val notifMgr = getSystemService(NotificationManager::class.java)
967
+ if (notifMgr?.getNotificationChannel(CARPLAY_CHANNEL_ID) == null) {
968
+ val channel = NotificationChannel(CARPLAY_CHANNEL_ID, channelName, importance).apply {
969
+ description = channelDesc
970
+ }
971
+ notifMgr?.createNotificationChannel(channel)
972
+ }
973
+ }
974
+ }
975
+
887
976
  private fun buildForegroundNotification(): Notification {
888
977
  return Companion.buildForegroundNotification(this)
889
978
  }
@@ -926,6 +1015,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
926
1015
  context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
927
1016
  .edit().putBoolean("active", true).apply()
928
1017
  ensureNotificationChannel(context)
1018
+ ensureCarPlayNotificationChannel(context)
929
1019
  val intent = Intent(context, BeaconForegroundService::class.java)
930
1020
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
931
1021
  context.startForegroundService(intent)
@@ -976,6 +1066,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
976
1066
  fun enableCarPlay(context: Context) {
977
1067
  setCarPlayEnabled(context, true)
978
1068
  ensureNotificationChannel(context)
1069
+ ensureCarPlayNotificationChannel(context)
979
1070
  val intent = Intent(context, BeaconForegroundService::class.java)
980
1071
  .setAction(ACTION_ENABLE_CARPLAY)
981
1072
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -991,6 +1082,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
991
1082
  */
992
1083
  fun disableCarPlay(context: Context) {
993
1084
  setCarPlayEnabled(context, false)
1085
+ // Wipe the monitor's persisted last-known connection so a future
1086
+ // re-enable starts from a clean slate (no stale "was connected"
1087
+ // assumption that would arm the bootstrap-grace re-check).
1088
+ CarPlayMonitor.clearPersistedState(context)
994
1089
  val intent = Intent(context, BeaconForegroundService::class.java)
995
1090
  .setAction(ACTION_DISABLE_CARPLAY)
996
1091
  // Best-effort: if the service isn't running, sending the intent will
@@ -1050,6 +1145,38 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1050
1145
  }
1051
1146
  }
1052
1147
 
1148
+ /**
1149
+ * Ensure the CarPlay notification channel exists. Mirrors
1150
+ * [ensureNotificationChannel] for the dedicated CarPlay channel so that
1151
+ * users can mute CarPlay notifications independently in system settings.
1152
+ */
1153
+ fun ensureCarPlayNotificationChannel(context: Context) {
1154
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1155
+ val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
1156
+ .getString("config", null)
1157
+ val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
1158
+ val channelConfig = config.optJSONObject("carPlayChannel")
1159
+
1160
+ val channelName = channelConfig?.optString("name")?.takeIf { it.isNotEmpty() }
1161
+ ?: "CarPlay / Android Auto"
1162
+ val channelDesc = channelConfig?.optString("description")?.takeIf { it.isNotEmpty() }
1163
+ ?: "CarPlay and Android Auto connect/disconnect notifications"
1164
+ val importance = when (channelConfig?.optString("importance")) {
1165
+ "high" -> NotificationManager.IMPORTANCE_HIGH
1166
+ "low" -> NotificationManager.IMPORTANCE_LOW
1167
+ else -> NotificationManager.IMPORTANCE_DEFAULT
1168
+ }
1169
+
1170
+ val notifMgr = context.getSystemService(NotificationManager::class.java)
1171
+ if (notifMgr?.getNotificationChannel(CARPLAY_CHANNEL_ID) == null) {
1172
+ val channel = NotificationChannel(CARPLAY_CHANNEL_ID, channelName, importance).apply {
1173
+ description = channelDesc
1174
+ }
1175
+ notifMgr?.createNotificationChannel(channel)
1176
+ }
1177
+ }
1178
+ }
1179
+
1053
1180
  /**
1054
1181
  * Build the foreground notification from any Context (service or module).
1055
1182
  * Shared so that ExpoBeaconModule can pass the same notification to
@@ -1137,6 +1264,30 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1137
1264
 
1138
1265
  override fun onBind(intent: Intent?): IBinder? = null
1139
1266
 
1267
+ /**
1268
+ * Defensive override: when the user swipes the host app away from
1269
+ * Recents, Android delivers `onTaskRemoved` to bound services. Our
1270
+ * service is started via `startForegroundService` with `START_STICKY`
1271
+ * and is intended to keep running independently of the app's task —
1272
+ * specifically so that CarPlay / Android Auto observation and beacon
1273
+ * monitoring continue across swipe-away. We intentionally do NOT call
1274
+ * `stopSelf()` here; the system will redeliver `onStartCommand` on
1275
+ * its own if the process is later reclaimed.
1276
+ *
1277
+ * Logging is the only side-effect so that stuck-state issues are
1278
+ * traceable in logcat.
1279
+ */
1280
+ override fun onTaskRemoved(rootIntent: Intent?) {
1281
+ val keepAlive = isMonitoringActive(this) || isCarPlayEnabled(this)
1282
+ Log.d(
1283
+ TAG,
1284
+ "onTaskRemoved received (monitoring=${isMonitoringActive(this)}, " +
1285
+ "carPlay=${isCarPlayEnabled(this)}, keepAlive=$keepAlive). " +
1286
+ "Service will remain in foreground."
1287
+ )
1288
+ super.onTaskRemoved(rootIntent)
1289
+ }
1290
+
1140
1291
  override fun getApplicationContext(): Context = super.getApplicationContext()
1141
1292
  }
1142
1293
 
@@ -1,12 +1,17 @@
1
1
  package expo.modules.beacon
2
2
 
3
3
  import android.content.Context
4
+ import android.content.SharedPreferences
4
5
  import android.os.Handler
5
6
  import android.os.Looper
6
7
  import android.util.Log
7
8
  import androidx.car.app.connection.CarConnection
8
9
  import androidx.lifecycle.LiveData
9
10
  import androidx.lifecycle.Observer
11
+ import java.text.SimpleDateFormat
12
+ import java.util.Date
13
+ import java.util.Locale
14
+ import java.util.TimeZone
10
15
 
11
16
  /**
12
17
  * Wraps [CarConnection] LiveData to surface Android Auto / Automotive OS
@@ -32,6 +37,17 @@ internal class CarPlayMonitor(private val context: Context) {
32
37
  private var observer: Observer<Int>? = null
33
38
  private var emit: Emit? = null
34
39
  @Volatile private var lastConnected: Boolean? = null
40
+ /**
41
+ * Pending bootstrap-grace runnable that will re-check the connection state
42
+ * after [BOOTSTRAP_GRACE_MS] before emitting a `disconnected` event.
43
+ * Used only when the persisted state was `connected` but the first
44
+ * observed value after [start] is `NOT_CONNECTED` — this absorbs the
45
+ * LiveData/Content-Provider initial-value race that occurs after a
46
+ * cold service restart.
47
+ */
48
+ private var pendingDisconnectCheck: Runnable? = null
49
+ /** Most recent raw type seen from the LiveData; used by the grace re-check. */
50
+ @Volatile private var lastObservedType: Int = CarConnection.CONNECTION_TYPE_NOT_CONNECTED
35
51
 
36
52
  /**
37
53
  * Begin observing connection state. Idempotent — calling twice replaces the
@@ -41,12 +57,18 @@ internal class CarPlayMonitor(private val context: Context) {
41
57
  fun start(emit: Emit) {
42
58
  runOnMain {
43
59
  this.emit = emit
60
+ // Seed in-memory state from persisted last-known connection so a
61
+ // cold restart can distinguish "we lost a previously-active
62
+ // connection" from "we have no prior knowledge yet".
63
+ if (lastConnected == null) {
64
+ lastConnected = readPersistedConnected()
65
+ }
44
66
  if (observer == null) {
45
67
  val obs = Observer<Int> { type -> handleType(type) }
46
68
  observer = obs
47
69
  try {
48
70
  liveData.observeForever(obs)
49
- Log.d(TAG, "CarPlay monitoring started")
71
+ Log.d(TAG, "CarPlay monitoring started (seeded lastConnected=$lastConnected)")
50
72
  } catch (e: Exception) {
51
73
  Log.w(TAG, "Failed to start CarPlay monitoring: ${e.message}")
52
74
  }
@@ -57,6 +79,7 @@ internal class CarPlayMonitor(private val context: Context) {
57
79
  /** Stop observing connection state and release the emit callback. */
58
80
  fun stop() {
59
81
  runOnMain {
82
+ cancelPendingDisconnectCheck()
60
83
  observer?.let {
61
84
  try { liveData.removeObserver(it) } catch (_: Exception) {}
62
85
  }
@@ -68,10 +91,70 @@ internal class CarPlayMonitor(private val context: Context) {
68
91
  }
69
92
 
70
93
  private fun handleType(type: Int) {
94
+ lastObservedType = type
71
95
  val connected = type != CarConnection.CONNECTION_TYPE_NOT_CONNECTED
72
- if (lastConnected == connected) return
96
+ val previous = lastConnected
97
+
98
+ // Same state as last emitted/seeded value → cancel any pending grace
99
+ // re-check (the connection came back) and bail.
100
+ if (previous == connected) {
101
+ if (connected) cancelPendingDisconnectCheck()
102
+ return
103
+ }
104
+
105
+ // First-ever observation in this process AND no persisted prior state:
106
+ // the LiveData's initial value during a cold start is frequently
107
+ // NOT_CONNECTED before the underlying Content-Provider query resolves.
108
+ // Treat this purely as a state seed; do not emit a spurious disconnect.
109
+ if (previous == null && !connected) {
110
+ lastConnected = false
111
+ Log.d(TAG, "Suppressed initial NOT_CONNECTED — seeded lastConnected=false")
112
+ return
113
+ }
114
+
115
+ // Persisted/known state was `connected` but we just observed
116
+ // `NOT_CONNECTED`. This is the most likely false-positive path on
117
+ // process restart — defer emission for a short grace period and
118
+ // re-check; only emit if it's still NOT_CONNECTED.
119
+ if (previous == true && !connected) {
120
+ scheduleDisconnectCheck()
121
+ return
122
+ }
123
+
124
+ // Genuine transition — emit immediately.
125
+ emitTransition(connected, type)
126
+ }
127
+
128
+ private fun scheduleDisconnectCheck() {
129
+ cancelPendingDisconnectCheck()
130
+ val r = Runnable {
131
+ pendingDisconnectCheck = null
132
+ val nowType = lastObservedType
133
+ val nowConnected = nowType != CarConnection.CONNECTION_TYPE_NOT_CONNECTED
134
+ if (nowConnected) {
135
+ Log.d(TAG, "Disconnect grace re-check: connection recovered (type=$nowType) — suppressed")
136
+ // lastConnected stays true; no event.
137
+ return@Runnable
138
+ }
139
+ // Still disconnected after grace — emit the real transition.
140
+ emitTransition(false, nowType)
141
+ }
142
+ pendingDisconnectCheck = r
143
+ mainHandler.postDelayed(r, BOOTSTRAP_GRACE_MS)
144
+ Log.d(TAG, "Deferred CarPlay disconnect by ${BOOTSTRAP_GRACE_MS}ms (grace re-check pending)")
145
+ }
146
+
147
+ private fun cancelPendingDisconnectCheck() {
148
+ pendingDisconnectCheck?.let { mainHandler.removeCallbacks(it) }
149
+ pendingDisconnectCheck = null
150
+ }
151
+
152
+ private fun emitTransition(connected: Boolean, type: Int) {
73
153
  lastConnected = connected
154
+ writePersistedConnected(connected)
74
155
  val callback = emit ?: return
156
+ val now = System.currentTimeMillis()
157
+ val nowIso = formatIso(now)
75
158
  if (connected) {
76
159
  val transport = when (type) {
77
160
  CarConnection.CONNECTION_TYPE_PROJECTION -> "projection"
@@ -80,15 +163,41 @@ internal class CarPlayMonitor(private val context: Context) {
80
163
  }
81
164
  callback("onCarPlayConnected", mapOf(
82
165
  "transport" to transport,
83
- "timestamp" to System.currentTimeMillis(),
166
+ "timestamp" to now,
167
+ "timestampIso" to nowIso,
84
168
  ))
85
169
  } else {
86
170
  callback("onCarPlayDisconnected", mapOf(
87
- "timestamp" to System.currentTimeMillis(),
171
+ "timestamp" to now,
172
+ "timestampIso" to nowIso,
88
173
  ))
89
174
  }
90
175
  }
91
176
 
177
+ private fun readPersistedConnected(): Boolean? {
178
+ val prefs = prefs() ?: return null
179
+ if (!prefs.contains(KEY_LAST_CONNECTED)) return null
180
+ return prefs.getBoolean(KEY_LAST_CONNECTED, false)
181
+ }
182
+
183
+ private fun writePersistedConnected(connected: Boolean) {
184
+ prefs()?.edit()?.putBoolean(KEY_LAST_CONNECTED, connected)?.apply()
185
+ }
186
+
187
+ private fun prefs(): SharedPreferences? = try {
188
+ context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
189
+ } catch (e: Throwable) {
190
+ Log.w(TAG, "Failed to open CarPlayMonitor prefs", e)
191
+ null
192
+ }
193
+
194
+ private fun formatIso(millis: Long): String {
195
+ // SimpleDateFormat is not thread-safe — synchronize on the shared instance.
196
+ synchronized(ISO_FORMAT) {
197
+ return ISO_FORMAT.format(Date(millis))
198
+ }
199
+ }
200
+
92
201
  private fun runOnMain(block: () -> Unit) {
93
202
  if (Looper.myLooper() == Looper.getMainLooper()) {
94
203
  block()
@@ -97,7 +206,37 @@ internal class CarPlayMonitor(private val context: Context) {
97
206
  }
98
207
  }
99
208
 
100
- private companion object {
101
- const val TAG = "CarPlayMonitor"
209
+ companion object {
210
+ private const val TAG = "CarPlayMonitor"
211
+ private const val PREFS_NAME = "expo_beacon_carplay_monitor"
212
+ private const val KEY_LAST_CONNECTED = "last_connected"
213
+ /**
214
+ * Grace window before emitting a `disconnected` event when the
215
+ * persisted state was `connected` but the freshly-attached observer
216
+ * reports `NOT_CONNECTED`. Absorbs the LiveData / Content-Provider
217
+ * initial-value race that occurs during a cold service restart
218
+ * (e.g. after the app is swiped away and START_STICKY revives us).
219
+ */
220
+ private const val BOOTSTRAP_GRACE_MS = 3_000L
221
+ // ISO 8601 UTC with millisecond precision. Safe for all supported APIs (minSdk 23).
222
+ private val ISO_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
223
+ timeZone = TimeZone.getTimeZone("UTC")
224
+ }
225
+
226
+ /**
227
+ * Clear the persisted last-known connection state. Call when CarPlay
228
+ * monitoring is being explicitly disabled by the user, so that a
229
+ * subsequent re-enable does not assume a stale prior connection.
230
+ */
231
+ @JvmStatic
232
+ fun clearPersistedState(context: Context) {
233
+ try {
234
+ context.applicationContext
235
+ .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
236
+ .edit()
237
+ .remove(KEY_LAST_CONNECTED)
238
+ .apply()
239
+ } catch (_: Throwable) { /* best-effort */ }
240
+ }
102
241
  }
103
242
  }
@@ -104,14 +104,50 @@ export type NotificationChannelConfig = {
104
104
  */
105
105
  importance?: "low" | "default" | "high";
106
106
  };
107
+ /** Configuration for CarPlay / Android Auto connect/disconnect notifications. */
108
+ export type CarPlayNotificationConfig = {
109
+ /** Whether to show CarPlay connect/disconnect notifications. Default: true. */
110
+ enabled?: boolean;
111
+ /** Notification title on CarPlay/Android Auto connect. Default: "CarPlay Connected". */
112
+ connectedTitle?: string;
113
+ /** Notification title on CarPlay/Android Auto disconnect. Default: "CarPlay Disconnected". */
114
+ disconnectedTitle?: string;
115
+ /**
116
+ * Notification body template. Supports `{event}` ("connected"/"disconnected") and
117
+ * `{transport}` (e.g. "wired", "wireless", "projection", "native", "unknown") placeholders.
118
+ * Note: `{transport}` is only meaningful for connect events; on disconnect it is replaced with an empty string.
119
+ * Default: "CarPlay session {event}".
120
+ */
121
+ body?: string;
122
+ /** Play a sound with the notification (iOS only). Default: true. */
123
+ sound?: boolean;
124
+ /** Android drawable resource name for the notification icon (e.g. "ic_notification"). */
125
+ icon?: string;
126
+ };
127
+ /** Configuration for the Android notification channel used for CarPlay events. */
128
+ export type CarPlayChannelConfig = {
129
+ /** Channel display name shown in system settings. Default: "CarPlay / Android Auto". */
130
+ name?: string;
131
+ /** Channel description shown in system settings. Default: "CarPlay and Android Auto connect/disconnect notifications". */
132
+ description?: string;
133
+ /**
134
+ * Channel importance level. Default: 'default' (so connect/disconnect events make a sound).
135
+ * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.
136
+ */
137
+ importance?: "low" | "default" | "high";
138
+ };
107
139
  /** Combined notification configuration for all notification types. */
108
140
  export type NotificationConfig = {
109
141
  /** Settings for beacon enter/exit event notifications. */
110
142
  beaconEvents?: BeaconNotificationConfig;
143
+ /** Settings for CarPlay / Android Auto connect/disconnect notifications. */
144
+ carPlayEvents?: CarPlayNotificationConfig;
111
145
  /** Settings for the persistent foreground service notification (Android only). */
112
146
  foregroundService?: ForegroundServiceConfig;
113
147
  /** Settings for the Android notification channel (Android only). */
114
148
  channel?: NotificationChannelConfig;
149
+ /** Settings for the Android notification channel used for CarPlay events (Android only). */
150
+ carPlayChannel?: CarPlayChannelConfig;
115
151
  };
116
152
  /** Snapshot of the current monitoring configuration and active state. */
117
153
  export type MonitoringConfig = {
@@ -261,11 +297,24 @@ export type CarPlayConnectedEvent = {
261
297
  transport: CarPlayTransport;
262
298
  /** Timestamp in milliseconds since epoch. */
263
299
  timestamp: number;
300
+ /** ISO 8601 UTC representation of {@link timestamp} (e.g. "2026-05-12T14:23:45.678Z"). */
301
+ timestampIso?: string;
264
302
  };
265
303
  /** Payload fired when the device disconnects from a CarPlay or Android Auto session. */
266
304
  export type CarPlayDisconnectedEvent = {
267
305
  /** Timestamp in milliseconds since epoch. */
268
306
  timestamp: number;
307
+ /** ISO 8601 UTC representation of {@link timestamp} (e.g. "2026-05-12T14:23:45.678Z"). */
308
+ timestampIso?: string;
309
+ /**
310
+ * Reason this disconnect was emitted. Absent for normal real-time disconnects.
311
+ * `"reconciled"` (iOS only) indicates the disconnect was synthesized after the
312
+ * module was recreated in a new process and detected that the previously
313
+ * persisted CarPlay state no longer matches the current audio route — i.e.
314
+ * the disconnect happened off-process (force-quit, OS reclaim, abrupt cable
315
+ * yank) and is being delivered post-hoc.
316
+ */
317
+ reason?: "reconciled";
269
318
  };
270
319
  /** Payload for native beacon error events (monitoring/ranging failures). */
271
320
  export type BeaconErrorEvent = {
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoBeacon.types.d.ts","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoBeacon.types.js","sourceRoot":"","sources":["../src/ExpoBeacon.types.ts"],"names":[],"mappings":"","sourcesContent":["/** Raw beacon discovered during a scan. */\r\nexport type BeaconScanResult = {\r\n uuid: string; // iBeacon proximity UUID (uppercase, formatted)\r\n major: number; // iBeacon major value (0–65535)\r\n minor: number; // iBeacon minor value (0–65535)\r\n rssi: number; // Signal strength in dBm (negative number)\r\n distance: number; // Estimated distance in meters\r\n txPower: number; // Calibrated TX power\r\n /** BLE advertising device name. May be undefined on iOS (CoreLocation does not expose it for iBeacon). */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * A beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedBeacon = {\r\n identifier: string; // User-defined label (e.g. \"lobby-door\")\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onBeaconTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * 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/** Snapshot of the current monitoring configuration and active state. */\r\nexport type MonitoringConfig = {\r\n /** Whether background monitoring is currently active. */\r\n isMonitoring: boolean;\r\n maxDistance?: number;\r\n exitDistance?: number;\r\n minRssi?: number;\r\n level?: 'all' | 'events';\r\n /** Seconds after last beacon sighting before an exit event fires. Default: 300. */\r\n exitTimeoutSeconds?: number;\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Current state snapshot for a paired monitored device. */\r\nexport type MonitoredDeviceState =\r\n | {\r\n kind: \"ibeacon\";\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\r\n }\r\n | {\r\n kind: \"eddystone\";\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\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 /**\r\n * Seconds after last beacon sighting before an exit event fires when the beacon\r\n * disappears without moving outside the exit distance threshold.\r\n *\r\n * Default: 300 (5 minutes). Minimum: 1.\r\n */\r\n exitTimeoutSeconds?: number;\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * 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/** Transport reported with CarPlay / Android Auto connection events. */\r\nexport type CarPlayTransport =\r\n | \"wired\" // iOS CarPlay over USB / Lightning\r\n | \"wireless\" // iOS CarPlay over Bluetooth + Wi-Fi\r\n | \"projection\" // Android Auto projection (phone projecting to head unit)\r\n | \"native\" // Android Automotive OS (running on the head unit)\r\n | \"unknown\";\r\n\r\n/** Payload fired when the device connects to a CarPlay or Android Auto session. */\r\nexport type CarPlayConnectedEvent = {\r\n transport: CarPlayTransport;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n};\r\n\r\n/** Payload fired when the device disconnects from a CarPlay or Android Auto session. */\r\nexport type CarPlayDisconnectedEvent = {\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n};\r\n\r\n/** Payload for native beacon error events (monitoring/ranging failures). */\r\nexport type BeaconErrorEvent = {\r\n /** Region or constraint identifier, empty string if unavailable. */\r\n identifier: string;\r\n /** Machine-readable error code (e.g. \"MONITORING_FAILED\", \"RANGING_FAILED\", \"SECURITY_EXCEPTION\"). */\r\n code: string;\r\n /** Human-readable error message from the native layer. */\r\n message: string;\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 /** Fired when a native monitoring or ranging failure occurs (logged to DB and forwarded to JS). */\r\n onBeaconError: (params: BeaconErrorEvent) => void;\r\n /** Fired when the device connects to a CarPlay (iOS) or Android Auto (Android) session. */\r\n onCarPlayConnected: (params: CarPlayConnectedEvent) => void;\r\n /** Fired when the device disconnects from a CarPlay (iOS) or Android Auto (Android) session. */\r\n onCarPlayDisconnected: (params: CarPlayDisconnectedEvent) => 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/** Configuration for CarPlay / Android Auto connect/disconnect notifications. */\r\nexport type CarPlayNotificationConfig = {\r\n /** Whether to show CarPlay connect/disconnect notifications. Default: true. */\r\n enabled?: boolean;\r\n /** Notification title on CarPlay/Android Auto connect. Default: \"CarPlay Connected\". */\r\n connectedTitle?: string;\r\n /** Notification title on CarPlay/Android Auto disconnect. Default: \"CarPlay Disconnected\". */\r\n disconnectedTitle?: string;\r\n /**\r\n * Notification body template. Supports `{event}` (\"connected\"/\"disconnected\") and\r\n * `{transport}` (e.g. \"wired\", \"wireless\", \"projection\", \"native\", \"unknown\") placeholders.\r\n * Note: `{transport}` is only meaningful for connect events; on disconnect it is replaced with an empty string.\r\n * Default: \"CarPlay session {event}\".\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 notification channel used for CarPlay events. */\r\nexport type CarPlayChannelConfig = {\r\n /** Channel display name shown in system settings. Default: \"CarPlay / Android Auto\". */\r\n name?: string;\r\n /** Channel description shown in system settings. Default: \"CarPlay and Android Auto connect/disconnect notifications\". */\r\n description?: string;\r\n /**\r\n * Channel importance level. Default: 'default' (so connect/disconnect events make a sound).\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 CarPlay / Android Auto connect/disconnect notifications. */\r\n carPlayEvents?: CarPlayNotificationConfig;\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 /** Settings for the Android notification channel used for CarPlay events (Android only). */\r\n carPlayChannel?: CarPlayChannelConfig;\r\n};\r\n\r\n/** Snapshot of the current monitoring configuration and active state. */\r\nexport type MonitoringConfig = {\r\n /** Whether background monitoring is currently active. */\r\n isMonitoring: boolean;\r\n maxDistance?: number;\r\n exitDistance?: number;\r\n minRssi?: number;\r\n level?: 'all' | 'events';\r\n /** Seconds after last beacon sighting before an exit event fires. Default: 300. */\r\n exitTimeoutSeconds?: number;\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Current state snapshot for a paired monitored device. */\r\nexport type MonitoredDeviceState =\r\n | {\r\n kind: \"ibeacon\";\r\n identifier: string;\r\n uuid: string;\r\n major: number;\r\n minor: number;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\r\n }\r\n | {\r\n kind: \"eddystone\";\r\n identifier: string;\r\n namespace: string;\r\n instance: string;\r\n state: \"entered\" | \"exited\";\r\n /** Current distance in metres, or null when exited or no live reading is available. */\r\n distance: number | null;\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 /**\r\n * Seconds after last beacon sighting before an exit event fires when the beacon\r\n * disappears without moving outside the exit distance threshold.\r\n *\r\n * Default: 300 (5 minutes). Minimum: 1.\r\n */\r\n exitTimeoutSeconds?: number;\r\n /** Notification configuration overrides to apply for this monitoring session. */\r\n notifications?: NotificationConfig;\r\n};\r\n\r\n/** Eddystone frame type. */\r\nexport type EddystoneFrameType = \"uid\" | \"url\";\r\n\r\n/** Raw Eddystone beacon discovered during a scan. */\r\nexport type EddystoneScanResult = {\r\n frameType: EddystoneFrameType;\r\n /** 10-byte namespace ID as hex string (20 chars). Present for UID frames. */\r\n namespace?: string;\r\n /** 6-byte instance ID as hex string (12 chars). Present for UID frames. */\r\n instance?: string;\r\n /** Decoded URL. Present for URL frames. */\r\n url?: string;\r\n rssi: number;\r\n distance: number;\r\n txPower: number;\r\n /** BLE advertising device name. */\r\n name?: string;\r\n};\r\n\r\n/**\r\n * An Eddystone-UID beacon that has been paired/registered for monitoring.\r\n *\r\n * Note: Paired beacon data is stored unencrypted in UserDefaults (iOS) /\r\n * SharedPreferences (Android) and may be included in device backups.\r\n */\r\nexport type PairedEddystone = {\r\n identifier: string;\r\n /** 10-byte namespace ID as hex string (20 chars). */\r\n namespace: string;\r\n /** 6-byte instance ID as hex string (12 chars). */\r\n instance: string;\r\n /** BLE advertising device name, if provided at pairing time. */\r\n name?: string;\r\n /**\r\n * Timeout in seconds. When set, the module fires `onEddystoneTimeout` once\r\n * after the beacon has been continuously in range for this duration.\r\n * The timer resets if the beacon exits and re-enters range.\r\n *\r\n * 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/** Transport reported with CarPlay / Android Auto connection events. */\r\nexport type CarPlayTransport =\r\n | \"wired\" // iOS CarPlay over USB / Lightning\r\n | \"wireless\" // iOS CarPlay over Bluetooth + Wi-Fi\r\n | \"projection\" // Android Auto projection (phone projecting to head unit)\r\n | \"native\" // Android Automotive OS (running on the head unit)\r\n | \"unknown\";\r\n\r\n/** Payload fired when the device connects to a CarPlay or Android Auto session. */\r\nexport type CarPlayConnectedEvent = {\r\n transport: CarPlayTransport;\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** ISO 8601 UTC representation of {@link timestamp} (e.g. \"2026-05-12T14:23:45.678Z\"). */\r\n timestampIso?: string;\r\n};\r\n\r\n/** Payload fired when the device disconnects from a CarPlay or Android Auto session. */\r\nexport type CarPlayDisconnectedEvent = {\r\n /** Timestamp in milliseconds since epoch. */\r\n timestamp: number;\r\n /** ISO 8601 UTC representation of {@link timestamp} (e.g. \"2026-05-12T14:23:45.678Z\"). */\r\n timestampIso?: string;\r\n /**\r\n * Reason this disconnect was emitted. Absent for normal real-time disconnects.\r\n * `\"reconciled\"` (iOS only) indicates the disconnect was synthesized after the\r\n * module was recreated in a new process and detected that the previously\r\n * persisted CarPlay state no longer matches the current audio route — i.e.\r\n * the disconnect happened off-process (force-quit, OS reclaim, abrupt cable\r\n * yank) and is being delivered post-hoc.\r\n */\r\n reason?: \"reconciled\";\r\n};\r\n\r\n/** Payload for native beacon error events (monitoring/ranging failures). */\r\nexport type BeaconErrorEvent = {\r\n /** Region or constraint identifier, empty string if unavailable. */\r\n identifier: string;\r\n /** Machine-readable error code (e.g. \"MONITORING_FAILED\", \"RANGING_FAILED\", \"SECURITY_EXCEPTION\"). */\r\n code: string;\r\n /** Human-readable error message from the native layer. */\r\n message: string;\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 /** Fired when a native monitoring or ranging failure occurs (logged to DB and forwarded to JS). */\r\n onBeaconError: (params: BeaconErrorEvent) => void;\r\n /** Fired when the device connects to a CarPlay (iOS) or Android Auto (Android) session. */\r\n onCarPlayConnected: (params: CarPlayConnectedEvent) => void;\r\n /** Fired when the device disconnects from a CarPlay (iOS) or Android Auto (Android) session. */\r\n onCarPlayDisconnected: (params: CarPlayDisconnectedEvent) => 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"]}
package/build/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from "./ExpoBeaconModule.js";
2
- export type { BeaconScanResult, PairedBeacon, BeaconRegionEvent, BeaconDistanceEvent, BeaconTimeoutEvent, ExpoBeaconModuleEvents, NotificationConfig, MonitoringOptions, MonitoringConfig, MonitoredDeviceState, BeaconNotificationConfig, ForegroundServiceConfig, NotificationChannelConfig, EddystoneFrameType, EddystoneScanResult, PairedEddystone, EddystoneRegionEvent, EddystoneDistanceEvent, EddystoneTimeoutEvent, EventLogQueryOptions, EventLogEntry, CarPlayTransport, CarPlayConnectedEvent, CarPlayDisconnectedEvent, } from "./ExpoBeacon.types";
2
+ export type { BeaconScanResult, PairedBeacon, BeaconRegionEvent, BeaconDistanceEvent, BeaconTimeoutEvent, ExpoBeaconModuleEvents, NotificationConfig, MonitoringOptions, MonitoringConfig, MonitoredDeviceState, BeaconNotificationConfig, CarPlayNotificationConfig, CarPlayChannelConfig, ForegroundServiceConfig, NotificationChannelConfig, EddystoneFrameType, EddystoneScanResult, PairedEddystone, EddystoneRegionEvent, EddystoneDistanceEvent, EddystoneTimeoutEvent, EventLogQueryOptions, EventLogEntry, CarPlayTransport, CarPlayConnectedEvent, CarPlayDisconnectedEvent, } from "./ExpoBeacon.types";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGhD,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGhD,YAAY,EACV,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["// Native module (default export)\r\nexport { default } from \"./ExpoBeaconModule.js\";\r\n\r\n// All public types\r\nexport type {\r\n BeaconScanResult,\r\n PairedBeacon,\r\n BeaconRegionEvent,\r\n BeaconDistanceEvent,\r\n BeaconTimeoutEvent,\r\n ExpoBeaconModuleEvents,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n MonitoringConfig,\r\n MonitoredDeviceState,\r\n BeaconNotificationConfig,\r\n ForegroundServiceConfig,\r\n NotificationChannelConfig,\r\n EddystoneFrameType,\r\n EddystoneScanResult,\r\n PairedEddystone,\r\n EddystoneRegionEvent,\r\n EddystoneDistanceEvent,\r\n EddystoneTimeoutEvent,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n CarPlayTransport,\r\n CarPlayConnectedEvent,\r\n CarPlayDisconnectedEvent,\r\n} from \"./ExpoBeacon.types\";\r\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["// Native module (default export)\r\nexport { default } from \"./ExpoBeaconModule.js\";\r\n\r\n// All public types\r\nexport type {\r\n BeaconScanResult,\r\n PairedBeacon,\r\n BeaconRegionEvent,\r\n BeaconDistanceEvent,\r\n BeaconTimeoutEvent,\r\n ExpoBeaconModuleEvents,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n MonitoringConfig,\r\n MonitoredDeviceState,\r\n BeaconNotificationConfig,\r\n CarPlayNotificationConfig,\r\n CarPlayChannelConfig,\r\n ForegroundServiceConfig,\r\n NotificationChannelConfig,\r\n EddystoneFrameType,\r\n EddystoneScanResult,\r\n PairedEddystone,\r\n EddystoneRegionEvent,\r\n EddystoneDistanceEvent,\r\n EddystoneTimeoutEvent,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n CarPlayTransport,\r\n CarPlayConnectedEvent,\r\n CarPlayDisconnectedEvent,\r\n} from \"./ExpoBeacon.types\";\r\n"]}
@@ -20,9 +20,24 @@ final class CarPlayMonitor {
20
20
  private let log = OSLog(subsystem: "expo.modules.beacon", category: "CarPlayMonitor")
21
21
  private let queue = DispatchQueue.main
22
22
 
23
+ /// Cached ISO8601 formatter (UTC, fractional seconds). Reused across emits
24
+ /// to avoid per-event allocation. `ISO8601DateFormatter` is documented as
25
+ /// thread-safe.
26
+ private static let isoFormatter: ISO8601DateFormatter = {
27
+ let f = ISO8601DateFormatter()
28
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
29
+ f.timeZone = TimeZone(identifier: "UTC")
30
+ return f
31
+ }()
32
+
23
33
  private var observer: NSObjectProtocol?
24
34
  private var emit: Emit?
25
35
  private var isConnected: Bool = false
36
+ /// Optional persistence target for last-known connection state. Injected via
37
+ /// `start(emit:defaults:)`. When set, every connect/disconnect emission is
38
+ /// mirrored to `CARPLAY_LAST_CONNECTED_KEY` so a freshly relaunched process
39
+ /// can detect a missed disconnect via `reconcileOnProcessStart(emit:)`.
40
+ private var defaults: UserDefaults?
26
41
  /// When `true`, an authoritative source (CarPlay scene delegate, granted via the
27
42
  /// `com.apple.developer.carplay-driving-task` entitlement) is providing
28
43
  /// connect/disconnect events. The audio-session observer becomes a passive
@@ -36,10 +51,17 @@ final class CarPlayMonitor {
36
51
  /// previous emit callback but does not register a duplicate observer.
37
52
  /// Emits an immediate `onCarPlayConnected` event if a CarPlay route is
38
53
  /// already active at the time of the call.
39
- func start(emit: @escaping Emit) {
54
+ ///
55
+ /// - Parameter defaults: Optional `UserDefaults` suite used to persist the
56
+ /// last-known connection state for cross-process reconciliation. When the
57
+ /// module is recreated in a new process (e.g. after a background-wake)
58
+ /// the persisted value is used by `reconcileOnProcessStart(emit:)` to
59
+ /// synthesize a missed disconnect. Pass `nil` to disable persistence.
60
+ func start(emit: @escaping Emit, defaults: UserDefaults? = nil) {
40
61
  queue.async { [weak self] in
41
62
  guard let self = self else { return }
42
63
  self.emit = emit
64
+ if defaults != nil { self.defaults = defaults }
43
65
  if self.observer == nil {
44
66
  self.observer = NotificationCenter.default.addObserver(
45
67
  forName: AVAudioSession.routeChangeNotification,
@@ -59,7 +81,10 @@ final class CarPlayMonitor {
59
81
  }
60
82
  }
61
83
 
62
- /// Stop observing route changes and clear the emit callback.
84
+ /// Stop observing route changes and clear the emit callback. Also clears
85
+ /// the persisted last-known state so a subsequent `start(...)` in a future
86
+ /// process doesn't trigger a spurious reconciliation. Call this only when
87
+ /// the user has explicitly opted out of CarPlay monitoring.
63
88
  func stop() {
64
89
  queue.async { [weak self] in
65
90
  guard let self = self else { return }
@@ -70,6 +95,8 @@ final class CarPlayMonitor {
70
95
  }
71
96
  self.emit = nil
72
97
  self.isConnected = false
98
+ self.isEntitledMode = false
99
+ self.persistConnectionState(false)
73
100
  }
74
101
  }
75
102
 
@@ -101,13 +128,17 @@ final class CarPlayMonitor {
101
128
  self.isEntitledMode = true
102
129
  os_log("CarPlay scene connected (entitled source)", log: self.log, type: .info)
103
130
  if !self.isConnected {
104
- self.isConnected = true
105
- self.emitConnected(transport: transport)
106
- }
107
- }
108
- }
109
-
110
- /// Called by `BeaconCarPlaySceneDelegate.templateApplicationScene(_:didDisconnect:)`.
131
+ self.isConnected = trueclears the entitled-mode flag so the
132
+ /// audio-session fallback path becomes authoritative again until the next
133
+ /// entitled connect. Without this reset, a single missed scene-delegate
134
+ /// disconnect (force-quit, OS reclaim, abrupt cable yank between connect
135
+ /// and disconnect callbacks) would silently suppress the audio-session
136
+ /// fallback for the rest of the process lifetime.
137
+ func notifyEntitledDisconnect() {
138
+ queue.async { [weak self] in
139
+ guard let self = self else { return }
140
+ os_log("CarPlay scene disconnected (entitled source)", log: self.log, type: .info)
141
+ self.isEntitledMode = false
111
142
  /// Emits `onCarPlayDisconnected` and keeps the entitled-mode flag set so
112
143
  /// subsequent audio-session events remain suppressed (the scene delegate is
113
144
  /// the source of truth for the lifetime of the process).
@@ -142,20 +173,67 @@ final class CarPlayMonitor {
142
173
  /// is always trusted.
143
174
  private func handleRouteChange(notification: Notification?) {
144
175
  if let userInfo = notification?.userInfo,
145
- let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
146
- let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw) {
147
- switch reason {
148
- case .newDeviceAvailable, .oldDeviceUnavailable:
149
- break // real device change — proceed
150
- default:
151
- // Category/override/configuration changes etc. don't represent
152
- // a CarPlay connect/disconnect. Skip to avoid spurious events.
153
- return
154
- }
155
- }
176
+ let (connected, transport) = Self.currentCarPlayState()
156
177
  // When an entitled CarPlay scene source is active it is authoritative.
157
178
  // The audio-session signal is kept as a redundant secondary check but
158
179
  // must NOT emit events — the scene delegate already did, or will.
180
+ // We DO still update the cached `isConnected` snapshot and persist it
181
+ // so that cross-process reconciliation (`reconcileOnProcessStart`) and
182
+ // the post-entitled-disconnect fallback path see an accurate state.
183
+ if isEntitledMode {
184
+ if connected != isConnected {
185
+ isConnected = connected
186
+ persistConnectionState(connected)
187
+ }
188
+ return
189
+ }
190
+ if connected == isConnected { return }
191
+ isConnected = connected
192
+ if connected {
193
+ emitConnected(transport: transport)
194
+ } else {
195
+ emitDisconnected()
196
+ }
197
+ }
198
+
199
+ // MARK: - Cross-process reconciliation
200
+
201
+ /// Compare persisted last-known state against the current audio route and
202
+ /// emit a synthetic `onCarPlayDisconnected` if persisted=connected but the
203
+ /// route is no longer CarPlay. Use case: the previous process was killed or
204
+ /// suspended-then-OS-reclaimed while CarPlay was connected, and the disconnect
205
+ /// fired off-process. JS listeners attached to the freshly recreated module
206
+ /// persistConnectionState(true)
207
+ emit?("onCarPlayConnected", payload)
208
+ }
209
+
210
+ /// Emit a disconnect event. When `reason` is non-nil it is included in the
211
+ /// payload so consumers can distinguish real-time disconnects from
212
+ /// post-hoc reconciled ones (currently `"reconciled"` from
213
+ /// `reconcileOnProcessStart`). Additive, non-breaking.
214
+ private func emitDisconnected(reason: String? = nil) {
215
+ let now = Date()
216
+ var payload: [String: Any] = [
217
+ "timestamp": now.timeIntervalSince1970 * 1000.0,
218
+ "timestampIso": Self.isoFormatter.string(from: now),
219
+ ]
220
+ if let reason = reason {
221
+ payload["reason"] = reason
222
+ }
223
+ persistConnectionState(false) let persistedConnected = defaults.bool(forKey: CARPLAY_LAST_CONNECTED_KEY)
224
+ let (currentConnected, _) = Self.currentCarPlayState()
225
+ if persistedConnected && !currentConnected {
226
+ os_log("CarPlay reconcile: persisted=connected, current=disconnected — emitting synthetic disconnect", log: self.log, type: .info)
227
+ self.isConnected = false
228
+ self.emitDisconnected(reason: "reconciled")
229
+ }
230
+ }
231
+ }
232
+
233
+ /// Write the current connection state to the injected `UserDefaults` suite
234
+ /// (when available). No-op when persistence wasn't configured.
235
+ private func persistConnectionState(_ connected: Bool) {
236
+ defaults?.set(connected, forKey: CARPLAY_LAST_CONNECTED_KEY)/ must NOT emit events — the scene delegate already did, or will.
159
237
  if isEntitledMode {
160
238
  return
161
239
  }
@@ -170,16 +248,20 @@ final class CarPlayMonitor {
170
248
  }
171
249
 
172
250
  private func emitConnected(transport: String) {
251
+ let now = Date()
173
252
  let payload: [String: Any] = [
174
253
  "transport": transport,
175
- "timestamp": Date().timeIntervalSince1970 * 1000.0,
254
+ "timestamp": now.timeIntervalSince1970 * 1000.0,
255
+ "timestampIso": Self.isoFormatter.string(from: now),
176
256
  ]
177
257
  emit?("onCarPlayConnected", payload)
178
258
  }
179
259
 
180
260
  private func emitDisconnected() {
261
+ let now = Date()
181
262
  let payload: [String: Any] = [
182
- "timestamp": Date().timeIntervalSince1970 * 1000.0,
263
+ "timestamp": now.timeIntervalSince1970 * 1000.0,
264
+ "timestampIso": Self.isoFormatter.string(from: now),
183
265
  ]
184
266
  emit?("onCarPlayDisconnected", payload)
185
267
  }
@@ -13,6 +13,10 @@ internal let MIN_RSSI_KEY = "expo.beacon.min_rssi"
13
13
  internal let EVENT_LEVEL_KEY = "expo.beacon.event_level"
14
14
  internal let EXIT_TIMEOUT_SECONDS_KEY = "expo.beacon.exit_timeout_seconds"
15
15
  internal let CARPLAY_MONITORING_ENABLED_KEY = "expo.beacon.carplay_monitoring_enabled"
16
+ /// Persisted last-known CarPlay connection state. Used by `CarPlayMonitor` to
17
+ /// reconcile across process recreations (e.g. background-launch wake) and emit
18
+ /// a synthetic `onCarPlayDisconnected` if the route is no longer CarPlay.
19
+ internal let CARPLAY_LAST_CONNECTED_KEY = "expo.beacon.carplay_last_connected"
16
20
 
17
21
  // MARK: - Tuning thresholds
18
22
 
@@ -1,4 +1,5 @@
1
1
  import CoreLocation
2
+ import UIKit
2
3
 
3
4
  extension ExpoBeaconModule {
4
5
  /// Starts the shared `CarPlayMonitor` and routes its events through the
@@ -6,17 +7,34 @@ extension ExpoBeaconModule {
6
7
  /// + lifecycle plugin registry). Idempotent — safe to call multiple times
7
8
  /// (see `CarPlayMonitor.start(emit:)` semantics).
8
9
  func startCarPlayMonitoringInternal() {
9
- CarPlayMonitor.shared.start { [weak self] eventName, payload in
10
- self?.sendLoggedEvent(eventName, payload)
11
- }
10
+ CarPlayMonitor.shared.start(emit: { [weak self] eventName, payload in
11
+ guard let self = self else { return }
12
+ self.sendLoggedEvent(eventName, payload)
13
+ switch eventName {
14
+ case "onCarPlayConnected":
15
+ self.postCarPlayNotification(
16
+ eventType: "connected",
17
+ transport: payload["transport"] as? String
18
+ )
19
+ case "onCarPlayDisconnected":
20
+ self.postCarPlayNotification(eventType: "disconnected", transport: nil)
21
+ default:
22
+ break
23
+ }
24
+ }, defaults: defaults)
12
25
  // Tier 2 fallback: subscribe to background-wake signals so suspended
13
26
  // apps still notice CarPlay route changes that happened off-process.
14
- // Skipped when the entitled CarPlay scene path is providing real-time
15
- // events that source keeps the app awake for the entire CarPlay
16
- // session and renders SLC/Visit redundant.
17
- if !CarPlayMonitor.shared.isUsingEntitledSource {
18
- startCarPlayBackgroundWakes()
19
- }
27
+ // Run this UNCONDITIONALLY (even when the entitled scene-delegate
28
+ // source is active) the scene delegate can fail to deliver a
29
+ // disconnect callback in edge cases (force-quit, OS reclaim, abrupt
30
+ // cable yank) and SLC + Visit are the only reconciliation safety net.
31
+ // Cost is negligible: no continuous GPS, just opportunistic wakes.
32
+ startCarPlayBackgroundWakes()
33
+ // Foreground reconciliation: when the user returns to the app, snapshot
34
+ // the current audio route and emit a connect/disconnect transition if
35
+ // it diverges from the cached state. Catches missed route-change
36
+ // notifications during long suspensions.
37
+ observeAppForegroundForCarPlay()
20
38
  }
21
39
 
22
40
  /// Start Significant Location Change + Visit monitoring as background-wake
@@ -46,4 +64,26 @@ extension ExpoBeaconModule {
46
64
  CarPlayMonitor.shared.resyncIfNeeded()
47
65
  }
48
66
  }
67
+
68
+ /// Register a `UIApplication.didBecomeActiveNotification` observer that
69
+ /// resyncs CarPlay state on foreground. Idempotent — the observer token is
70
+ /// cached on the module instance and re-registration is a no-op.
71
+ func observeAppForegroundForCarPlay() {
72
+ if carPlayForegroundObserver != nil { return }
73
+ carPlayForegroundObserver = NotificationCenter.default.addObserver(
74
+ forName: UIApplication.didBecomeActiveNotification,
75
+ object: nil,
76
+ queue: .main
77
+ ) { _ in
78
+ CarPlayMonitor.shared.resyncIfNeeded()
79
+ }
80
+ }
81
+
82
+ /// Tear down the foreground observer. Safe to call when none is registered.
83
+ func removeAppForegroundObserverForCarPlay() {
84
+ if let token = carPlayForegroundObserver {
85
+ NotificationCenter.default.removeObserver(token)
86
+ carPlayForegroundObserver = nil
87
+ }
88
+ }
49
89
  }
@@ -46,6 +46,48 @@ extension ExpoBeaconModule {
46
46
  UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
47
47
  }
48
48
 
49
+ /// Post a local notification when a CarPlay session connects or disconnects.
50
+ /// `eventType` is `"connected"` or `"disconnected"`. `transport` is the
51
+ /// transport string from the connect payload (e.g. "wired", "wireless");
52
+ /// pass `nil` on disconnect — the `{transport}` body placeholder is then
53
+ /// substituted with an empty string.
54
+ func postCarPlayNotification(eventType: String, transport: String?) {
55
+ let cfg = loadNotificationConfig()
56
+ let eventsCfg = cfg["carPlayEvents"] as? [String: Any]
57
+
58
+ // Respect the enabled flag (defaults to true)
59
+ if let enabled = eventsCfg?["enabled"] as? Bool, !enabled { return }
60
+
61
+ let defaultTitle = eventType == "connected" ? "CarPlay Connected" : "CarPlay Disconnected"
62
+ let title: String
63
+ switch eventType {
64
+ case "connected":
65
+ title = (eventsCfg?["connectedTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
66
+ default:
67
+ title = (eventsCfg?["disconnectedTitle"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? defaultTitle
68
+ }
69
+
70
+ let bodyTemplate = (eventsCfg?["body"] as? String).flatMap { $0.isEmpty ? nil : $0 }
71
+ ?? "CarPlay session {event}"
72
+ let body = bodyTemplate
73
+ .replacingOccurrences(of: "{event}", with: eventType)
74
+ .replacingOccurrences(of: "{transport}", with: transport ?? "")
75
+
76
+ let content = UNMutableNotificationContent()
77
+ content.title = title
78
+ content.body = body
79
+
80
+ let playSound = eventsCfg?["sound"] as? Bool ?? true
81
+ if playSound { content.sound = .default }
82
+
83
+ let request = UNNotificationRequest(
84
+ identifier: "carplay_\(eventType)_\(Date().timeIntervalSince1970)",
85
+ content: content,
86
+ trigger: nil
87
+ )
88
+ UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
89
+ }
90
+
49
91
  func loadNotificationConfig() -> [String: Any] {
50
92
  guard let json = self.defaults.string(forKey: NOTIFICATION_CONFIG_KEY),
51
93
  let data = json.data(using: .utf8) else { return [:] }
@@ -93,6 +93,13 @@ public class ExpoBeaconModule: Module {
93
93
 
94
94
  internal var permissionCompletion: ((Bool) -> Void)?
95
95
 
96
+ // MARK: - CarPlay
97
+
98
+ /// Observer token for `UIApplication.didBecomeActiveNotification` used to
99
+ /// resync CarPlay state on foreground. Owned by the CarPlay extension —
100
+ /// declared here so the module instance can hold it across calls.
101
+ internal var carPlayForegroundObserver: NSObjectProtocol?
102
+
96
103
  // MARK: - Cached paired data (invalidated on pair/unpair)
97
104
 
98
105
  internal var cachedPairedBeacons: [[String: Any]]?
@@ -137,6 +144,11 @@ public class ExpoBeaconModule: Module {
137
144
  // would be missed until JS calls startCarPlayMonitoring() again.
138
145
  if self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
139
146
  self.startCarPlayMonitoringInternal()
147
+ // Cross-process reconciliation: if the previous process recorded
148
+ // CarPlay as connected but the current audio route is no longer
149
+ // CarPlay, emit a synthetic disconnect so JS listeners attached
150
+ // to this freshly-created module learn the session ended.
151
+ CarPlayMonitor.shared.reconcileOnProcessStart()
140
152
  }
141
153
  }
142
154
 
@@ -455,7 +467,8 @@ public class ExpoBeaconModule: Module {
455
467
  self.startCarPlayMonitoringInternal()
456
468
  promise.resolve(nil)
457
469
  }
458
-
470
+ self.removeAppForegroundObserverForCarPlay()
471
+
459
472
  AsyncFunction("stopCarPlayMonitoring") { (promise: Promise) in
460
473
  self.defaults.set(false, forKey: CARPLAY_MONITORING_ENABLED_KEY)
461
474
  CarPlayMonitor.shared.stop()
@@ -620,7 +633,10 @@ public class ExpoBeaconModule: Module {
620
633
  // that route changes continue to be observed across module recreations
621
634
  // (e.g. background-launch wake → module re-init → OnDestroy on suspend).
622
635
  if !self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
623
- CarPlayMonitor.shared.stop()
636
+
637
+ // Foreground observer is bound to this module instance — always
638
+ // remove it on destroy to avoid leaking observers across recreations.
639
+ self.removeAppForegroundObserverForCarPlay() CarPlayMonitor.shared.stop()
624
640
  }
625
641
  self.centralManager?.stopScan()
626
642
  self.centralManager = nil
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",
@@ -112,14 +112,52 @@ export type NotificationChannelConfig = {
112
112
  importance?: "low" | "default" | "high";
113
113
  };
114
114
 
115
+ /** Configuration for CarPlay / Android Auto connect/disconnect notifications. */
116
+ export type CarPlayNotificationConfig = {
117
+ /** Whether to show CarPlay connect/disconnect notifications. Default: true. */
118
+ enabled?: boolean;
119
+ /** Notification title on CarPlay/Android Auto connect. Default: "CarPlay Connected". */
120
+ connectedTitle?: string;
121
+ /** Notification title on CarPlay/Android Auto disconnect. Default: "CarPlay Disconnected". */
122
+ disconnectedTitle?: string;
123
+ /**
124
+ * Notification body template. Supports `{event}` ("connected"/"disconnected") and
125
+ * `{transport}` (e.g. "wired", "wireless", "projection", "native", "unknown") placeholders.
126
+ * Note: `{transport}` is only meaningful for connect events; on disconnect it is replaced with an empty string.
127
+ * Default: "CarPlay session {event}".
128
+ */
129
+ body?: string;
130
+ /** Play a sound with the notification (iOS only). Default: true. */
131
+ sound?: boolean;
132
+ /** Android drawable resource name for the notification icon (e.g. "ic_notification"). */
133
+ icon?: string;
134
+ };
135
+
136
+ /** Configuration for the Android notification channel used for CarPlay events. */
137
+ export type CarPlayChannelConfig = {
138
+ /** Channel display name shown in system settings. Default: "CarPlay / Android Auto". */
139
+ name?: string;
140
+ /** Channel description shown in system settings. Default: "CarPlay and Android Auto connect/disconnect notifications". */
141
+ description?: string;
142
+ /**
143
+ * Channel importance level. Default: 'default' (so connect/disconnect events make a sound).
144
+ * Note: Android may ignore decreases in importance after first channel creation until the app is reinstalled.
145
+ */
146
+ importance?: "low" | "default" | "high";
147
+ };
148
+
115
149
  /** Combined notification configuration for all notification types. */
116
150
  export type NotificationConfig = {
117
151
  /** Settings for beacon enter/exit event notifications. */
118
152
  beaconEvents?: BeaconNotificationConfig;
153
+ /** Settings for CarPlay / Android Auto connect/disconnect notifications. */
154
+ carPlayEvents?: CarPlayNotificationConfig;
119
155
  /** Settings for the persistent foreground service notification (Android only). */
120
156
  foregroundService?: ForegroundServiceConfig;
121
157
  /** Settings for the Android notification channel (Android only). */
122
158
  channel?: NotificationChannelConfig;
159
+ /** Settings for the Android notification channel used for CarPlay events (Android only). */
160
+ carPlayChannel?: CarPlayChannelConfig;
123
161
  };
124
162
 
125
163
  /** Snapshot of the current monitoring configuration and active state. */
@@ -287,12 +325,25 @@ export type CarPlayConnectedEvent = {
287
325
  transport: CarPlayTransport;
288
326
  /** Timestamp in milliseconds since epoch. */
289
327
  timestamp: number;
328
+ /** ISO 8601 UTC representation of {@link timestamp} (e.g. "2026-05-12T14:23:45.678Z"). */
329
+ timestampIso?: string;
290
330
  };
291
331
 
292
332
  /** Payload fired when the device disconnects from a CarPlay or Android Auto session. */
293
333
  export type CarPlayDisconnectedEvent = {
294
334
  /** Timestamp in milliseconds since epoch. */
295
335
  timestamp: number;
336
+ /** ISO 8601 UTC representation of {@link timestamp} (e.g. "2026-05-12T14:23:45.678Z"). */
337
+ timestampIso?: string;
338
+ /**
339
+ * Reason this disconnect was emitted. Absent for normal real-time disconnects.
340
+ * `"reconciled"` (iOS only) indicates the disconnect was synthesized after the
341
+ * module was recreated in a new process and detected that the previously
342
+ * persisted CarPlay state no longer matches the current audio route — i.e.
343
+ * the disconnect happened off-process (force-quit, OS reclaim, abrupt cable
344
+ * yank) and is being delivered post-hoc.
345
+ */
346
+ reason?: "reconciled";
296
347
  };
297
348
 
298
349
  /** Payload for native beacon error events (monitoring/ranging failures). */
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export type {
14
14
  MonitoringConfig,
15
15
  MonitoredDeviceState,
16
16
  BeaconNotificationConfig,
17
+ CarPlayNotificationConfig,
18
+ CarPlayChannelConfig,
17
19
  ForegroundServiceConfig,
18
20
  NotificationChannelConfig,
19
21
  EddystoneFrameType,