fix(macos): onboarding location + layout
This commit is contained in:
@@ -157,13 +157,18 @@ struct GeneralSettings: View {
|
||||
|
||||
private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool {
|
||||
guard mode != .off else { return true }
|
||||
guard CLLocationManager.locationServicesEnabled() else {
|
||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||
return false
|
||||
}
|
||||
|
||||
let status = CLLocationManager().authorizationStatus
|
||||
// Note: macOS only supports authorizedAlways, not authorizedWhenInUse (iOS only)
|
||||
if status == .authorizedAlways {
|
||||
let requireAlways = mode == .always
|
||||
if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) {
|
||||
return true
|
||||
}
|
||||
let updated = await LocationPermissionRequester.shared.request(always: mode == .always)
|
||||
return updated == .authorizedAlways
|
||||
let updated = await LocationPermissionRequester.shared.request(always: requireAlways)
|
||||
return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways)
|
||||
}
|
||||
|
||||
private var connectionSection: some View {
|
||||
|
||||
@@ -222,7 +222,15 @@ actor MacNodeRuntime {
|
||||
(Self.locationPreciseEnabled() ? .precise : .balanced)
|
||||
let services = await self.mainActorServices()
|
||||
let status = await services.locationAuthorizationStatus()
|
||||
if status != .authorizedAlways {
|
||||
let hasPermission = switch mode {
|
||||
case .always:
|
||||
status == .authorizedAlways
|
||||
case .whileUsing:
|
||||
status == .authorizedAlways
|
||||
case .off:
|
||||
false
|
||||
}
|
||||
if !hasPermission {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
|
||||
@@ -32,7 +32,7 @@ final class OnboardingController {
|
||||
let hosting = NSHostingController(rootView: OnboardingView())
|
||||
let window = NSWindow(contentViewController: hosting)
|
||||
window.title = UIStrings.welcomeTitle
|
||||
window.setContentSize(NSSize(width: 630, height: 684))
|
||||
window.setContentSize(NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight))
|
||||
window.styleMask = [.titled, .closable, .fullSizeContentView]
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.titleVisibility = .hidden
|
||||
@@ -98,7 +98,10 @@ struct OnboardingView: View {
|
||||
@Bindable var state: AppState
|
||||
var permissionMonitor: PermissionMonitor
|
||||
|
||||
let pageWidth: CGFloat = 630
|
||||
static let windowWidth: CGFloat = 630
|
||||
static let windowHeight: CGFloat = 752 // ~+10% to fit full onboarding content
|
||||
|
||||
let pageWidth: CGFloat = Self.windowWidth
|
||||
let contentHeight: CGFloat = 460
|
||||
let connectionPageIndex = 1
|
||||
let anthropicAuthPageIndex = 2
|
||||
|
||||
@@ -27,7 +27,7 @@ extension OnboardingView {
|
||||
Spacer(minLength: 0)
|
||||
self.navigationBar
|
||||
}
|
||||
.frame(width: self.pageWidth, height: 684)
|
||||
.frame(width: self.pageWidth, height: Self.windowHeight)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
.onAppear {
|
||||
self.currentPage = 0
|
||||
|
||||
@@ -10,6 +10,10 @@ import Speech
|
||||
import UserNotifications
|
||||
|
||||
enum PermissionManager {
|
||||
static func isLocationAuthorized(status: CLAuthorizationStatus, requireAlways _: Bool) -> Bool {
|
||||
status == .authorizedAlways
|
||||
}
|
||||
|
||||
static func ensure(_ caps: [Capability], interactive: Bool) async -> [Capability: Bool] {
|
||||
var results: [Capability: Bool] = [:]
|
||||
for cap in caps {
|
||||
@@ -138,18 +142,23 @@ enum PermissionManager {
|
||||
}
|
||||
|
||||
private static func ensureLocation(interactive: Bool) async -> Bool {
|
||||
guard CLLocationManager.locationServicesEnabled() else {
|
||||
if interactive {
|
||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||
}
|
||||
return false
|
||||
}
|
||||
let status = CLLocationManager().authorizationStatus
|
||||
switch status {
|
||||
// Note: macOS only supports authorizedAlways, not authorizedWhenInUse (iOS only)
|
||||
case .authorizedAlways:
|
||||
return true
|
||||
case .notDetermined:
|
||||
guard interactive else { return false }
|
||||
let updated = await LocationPermissionRequester.shared.request(always: false)
|
||||
return updated == .authorizedAlways
|
||||
return self.isLocationAuthorized(status: updated, requireAlways: false)
|
||||
case .denied, .restricted:
|
||||
if interactive {
|
||||
LocationPermissionHelper.openSettings()
|
||||
await MainActor.run { LocationPermissionHelper.openSettings() }
|
||||
}
|
||||
return false
|
||||
@unknown default:
|
||||
@@ -202,8 +211,8 @@ enum PermissionManager {
|
||||
|
||||
case .location:
|
||||
let status = CLLocationManager().authorizationStatus
|
||||
// Note: macOS only supports authorizedAlways
|
||||
results[cap] = status == .authorizedAlways
|
||||
results[cap] = CLLocationManager.locationServicesEnabled()
|
||||
&& self.isLocationAuthorized(status: status, requireAlways: false)
|
||||
}
|
||||
}
|
||||
return results
|
||||
@@ -282,13 +291,21 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
|
||||
}
|
||||
|
||||
func request(always: Bool) async -> CLAuthorizationStatus {
|
||||
if always {
|
||||
self.manager.requestAlwaysAuthorization()
|
||||
} else {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
let current = self.manager.authorizationStatus
|
||||
if PermissionManager.isLocationAuthorized(status: current, requireAlways: always) {
|
||||
return current
|
||||
}
|
||||
|
||||
return await withCheckedContinuation { cont in
|
||||
self.continuation = cont
|
||||
if always {
|
||||
self.manager.requestAlwaysAuthorization()
|
||||
} else {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
|
||||
// On macOS, requesting an actual fix makes the prompt more reliable.
|
||||
self.manager.requestLocation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,14 @@
|
||||
<string>Clawdbot captures the screen when the agent needs screenshots for context.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Clawdbot can capture photos or short video clips when requested by the agent.</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>Clawdbot can share your location when requested by the agent.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Clawdbot needs the mic for Voice Wake tests and agent audio capture.</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>Clawdbot can share your location when requested by the agent.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Clawdbot can share your location when requested by the agent.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Clawdbot can share your location when requested by the agent.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Clawdbot needs the mic for Voice Wake tests and agent audio capture.</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>Clawdbot uses speech recognition to detect your Voice Wake trigger phrase.</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import CoreLocation
|
||||
import Testing
|
||||
|
||||
@testable import Clawdbot
|
||||
|
||||
@Suite("PermissionManager Location")
|
||||
struct PermissionManagerLocationTests {
|
||||
@Test("authorizedAlways counts for both modes")
|
||||
func authorizedAlwaysCountsForBothModes() {
|
||||
#expect(PermissionManager.isLocationAuthorized(status: .authorizedAlways, requireAlways: false))
|
||||
#expect(PermissionManager.isLocationAuthorized(status: .authorizedAlways, requireAlways: true))
|
||||
}
|
||||
|
||||
@Test("other statuses not authorized")
|
||||
func otherStatusesNotAuthorized() {
|
||||
#expect(!PermissionManager.isLocationAuthorized(status: .notDetermined, requireAlways: false))
|
||||
#expect(!PermissionManager.isLocationAuthorized(status: .denied, requireAlways: false))
|
||||
#expect(!PermissionManager.isLocationAuthorized(status: .restricted, requireAlways: false))
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,10 @@ public struct ClawdbotChatView: View {
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
ClawdbotChatTheme.background
|
||||
.ignoresSafeArea()
|
||||
if self.style == .standard {
|
||||
ClawdbotChatTheme.background
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
VStack(spacing: Layout.stackSpacing) {
|
||||
self.messageList
|
||||
|
||||
@@ -146,6 +146,8 @@ cat > "$ENT_TMP_APP_BASE" <<'PLIST'
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
@@ -176,6 +178,8 @@ cat > "$ENT_TMP_APP" <<'PLIST'
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
Reference in New Issue
Block a user