expo-beacon 0.8.4 → 0.8.5

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.
@@ -36,13 +36,14 @@ final class CarPlayMonitor {
36
36
  /// Optional persistence target for last-known connection state. Injected via
37
37
  /// `start(emit:defaults:)`. When set, every connect/disconnect emission is
38
38
  /// mirrored to `CARPLAY_LAST_CONNECTED_KEY` so a freshly relaunched process
39
- /// can detect a missed disconnect via `reconcileOnProcessStart(emit:)`.
39
+ /// can detect a missed disconnect via `reconcileOnProcessStart()`.
40
40
  private var defaults: UserDefaults?
41
41
  /// When `true`, an authoritative source (CarPlay scene delegate, granted via the
42
42
  /// `com.apple.developer.carplay-driving-task` entitlement) is providing
43
43
  /// connect/disconnect events. The audio-session observer becomes a passive
44
44
  /// secondary signal only — it will not emit events, to avoid duplicate
45
- /// connect/disconnect notifications.
45
+ /// connect/disconnect notifications. Reset to `false` on entitled disconnect
46
+ /// so the audio-session fallback can take over for any subsequent events.
46
47
  private var isEntitledMode: Bool = false
47
48
 
48
49
  private init() {}
@@ -55,7 +56,7 @@ final class CarPlayMonitor {
55
56
  /// - Parameter defaults: Optional `UserDefaults` suite used to persist the
56
57
  /// last-known connection state for cross-process reconciliation. When the
57
58
  /// module is recreated in a new process (e.g. after a background-wake)
58
- /// the persisted value is used by `reconcileOnProcessStart(emit:)` to
59
+ /// the persisted value is used by `reconcileOnProcessStart()` to
59
60
  /// synthesize a missed disconnect. Pass `nil` to disable persistence.
60
61
  func start(emit: @escaping Emit, defaults: UserDefaults? = nil) {
61
62
  queue.async { [weak self] in
@@ -128,7 +129,14 @@ final class CarPlayMonitor {
128
129
  self.isEntitledMode = true
129
130
  os_log("CarPlay scene connected (entitled source)", log: self.log, type: .info)
130
131
  if !self.isConnected {
131
- self.isConnected = trueclears the entitled-mode flag so the
132
+ self.isConnected = true
133
+ self.emitConnected(transport: transport)
134
+ }
135
+ }
136
+ }
137
+
138
+ /// Called by `BeaconCarPlaySceneDelegate.templateApplicationScene(_:didDisconnect:)`.
139
+ /// Emits `onCarPlayDisconnected` and clears the entitled-mode flag so the
132
140
  /// audio-session fallback path becomes authoritative again until the next
133
141
  /// entitled connect. Without this reset, a single missed scene-delegate
134
142
  /// disconnect (force-quit, OS reclaim, abrupt cable yank between connect
@@ -139,13 +147,6 @@ final class CarPlayMonitor {
139
147
  guard let self = self else { return }
140
148
  os_log("CarPlay scene disconnected (entitled source)", log: self.log, type: .info)
141
149
  self.isEntitledMode = false
142
- /// Emits `onCarPlayDisconnected` and keeps the entitled-mode flag set so
143
- /// subsequent audio-session events remain suppressed (the scene delegate is
144
- /// the source of truth for the lifetime of the process).
145
- func notifyEntitledDisconnect() {
146
- queue.async { [weak self] in
147
- guard let self = self else { return }
148
- os_log("CarPlay scene disconnected (entitled source)", log: self.log, type: .info)
149
150
  if self.isConnected {
150
151
  self.isConnected = false
151
152
  self.emitDisconnected()
@@ -173,6 +174,17 @@ final class CarPlayMonitor {
173
174
  /// is always trusted.
174
175
  private func handleRouteChange(notification: Notification?) {
175
176
  if let userInfo = notification?.userInfo,
177
+ let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
178
+ let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw) {
179
+ switch reason {
180
+ case .newDeviceAvailable, .oldDeviceUnavailable:
181
+ break // real device change — proceed
182
+ default:
183
+ // Category/override/configuration changes etc. don't represent
184
+ // a CarPlay connect/disconnect. Skip to avoid spurious events.
185
+ return
186
+ }
187
+ }
176
188
  let (connected, transport) = Self.currentCarPlayState()
177
189
  // When an entitled CarPlay scene source is active it is authoritative.
178
190
  // The audio-session signal is kept as a redundant secondary check but
@@ -203,24 +215,15 @@ final class CarPlayMonitor {
203
215
  /// route is no longer CarPlay. Use case: the previous process was killed or
204
216
  /// suspended-then-OS-reclaimed while CarPlay was connected, and the disconnect
205
217
  /// 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)
218
+ /// would otherwise never learn the session ended.
219
+ ///
220
+ /// Must be called AFTER `start(emit:defaults:)` so the emit callback is
221
+ /// installed and persistence target is known. Idempotent.
222
+ func reconcileOnProcessStart() {
223
+ queue.async { [weak self] in
224
+ guard let self = self else { return }
225
+ guard let defaults = self.defaults else { return }
226
+ let persistedConnected = defaults.bool(forKey: CARPLAY_LAST_CONNECTED_KEY)
224
227
  let (currentConnected, _) = Self.currentCarPlayState()
225
228
  if persistedConnected && !currentConnected {
226
229
  os_log("CarPlay reconcile: persisted=connected, current=disconnected — emitting synthetic disconnect", log: self.log, type: .info)
@@ -233,18 +236,7 @@ final class CarPlayMonitor {
233
236
  /// Write the current connection state to the injected `UserDefaults` suite
234
237
  /// (when available). No-op when persistence wasn't configured.
235
238
  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.
237
- if isEntitledMode {
238
- return
239
- }
240
- let (connected, transport) = Self.currentCarPlayState()
241
- if connected == isConnected { return }
242
- isConnected = connected
243
- if connected {
244
- emitConnected(transport: transport)
245
- } else {
246
- emitDisconnected()
247
- }
239
+ defaults?.set(connected, forKey: CARPLAY_LAST_CONNECTED_KEY)
248
240
  }
249
241
 
250
242
  private func emitConnected(transport: String) {
@@ -254,15 +246,24 @@ final class CarPlayMonitor {
254
246
  "timestamp": now.timeIntervalSince1970 * 1000.0,
255
247
  "timestampIso": Self.isoFormatter.string(from: now),
256
248
  ]
249
+ persistConnectionState(true)
257
250
  emit?("onCarPlayConnected", payload)
258
251
  }
259
252
 
260
- private func emitDisconnected() {
253
+ /// Emit a disconnect event. When `reason` is non-nil it is included in the
254
+ /// payload so consumers can distinguish real-time disconnects from
255
+ /// post-hoc reconciled ones (currently `"reconciled"` from
256
+ /// `reconcileOnProcessStart`). Additive, non-breaking.
257
+ private func emitDisconnected(reason: String? = nil) {
261
258
  let now = Date()
262
- let payload: [String: Any] = [
259
+ var payload: [String: Any] = [
263
260
  "timestamp": now.timeIntervalSince1970 * 1000.0,
264
261
  "timestampIso": Self.isoFormatter.string(from: now),
265
262
  ]
263
+ if let reason = reason {
264
+ payload["reason"] = reason
265
+ }
266
+ persistConnectionState(false)
266
267
  emit?("onCarPlayDisconnected", payload)
267
268
  }
268
269
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
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",