Merge e5f876472050f1351ce6bdc3785ea043d6343576 into 598f1826d8b2bc969aace2c6459824737667218c
This commit is contained in:
commit
bbd0c6758c
@ -50,6 +50,16 @@
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>OpenClaw can capture photos or short video clips when requested via the gateway.</string>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.</string>
|
||||
<key>NSCalendarsFullAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.</string>
|
||||
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your calendars to add events when you enable calendar access.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>OpenClaw uses your contacts so you can search and reference people while using the assistant.</string>
|
||||
<key>NSRemindersFullAccessUsageDescription</key>
|
||||
<string>OpenClaw uses your reminders to list, add, and complete tasks when you enable reminders access.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>OpenClaw discovers and connects to your OpenClaw gateway on the local network.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
|
||||
303
apps/ios/Sources/Settings/PrivacyAccessSectionView.swift
Normal file
303
apps/ios/Sources/Settings/PrivacyAccessSectionView.swift
Normal file
@ -0,0 +1,303 @@
|
||||
import Contacts
|
||||
import EventKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct PrivacyAccessSectionView: View {
|
||||
@State private var contactsStatus: CNAuthorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
|
||||
@State private var calendarStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
@State private var remindersStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .reminder)
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Privacy & Access") {
|
||||
permissionRow(
|
||||
title: "Contacts",
|
||||
icon: "person.crop.circle",
|
||||
status: statusText(for: contactsStatus),
|
||||
detail: "Search and add contacts from the assistant.",
|
||||
actionTitle: actionTitle(for: contactsStatus),
|
||||
action: handleContactsAction
|
||||
)
|
||||
|
||||
permissionRow(
|
||||
title: "Calendar (Add Events)",
|
||||
icon: "calendar.badge.plus",
|
||||
status: calendarWriteStatusText,
|
||||
detail: "Add events with least privilege.",
|
||||
actionTitle: calendarWriteActionTitle,
|
||||
action: handleCalendarWriteAction
|
||||
)
|
||||
|
||||
permissionRow(
|
||||
title: "Calendar (View Events)",
|
||||
icon: "calendar",
|
||||
status: calendarReadStatusText,
|
||||
detail: "List and read calendar events.",
|
||||
actionTitle: calendarReadActionTitle,
|
||||
action: handleCalendarReadAction
|
||||
)
|
||||
|
||||
permissionRow(
|
||||
title: "Reminders",
|
||||
icon: "checklist",
|
||||
status: remindersStatusText,
|
||||
detail: "List, add, and complete reminders.",
|
||||
actionTitle: remindersActionTitle,
|
||||
action: handleRemindersAction
|
||||
)
|
||||
}
|
||||
.onAppear { refreshAll() }
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
if phase == .active { refreshAll() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Permission Row
|
||||
|
||||
@ViewBuilder
|
||||
private func permissionRow(
|
||||
title: String,
|
||||
icon: String,
|
||||
status: String,
|
||||
detail: String,
|
||||
actionTitle: String?,
|
||||
action: (() -> Void)?
|
||||
) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Label(title, systemImage: icon)
|
||||
Spacer()
|
||||
Text(status)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundStyle(statusColor(for: status))
|
||||
}
|
||||
Text(detail)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
if let actionTitle, let action {
|
||||
Button(actionTitle, action: action)
|
||||
.font(.footnote)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private func statusColor(for status: String) -> Color {
|
||||
switch status {
|
||||
case "Allowed": return .green
|
||||
case "Not Set": return .orange
|
||||
case "Add-Only": return .yellow
|
||||
default: return .red
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Contacts
|
||||
|
||||
private func statusText(for cnStatus: CNAuthorizationStatus) -> String {
|
||||
switch cnStatus {
|
||||
case .authorized, .limited: return "Allowed"
|
||||
case .notDetermined: return "Not Set"
|
||||
case .denied, .restricted: return "Not Allowed"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private func actionTitle(for cnStatus: CNAuthorizationStatus) -> String? {
|
||||
switch cnStatus {
|
||||
case .notDetermined: return "Request Access"
|
||||
case .denied, .restricted: return "Open Settings"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleContactsAction() {
|
||||
switch contactsStatus {
|
||||
case .notDetermined:
|
||||
Task {
|
||||
let store = CNContactStore()
|
||||
_ = await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
|
||||
store.requestAccess(for: .contacts) { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
await MainActor.run { refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Calendar Write
|
||||
|
||||
private var calendarWriteStatusText: String {
|
||||
switch calendarStatus {
|
||||
case .authorized, .fullAccess, .writeOnly: return "Allowed"
|
||||
case .notDetermined: return "Not Set"
|
||||
case .denied, .restricted: return "Not Allowed"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private var calendarWriteActionTitle: String? {
|
||||
switch calendarStatus {
|
||||
case .notDetermined: return "Request Access"
|
||||
case .denied, .restricted: return "Open Settings"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCalendarWriteAction() {
|
||||
switch calendarStatus {
|
||||
case .notDetermined:
|
||||
Task {
|
||||
_ = await requestCalendarWriteOnly()
|
||||
await MainActor.run { refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Calendar Read
|
||||
|
||||
private var calendarReadStatusText: String {
|
||||
switch calendarStatus {
|
||||
case .authorized, .fullAccess: return "Allowed"
|
||||
case .writeOnly: return "Add-Only"
|
||||
case .notDetermined: return "Not Set"
|
||||
case .denied, .restricted: return "Not Allowed"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private var calendarReadActionTitle: String? {
|
||||
switch calendarStatus {
|
||||
case .notDetermined: return "Request Full Access"
|
||||
case .writeOnly: return "Upgrade to Full Access"
|
||||
case .denied, .restricted: return "Open Settings"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCalendarReadAction() {
|
||||
switch calendarStatus {
|
||||
case .notDetermined, .writeOnly:
|
||||
Task {
|
||||
_ = await requestCalendarFull()
|
||||
await MainActor.run { refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reminders
|
||||
|
||||
private var remindersStatusText: String {
|
||||
switch remindersStatus {
|
||||
case .authorized, .fullAccess: return "Allowed"
|
||||
case .writeOnly: return "Add-Only"
|
||||
case .notDetermined: return "Not Set"
|
||||
case .denied, .restricted: return "Not Allowed"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private var remindersActionTitle: String? {
|
||||
switch remindersStatus {
|
||||
case .notDetermined: return "Request Access"
|
||||
case .writeOnly: return "Upgrade to Full Access"
|
||||
case .denied, .restricted: return "Open Settings"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRemindersAction() {
|
||||
switch remindersStatus {
|
||||
case .notDetermined:
|
||||
Task {
|
||||
_ = await requestRemindersFull()
|
||||
await MainActor.run { refreshAll() }
|
||||
}
|
||||
case .writeOnly:
|
||||
Task {
|
||||
_ = await requestRemindersFull()
|
||||
await MainActor.run { refreshAll() }
|
||||
}
|
||||
case .denied, .restricted:
|
||||
openSettings()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func refreshAll() {
|
||||
contactsStatus = CNContactStore.authorizationStatus(for: .contacts)
|
||||
calendarStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
remindersStatus = EKEventStore.authorizationStatus(for: .reminder)
|
||||
}
|
||||
|
||||
private func requestCalendarWriteOnly() async -> Bool {
|
||||
let store = EKEventStore()
|
||||
if #available(iOS 17.0, *) {
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestWriteOnlyAccessToEvents { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestAccess(to: .event) { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func requestCalendarFull() async -> Bool {
|
||||
let store = EKEventStore()
|
||||
if #available(iOS 17.0, *) {
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestFullAccessToEvents { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestAccess(to: .event) { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func requestRemindersFull() async -> Bool {
|
||||
let store = EKEventStore()
|
||||
if #available(iOS 17.0, *) {
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestFullAccessToReminders { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestAccess(to: .reminder) { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openSettings() {
|
||||
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
@ -378,6 +378,8 @@ struct SettingsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
PrivacyAccessSectionView()
|
||||
|
||||
DisclosureGroup("Device Info") {
|
||||
TextField("Name", text: self.$displayName)
|
||||
Text(self.instanceId)
|
||||
|
||||
@ -134,6 +134,11 @@ targets:
|
||||
NSBonjourServices:
|
||||
- _openclaw-gw._tcp
|
||||
NSCameraUsageDescription: OpenClaw can capture photos or short video clips when requested via the gateway.
|
||||
NSCalendarsUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsFullAccessUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
|
||||
NSCalendarsWriteOnlyAccessUsageDescription: OpenClaw uses your calendars to add events when you enable calendar access.
|
||||
NSContactsUsageDescription: OpenClaw uses your contacts so you can search and reference people while using the assistant.
|
||||
NSRemindersFullAccessUsageDescription: OpenClaw uses your reminders to list, add, and complete tasks when you enable reminders access.
|
||||
NSLocationWhenInUseUsageDescription: OpenClaw uses your location when you allow location sharing.
|
||||
NSLocationAlwaysAndWhenInUseUsageDescription: OpenClaw can share your location in the background when you enable Always.
|
||||
NSMicrophoneUsageDescription: OpenClaw needs microphone access for voice wake.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user