56abe28a82
CrashlyticsService was wired only for the is_premium custom key. Route the swallowed errors in the main catch blocks through CrashlyticsService.record with a context tag, so non-fatals show up in Firebase (StoreKit load/purchase/ restore, calendar access/create/update/delete, reset-all-data, onboarding save). Debug prints kept. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
148 lines
5.3 KiB
Swift
148 lines
5.3 KiB
Swift
import EventKit
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class CalendarService {
|
|
static let shared = CalendarService()
|
|
private let eventStore = EKEventStore()
|
|
|
|
private init() {}
|
|
|
|
func requestAccess() async -> Bool {
|
|
do {
|
|
return try await eventStore.requestFullAccessToEvents()
|
|
} catch {
|
|
CrashlyticsService.record(error, context: "calendar_access")
|
|
print("Calendar access error: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func availableCalendars() -> [EKCalendar] {
|
|
eventStore.calendars(for: .event)
|
|
}
|
|
|
|
func createEvent(
|
|
slot: MealSlot,
|
|
dishName: String,
|
|
dishDescription: String?,
|
|
weekStartDate: Date,
|
|
settings: AppSettings
|
|
) -> String? {
|
|
guard let calendar = resolvedCalendar(for: settings) else {
|
|
print("Calendar sync skipped: no writable calendar available")
|
|
return nil
|
|
}
|
|
|
|
let event = EKEvent(eventStore: eventStore)
|
|
|
|
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
|
|
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
|
|
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
|
|
event.title = "\(prefix)\(mealName): \(dishName)"
|
|
|
|
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
|
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
|
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
|
|
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
|
event.notes = dishDescription
|
|
event.calendar = calendar
|
|
|
|
if let reminder = settings.reminderMinutesBefore {
|
|
event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60)))
|
|
}
|
|
|
|
do {
|
|
try eventStore.save(event, span: .thisEvent)
|
|
return event.eventIdentifier
|
|
} catch {
|
|
CrashlyticsService.record(error, context: "calendar_create")
|
|
print("Error saving event: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func updateEvent(
|
|
eventId: String,
|
|
slot: MealSlot,
|
|
dishName: String,
|
|
dishDescription: String?,
|
|
weekStartDate: Date,
|
|
settings: AppSettings
|
|
) -> String? {
|
|
guard let event = eventStore.event(withIdentifier: eventId) else {
|
|
// Event was deleted externally — create a fresh one
|
|
return createEvent(
|
|
slot: slot,
|
|
dishName: dishName,
|
|
dishDescription: dishDescription,
|
|
weekStartDate: weekStartDate,
|
|
settings: settings
|
|
)
|
|
}
|
|
|
|
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
|
|
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
|
|
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
|
|
event.title = "\(prefix)\(mealName): \(dishName)"
|
|
|
|
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
|
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
|
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
|
|
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
|
event.notes = dishDescription
|
|
|
|
event.alarms?.forEach { event.removeAlarm($0) }
|
|
if let reminder = settings.reminderMinutesBefore {
|
|
event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60)))
|
|
}
|
|
|
|
do {
|
|
try eventStore.save(event, span: .thisEvent)
|
|
return event.eventIdentifier
|
|
} catch {
|
|
CrashlyticsService.record(error, context: "calendar_update")
|
|
print("Error updating event: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func deleteEvent(eventId: String) {
|
|
guard let event = eventStore.event(withIdentifier: eventId) else { return }
|
|
do {
|
|
try eventStore.remove(event, span: .thisEvent)
|
|
} catch {
|
|
CrashlyticsService.record(error, context: "calendar_delete")
|
|
print("Error deleting event: \(error)")
|
|
}
|
|
}
|
|
|
|
func updateEventsTime(slots: [MealSlot], weekStartDate: Date, settings: AppSettings) {
|
|
for slot in slots {
|
|
guard let eventId = slot.calendarEventId,
|
|
let event = eventStore.event(withIdentifier: eventId),
|
|
event.startDate >= Date() else { continue }
|
|
|
|
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
|
|
let newTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
|
|
event.startDate = combineDateAndTime(date: slotDate, time: newTime)
|
|
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
|
|
|
|
try? eventStore.save(event, span: .thisEvent)
|
|
}
|
|
}
|
|
|
|
private func resolvedCalendar(for settings: AppSettings) -> EKCalendar? {
|
|
if let calendarId = settings.calendarId,
|
|
let calendar = eventStore.calendar(withIdentifier: calendarId) {
|
|
return calendar
|
|
}
|
|
|
|
if let defaultCalendar = eventStore.defaultCalendarForNewEvents {
|
|
return defaultCalendar
|
|
}
|
|
|
|
return eventStore.calendars(for: .event).first
|
|
}
|
|
}
|