426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
561 lines
20 KiB
Swift
561 lines
20 KiB
Swift
import Foundation
|
|
import Capacitor
|
|
import EventKit
|
|
import UIKit
|
|
|
|
@objc(AppleCalendarPlugin)
|
|
public class AppleCalendarPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
public let identifier = "AppleCalendarPlugin"
|
|
public let jsName = "AppleCalendar"
|
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
|
|
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
|
|
CAPPluginMethod(name: "listCalendars", returnType: CAPPluginReturnPromise),
|
|
CAPPluginMethod(name: "listEvents", returnType: CAPPluginReturnPromise),
|
|
CAPPluginMethod(name: "createEvent", returnType: CAPPluginReturnPromise),
|
|
CAPPluginMethod(name: "updateEvent", returnType: CAPPluginReturnPromise),
|
|
CAPPluginMethod(name: "deleteEvent", returnType: CAPPluginReturnPromise),
|
|
]
|
|
|
|
private let eventStore = EKEventStore()
|
|
private let maxTitleLength = 512
|
|
private let maxDescriptionLength = 20000
|
|
private let maxLocationLength = 1024
|
|
private let unsupportedRecurrenceFields = [
|
|
"recurrence",
|
|
"recurrenceRule",
|
|
"recurrenceRules",
|
|
"rrule",
|
|
]
|
|
private lazy var isoWithFractionalSeconds: ISO8601DateFormatter = {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return formatter
|
|
}()
|
|
private lazy var isoWithoutFractionalSeconds: ISO8601DateFormatter = {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime]
|
|
return formatter
|
|
}()
|
|
|
|
@objc public override func checkPermissions(_ call: CAPPluginCall) {
|
|
call.resolve(permissionResult())
|
|
}
|
|
|
|
@objc public override func requestPermissions(_ call: CAPPluginCall) {
|
|
let status = EKEventStore.authorizationStatus(for: .event)
|
|
if isGranted(status) || isDeniedOrRestricted(status) {
|
|
call.resolve(permissionResult())
|
|
return
|
|
}
|
|
|
|
if #available(iOS 17.0, *) {
|
|
eventStore.requestFullAccessToEvents { [weak self] _, error in
|
|
DispatchQueue.main.async {
|
|
var result = self?.permissionResult() ?? [
|
|
"calendar": "restricted",
|
|
"canRequest": false,
|
|
]
|
|
if let error {
|
|
result["reason"] = error.localizedDescription
|
|
}
|
|
call.resolve(result)
|
|
}
|
|
}
|
|
} else {
|
|
eventStore.requestAccess(to: .event) { [weak self] _, error in
|
|
DispatchQueue.main.async {
|
|
var result = self?.permissionResult() ?? [
|
|
"calendar": "restricted",
|
|
"canRequest": false,
|
|
]
|
|
if let error {
|
|
result["reason"] = error.localizedDescription
|
|
}
|
|
call.resolve(result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func listCalendars(_ call: CAPPluginCall) {
|
|
guard hasFullAccess() else {
|
|
call.resolve(permissionError())
|
|
return
|
|
}
|
|
let defaultCalendar = eventStore.defaultCalendarForNewEvents
|
|
let calendars = eventStore.calendars(for: .event).map {
|
|
calendarJson($0, defaultCalendar: defaultCalendar)
|
|
}
|
|
call.resolve(["ok": true, "calendars": calendars])
|
|
}
|
|
|
|
@objc func listEvents(_ call: CAPPluginCall) {
|
|
guard hasFullAccess() else {
|
|
call.resolve(permissionError())
|
|
return
|
|
}
|
|
guard let timeMin = parseDate(call.getString("timeMin") ?? ""),
|
|
let timeMax = parseDate(call.getString("timeMax") ?? ""),
|
|
timeMax > timeMin
|
|
else {
|
|
call.resolve(nativeError("Calendar event window is invalid."))
|
|
return
|
|
}
|
|
|
|
let requestedCalendarId = (call.getString("calendarId") ?? "")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
var calendars: [EKCalendar]? = nil
|
|
if !requestedCalendarId.isEmpty && requestedCalendarId != "all" {
|
|
guard let calendar = calendar(withIdentifier: requestedCalendarId, requireWritable: false) else {
|
|
call.resolve([
|
|
"ok": false,
|
|
"error": "not_found",
|
|
"message": "Apple Calendar was not found.",
|
|
])
|
|
return
|
|
}
|
|
calendars = [calendar]
|
|
}
|
|
|
|
let predicate = eventStore.predicateForEvents(
|
|
withStart: timeMin,
|
|
end: timeMax,
|
|
calendars: calendars
|
|
)
|
|
let events = eventStore.events(matching: predicate)
|
|
.sorted { $0.startDate < $1.startDate }
|
|
.map(eventJson)
|
|
call.resolve(["ok": true, "events": events])
|
|
}
|
|
|
|
@objc func createEvent(_ call: CAPPluginCall) {
|
|
guard hasFullAccess() else {
|
|
call.resolve(permissionError())
|
|
return
|
|
}
|
|
let event = EKEvent(eventStore: eventStore)
|
|
if let error = applyEventPayload(call, to: event, requireTitle: true) {
|
|
call.resolve(error)
|
|
return
|
|
}
|
|
do {
|
|
try eventStore.save(event, span: .thisEvent, commit: true)
|
|
call.resolve(["ok": true, "event": eventJson(event)])
|
|
} catch {
|
|
call.resolve(nativeError("Failed to create Apple Calendar event: \(error.localizedDescription)"))
|
|
}
|
|
}
|
|
|
|
@objc func updateEvent(_ call: CAPPluginCall) {
|
|
guard hasFullAccess() else {
|
|
call.resolve(permissionError())
|
|
return
|
|
}
|
|
guard let eventId = nonEmptyString(call.getString("eventId")) else {
|
|
call.resolve(nativeError("Calendar event id is required."))
|
|
return
|
|
}
|
|
guard let item = eventStore.calendarItem(withIdentifier: eventId) as? EKEvent else {
|
|
call.resolve([
|
|
"ok": false,
|
|
"error": "not_found",
|
|
"message": "Apple Calendar event was not found.",
|
|
])
|
|
return
|
|
}
|
|
guard item.calendar.allowsContentModifications else {
|
|
call.resolve(nativeError("Apple Calendar event is not writable."))
|
|
return
|
|
}
|
|
if let error = applyEventPayload(call, to: item, requireTitle: false) {
|
|
call.resolve(error)
|
|
return
|
|
}
|
|
do {
|
|
try eventStore.save(item, span: .thisEvent, commit: true)
|
|
call.resolve(["ok": true, "event": eventJson(item)])
|
|
} catch {
|
|
call.resolve(nativeError("Failed to update Apple Calendar event: \(error.localizedDescription)"))
|
|
}
|
|
}
|
|
|
|
@objc func deleteEvent(_ call: CAPPluginCall) {
|
|
guard hasFullAccess() else {
|
|
call.resolve(permissionError())
|
|
return
|
|
}
|
|
guard let eventId = nonEmptyString(call.getString("eventId")) else {
|
|
call.resolve(nativeError("Calendar event id is required."))
|
|
return
|
|
}
|
|
guard let event = eventStore.calendarItem(withIdentifier: eventId) as? EKEvent else {
|
|
call.resolve([
|
|
"ok": false,
|
|
"error": "not_found",
|
|
"message": "Apple Calendar event was not found.",
|
|
])
|
|
return
|
|
}
|
|
do {
|
|
try eventStore.remove(event, span: .thisEvent, commit: true)
|
|
call.resolve(["ok": true])
|
|
} catch {
|
|
call.resolve(nativeError("Failed to delete Apple Calendar event: \(error.localizedDescription)"))
|
|
}
|
|
}
|
|
|
|
private func permissionResult() -> [String: Any] {
|
|
let status = EKEventStore.authorizationStatus(for: .event)
|
|
return [
|
|
"calendar": permissionString(status),
|
|
"canRequest": permissionString(status) == "prompt",
|
|
"reason": NSNull(),
|
|
]
|
|
}
|
|
|
|
private func permissionString(_ status: EKAuthorizationStatus) -> String {
|
|
if isGranted(status) {
|
|
return "granted"
|
|
}
|
|
if isDenied(status) {
|
|
return "denied"
|
|
}
|
|
if isRestricted(status) {
|
|
return "restricted"
|
|
}
|
|
return "prompt"
|
|
}
|
|
|
|
private func hasFullAccess() -> Bool {
|
|
isGranted(EKEventStore.authorizationStatus(for: .event))
|
|
}
|
|
|
|
private func isGranted(_ status: EKAuthorizationStatus) -> Bool {
|
|
if #available(iOS 17.0, *) {
|
|
if status == .fullAccess {
|
|
return true
|
|
}
|
|
if status == .writeOnly {
|
|
return false
|
|
}
|
|
}
|
|
return status == .authorized
|
|
}
|
|
|
|
private func isDenied(_ status: EKAuthorizationStatus) -> Bool {
|
|
status == .denied
|
|
}
|
|
|
|
private func isRestricted(_ status: EKAuthorizationStatus) -> Bool {
|
|
if #available(iOS 17.0, *), status == .writeOnly {
|
|
return true
|
|
}
|
|
return status == .restricted
|
|
}
|
|
|
|
private func isDeniedOrRestricted(_ status: EKAuthorizationStatus) -> Bool {
|
|
isDenied(status) || isRestricted(status)
|
|
}
|
|
|
|
private func permissionError() -> [String: Any] {
|
|
[
|
|
"ok": false,
|
|
"error": "permission",
|
|
"message": "Apple Calendar access has not been granted.",
|
|
]
|
|
}
|
|
|
|
private func nativeError(_ message: String) -> [String: Any] {
|
|
[
|
|
"ok": false,
|
|
"error": "native_error",
|
|
"message": message,
|
|
]
|
|
}
|
|
|
|
private func nonEmptyString(_ value: String?) -> String? {
|
|
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!trimmed.isEmpty
|
|
else {
|
|
return nil
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
private func parseDate(_ value: String) -> Date? {
|
|
guard !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
return nil
|
|
}
|
|
if let date = isoWithFractionalSeconds.date(from: value) {
|
|
return date
|
|
}
|
|
return isoWithoutFractionalSeconds.date(from: value)
|
|
}
|
|
|
|
private func textValue(
|
|
_ call: CAPPluginCall,
|
|
key: String,
|
|
maxLength: Int,
|
|
required: Bool
|
|
) -> (value: String?, error: [String: Any]?) {
|
|
let rawValue = call.options[key]
|
|
if rawValue == nil || rawValue is NSNull {
|
|
if required {
|
|
return (nil, nativeError("Calendar event \(key) is required."))
|
|
}
|
|
return (nil, nil)
|
|
}
|
|
guard rawValue is String else {
|
|
return (nil, nativeError("Calendar event \(key) must be a string."))
|
|
}
|
|
guard let value = call.getString(key) else {
|
|
if required {
|
|
return (nil, nativeError("Calendar event \(key) is required."))
|
|
}
|
|
return (nil, nil)
|
|
}
|
|
guard value.count <= maxLength else {
|
|
return (nil, nativeError("Calendar event \(key) is too long."))
|
|
}
|
|
return (value, nil)
|
|
}
|
|
|
|
private func isoString(_ date: Date?) -> String {
|
|
guard let date else { return "" }
|
|
return isoWithFractionalSeconds.string(from: date)
|
|
}
|
|
|
|
private func calendar(withIdentifier identifier: String, requireWritable: Bool) -> EKCalendar? {
|
|
if identifier.isEmpty || identifier == "primary" {
|
|
if let calendar = eventStore.defaultCalendarForNewEvents,
|
|
!requireWritable || calendar.allowsContentModifications
|
|
{
|
|
return calendar
|
|
}
|
|
if requireWritable {
|
|
return eventStore.calendars(for: .event)
|
|
.first(where: { $0.allowsContentModifications })
|
|
}
|
|
return eventStore.defaultCalendarForNewEvents
|
|
}
|
|
guard let calendar = eventStore.calendars(for: .event)
|
|
.first(where: { $0.calendarIdentifier == identifier })
|
|
else {
|
|
return nil
|
|
}
|
|
if requireWritable && !calendar.allowsContentModifications {
|
|
return nil
|
|
}
|
|
return calendar
|
|
}
|
|
|
|
private func calendarJson(_ calendar: EKCalendar, defaultCalendar: EKCalendar?) -> [String: Any] {
|
|
let color = UIColor(cgColor: calendar.cgColor)
|
|
let components = color.resolvedColor(with: UITraitCollection.current).cgColor.components ?? []
|
|
let red = components.indices.contains(0) ? components[0] : 0
|
|
let green = components.indices.contains(1) ? components[1] : red
|
|
let blue = components.indices.contains(2) ? components[2] : red
|
|
let hex = String(
|
|
format: "#%02X%02X%02X",
|
|
Int(max(0, min(1, red)) * 255),
|
|
Int(max(0, min(1, green)) * 255),
|
|
Int(max(0, min(1, blue)) * 255)
|
|
)
|
|
return [
|
|
"calendarId": calendar.calendarIdentifier,
|
|
"summary": calendar.title,
|
|
"description": calendar.source.title,
|
|
"primary": calendar.calendarIdentifier == defaultCalendar?.calendarIdentifier,
|
|
"accessRole": calendar.allowsContentModifications ? "writer" : "reader",
|
|
"backgroundColor": hex,
|
|
"foregroundColor": NSNull(),
|
|
"timeZone": TimeZone.current.identifier,
|
|
"selected": true,
|
|
]
|
|
}
|
|
|
|
private func participantEmail(_ participant: EKParticipant) -> String? {
|
|
guard participant.url.scheme?.lowercased() == "mailto" else {
|
|
return nil
|
|
}
|
|
let raw = participant.url.absoluteString
|
|
let prefix = "mailto:"
|
|
guard raw.lowercased().hasPrefix(prefix) else {
|
|
return nil
|
|
}
|
|
let address = String(raw.dropFirst(prefix.count))
|
|
return address.removingPercentEncoding ?? address
|
|
}
|
|
|
|
private func participantStatus(_ status: EKParticipantStatus) -> String {
|
|
switch status {
|
|
case .unknown: return "unknown"
|
|
case .pending: return "pending"
|
|
case .accepted: return "accepted"
|
|
case .declined: return "declined"
|
|
case .tentative: return "tentative"
|
|
case .delegated: return "delegated"
|
|
case .completed: return "completed"
|
|
case .inProcess: return "in_process"
|
|
@unknown default: return "unknown"
|
|
}
|
|
}
|
|
|
|
private func participantJson(_ participant: EKParticipant) -> [String: Any] {
|
|
[
|
|
"email": participantEmail(participant) ?? NSNull(),
|
|
"displayName": participant.name ?? NSNull(),
|
|
"responseStatus": participantStatus(participant.participantStatus),
|
|
"self": participant.isCurrentUser,
|
|
"organizer": participant.participantRole == .chair,
|
|
"optional": participant.participantRole == .optional,
|
|
]
|
|
}
|
|
|
|
private func eventStatus(_ status: EKEventStatus) -> String {
|
|
switch status {
|
|
case .none: return "none"
|
|
case .confirmed: return "confirmed"
|
|
case .tentative: return "tentative"
|
|
case .canceled: return "cancelled"
|
|
@unknown default: return "unknown"
|
|
}
|
|
}
|
|
|
|
private func eventJson(_ event: EKEvent) -> [String: Any] {
|
|
let identifier = event.calendarItemIdentifier
|
|
return [
|
|
"id": identifier,
|
|
"externalId": identifier,
|
|
"calendarId": event.calendar.calendarIdentifier,
|
|
"calendarSummary": event.calendar.title,
|
|
"title": event.title?.isEmpty == false ? event.title as Any : "(untitled)",
|
|
"description": event.notes ?? "",
|
|
"location": event.location ?? "",
|
|
"status": eventStatus(event.status),
|
|
"startAt": isoString(event.startDate),
|
|
"endAt": isoString(event.endDate),
|
|
"isAllDay": event.isAllDay,
|
|
"timezone": event.timeZone?.identifier ?? NSNull(),
|
|
"htmlLink": NSNull(),
|
|
"conferenceLink": NSNull(),
|
|
"organizer": event.organizer.map(participantJson) ?? NSNull(),
|
|
"attendees": event.attendees?.map(participantJson) ?? [],
|
|
]
|
|
}
|
|
|
|
private func applyEventPayload(
|
|
_ call: CAPPluginCall,
|
|
to event: EKEvent,
|
|
requireTitle: Bool
|
|
) -> [String: Any]? {
|
|
for key in unsupportedRecurrenceFields where call.options.keys.contains(key) {
|
|
return [
|
|
"ok": false,
|
|
"error": "unsupported_feature",
|
|
"message": "Apple Calendar recurrence editing is not supported by this bridge.",
|
|
]
|
|
}
|
|
|
|
if call.options.keys.contains("attendees") {
|
|
guard let attendees = call.options["attendees"] as? [Any] else {
|
|
return nativeError("Calendar event attendees must be an array.")
|
|
}
|
|
if !attendees.isEmpty {
|
|
return [
|
|
"ok": false,
|
|
"error": "unsupported_feature",
|
|
"message": "Apple Calendar does not allow this app to create or edit event invitees through EventKit. Remove attendees or use Google Calendar for invited meetings.",
|
|
]
|
|
}
|
|
}
|
|
|
|
if call.options.keys.contains("title") || requireTitle {
|
|
let titleResult = textValue(
|
|
call,
|
|
key: "title",
|
|
maxLength: maxTitleLength,
|
|
required: true
|
|
)
|
|
if let error = titleResult.error {
|
|
return error
|
|
}
|
|
guard let title = nonEmptyString(titleResult.value) else {
|
|
return nativeError("Calendar event title is required.")
|
|
}
|
|
event.title = title
|
|
}
|
|
if call.options.keys.contains("description") {
|
|
let descriptionResult = textValue(
|
|
call,
|
|
key: "description",
|
|
maxLength: maxDescriptionLength,
|
|
required: false
|
|
)
|
|
if let error = descriptionResult.error {
|
|
return error
|
|
}
|
|
event.notes = descriptionResult.value
|
|
}
|
|
if call.options.keys.contains("location") {
|
|
let locationResult = textValue(
|
|
call,
|
|
key: "location",
|
|
maxLength: maxLocationLength,
|
|
required: false
|
|
)
|
|
if let error = locationResult.error {
|
|
return error
|
|
}
|
|
event.location = locationResult.value
|
|
}
|
|
if call.options.keys.contains("timeZone") {
|
|
guard let timeZoneName = nonEmptyString(call.getString("timeZone")) else {
|
|
return nativeError("Calendar event timeZone is invalid.")
|
|
}
|
|
guard let timeZone = TimeZone(identifier: timeZoneName) else {
|
|
return nativeError("Calendar event timeZone is invalid.")
|
|
}
|
|
event.timeZone = timeZone
|
|
}
|
|
if call.options.keys.contains("calendarId") {
|
|
guard let calendar = calendar(
|
|
withIdentifier: call.getString("calendarId") ?? "",
|
|
requireWritable: true
|
|
) else {
|
|
return nativeError("The selected Apple Calendar is not writable or was not found.")
|
|
}
|
|
event.calendar = calendar
|
|
}
|
|
if call.options.keys.contains("isAllDay") {
|
|
event.isAllDay = call.getBool("isAllDay") ?? false
|
|
}
|
|
if call.options.keys.contains("startAt") {
|
|
guard let start = parseDate(call.getString("startAt") ?? "") else {
|
|
return nativeError("Calendar event startAt is invalid.")
|
|
}
|
|
event.startDate = start
|
|
}
|
|
if call.options.keys.contains("endAt") {
|
|
guard let end = parseDate(call.getString("endAt") ?? "") else {
|
|
return nativeError("Calendar event endAt is invalid.")
|
|
}
|
|
event.endDate = end
|
|
}
|
|
guard event.startDate != nil, event.endDate != nil else {
|
|
return nativeError("Calendar event startAt and endAt are required.")
|
|
}
|
|
guard event.endDate > event.startDate else {
|
|
return nativeError("Calendar event endAt must be later than startAt.")
|
|
}
|
|
if event.calendar == nil {
|
|
guard let calendar = calendar(withIdentifier: "primary", requireWritable: true) else {
|
|
return nativeError("No writable Apple Calendar is available.")
|
|
}
|
|
event.calendar = calendar
|
|
}
|
|
return nil
|
|
}
|
|
}
|