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.