@mentra/crust 3.4.0-dev.945 → 3.4.0-dev.947

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.
@@ -3,6 +3,70 @@ import CoreBluetooth
3
3
  import CoreLocation
4
4
  import Foundation
5
5
 
6
+ /// A new manager's getter can precede Core Location's initial authorization callback.
7
+ /// Keep the manager and delegate alive until that callback, without requesting consent or location updates.
8
+ @MainActor
9
+ private final class PassiveLocationAuthorization: NSObject, @preconcurrency CLLocationManagerDelegate {
10
+ private var manager: CLLocationManager?
11
+ private var completed: ((CLAuthorizationStatus) -> Void)?
12
+
13
+ func start(_ completed: @escaping (CLAuthorizationStatus) -> Void) {
14
+ self.completed = completed
15
+ let manager = CLLocationManager()
16
+ self.manager = manager
17
+ manager.delegate = self
18
+ }
19
+
20
+ func stop() {
21
+ manager?.delegate = nil
22
+ manager = nil
23
+ completed = nil
24
+ }
25
+
26
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
27
+ guard manager === self.manager else { return }
28
+ completed?(manager.authorizationStatus)
29
+ }
30
+ }
31
+
32
+ /// One finite initial callback. Missing initialization stays unknown, including when the task is canceled.
33
+ @MainActor
34
+ final class InitialLocationAuthorization {
35
+ private var continuation: CheckedContinuation<CLAuthorizationStatus?, Never>?
36
+ private var timeout: Task<Void, Never>?
37
+ private var stop: (() -> Void)?
38
+
39
+ func observe(timeoutMs: UInt64 = 5000,
40
+ start: (@escaping (CLAuthorizationStatus) -> Void) -> Void,
41
+ stop: @escaping () -> Void) async -> CLAuthorizationStatus? {
42
+ self.stop = stop
43
+ return await withTaskCancellationHandler {
44
+ await withCheckedContinuation { continuation in
45
+ self.continuation = continuation
46
+ if Task.isCancelled { finish(nil); return }
47
+ timeout = Task { @MainActor in
48
+ do { try await Task.sleep(nanoseconds: timeoutMs * 1_000_000) }
49
+ catch { return }
50
+ self.finish(nil)
51
+ }
52
+ start { self.finish($0) }
53
+ }
54
+ } onCancel: {
55
+ Task { @MainActor in self.finish(nil) }
56
+ }
57
+ }
58
+
59
+ private func finish(_ status: CLAuthorizationStatus?) {
60
+ guard let continuation else { return }
61
+ self.continuation = nil
62
+ timeout?.cancel()
63
+ timeout = nil
64
+ stop?()
65
+ stop = nil
66
+ continuation.resume(returning: status)
67
+ }
68
+ }
69
+
6
70
  /// Passive app-owned observations. None of these getters requests consent,
7
71
  /// starts capture, creates a Bluetooth central, or joins a network.
8
72
  enum PermissionStatusReceipt {
@@ -43,10 +107,17 @@ enum PermissionStatusReceipt {
43
107
  }
44
108
  }
45
109
 
110
+ @MainActor
111
+ private static func initializedLocationStatus() async -> CLAuthorizationStatus? {
112
+ let source = PassiveLocationAuthorization()
113
+ return await InitialLocationAuthorization().observe(start: source.start, stop: source.stop)
114
+ }
115
+
46
116
  static func current() async -> [String: Any] {
47
117
  // The global service getter may block; only the manager's authorization
48
118
  // observation belongs to the main actor, which owns Core Location objects.
49
119
  let servicesEnabled = CLLocationManager.locationServicesEnabled()
120
+ let locationStatus = await initializedLocationStatus()
50
121
  return await MainActor.run {
51
122
  let bundle = Bundle.main
52
123
  let formatter = ISO8601DateFormatter()
@@ -65,7 +136,7 @@ enum PermissionStatusReceipt {
65
136
  ] as [String: Any],
66
137
  "locationServicesEnabled": servicesEnabled,
67
138
  "permissions": [
68
- "location": location(CLLocationManager().authorizationStatus),
139
+ "location": locationStatus.map(location) ?? state("initializationUnconfirmed", "unknown"),
69
140
  "camera": capture(AVCaptureDevice.authorizationStatus(for: .video)),
70
141
  "microphone": capture(AVCaptureDevice.authorizationStatus(for: .audio)),
71
142
  "bluetooth": bluetooth(CBManager.authorization),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/crust",
3
- "version": "3.4.0-dev.945",
3
+ "version": "3.4.0-dev.947",
4
4
  "description": "Mentra Native Module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  "license": "Apache-2.0",
36
36
  "homepage": "https://github.com/Mentra-Community/MentraOS/tree/dev/mobile/modules/crust#readme",
37
37
  "dependencies": {
38
- "@mentra/jspolyfill": "3.4.0-dev.945"
38
+ "@mentra/jspolyfill": "3.4.0-dev.947"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@expo/config-plugins": ">=8.0.0",