diff --git a/apps/ios/Sources/Calendar/CalendarService.swift b/apps/ios/Sources/Calendar/CalendarService.swift index 94b2d9ea3f5..e11a6ee4075 100644 --- a/apps/ios/Sources/Calendar/CalendarService.swift +++ b/apps/ios/Sources/Calendar/CalendarService.swift @@ -6,7 +6,12 @@ final class CalendarService: CalendarServicing { func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload { let store = EKEventStore() let status = EKEventStore.authorizationStatus(for: .event) - let authorized = EventKitAuthorization.allowsRead(status: status) + let authorized: Bool + if status == .notDetermined { + authorized = await Self.requestEventAccess(store: store) + } else { + authorized = EventKitAuthorization.allowsRead(status: status) + } guard authorized else { throw NSError(domain: "Calendar", code: 1, userInfo: [ NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission", @@ -39,7 +44,12 @@ final class CalendarService: CalendarServicing { func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload { let store = EKEventStore() let status = EKEventStore.authorizationStatus(for: .event) - let authorized = EventKitAuthorization.allowsWrite(status: status) + let authorized: Bool + if status == .notDetermined { + authorized = await Self.requestWriteOnlyEventAccess(store: store) + } else { + authorized = EventKitAuthorization.allowsWrite(status: status) + } guard authorized else { throw NSError(domain: "Calendar", code: 2, userInfo: [ NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission", @@ -95,6 +105,38 @@ final class CalendarService: CalendarServicing { return OpenClawCalendarAddPayload(event: payload) } + private static func requestEventAccess(store: EKEventStore) async -> Bool { + if #available(iOS 17.0, *) { + return await withCheckedContinuation { continuation in + store.requestFullAccessToEvents { granted, _ in + continuation.resume(returning: granted) + } + } + } + + return await withCheckedContinuation { continuation in + store.requestAccess(to: .event) { granted, _ in + continuation.resume(returning: granted) + } + } + } + + private static func requestWriteOnlyEventAccess(store: EKEventStore) async -> Bool { + if #available(iOS 17.0, *) { + return await withCheckedContinuation { continuation in + store.requestWriteOnlyAccessToEvents { granted, _ in + continuation.resume(returning: granted) + } + } + } + + return await withCheckedContinuation { continuation in + store.requestAccess(to: .event) { granted, _ in + continuation.resume(returning: granted) + } + } + } + private static func resolveCalendar( store: EKEventStore, calendarId: String?, diff --git a/apps/ios/Sources/Contacts/ContactsService.swift b/apps/ios/Sources/Contacts/ContactsService.swift index efe89f8a218..d38bc7a923c 100644 --- a/apps/ios/Sources/Contacts/ContactsService.swift +++ b/apps/ios/Sources/Contacts/ContactsService.swift @@ -103,8 +103,7 @@ final class ContactsService: ContactsServicing { case .authorized, .limited: return true case .notDetermined: - // Don’t prompt during node.invoke; the caller should instruct the user to grant permission. - // Prompts block the invoke and lead to timeouts in headless flows. + // Avoid prompting during node.invoke; headless/unattended flows should fail fast. return false case .restricted, .denied: return false diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index 5908021fad3..8950acf4b01 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -50,6 +50,14 @@ 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. NSLocalNetworkUsageDescription OpenClaw discovers and connects to your OpenClaw gateway on the local network. NSLocationAlwaysAndWhenInUseUsageDescription diff --git a/apps/ios/project.yml b/apps/ios/project.yml index 53e6489a25b..f345eac5baf 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -134,6 +134,10 @@ 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. 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.