// // SharedMoodIntent.swift // Reflect // // Single VoteMoodIntent for all targets. // Main app uses ForegroundContinuableIntent to run widget intents in app process. // // Add this file to ALL targets: Reflect (iOS), ReflectWidgetExtension, Reflect Watch App // import AppIntents import SwiftUI import SwiftData import WidgetKit import os.log // MARK: - Vote Mood Intent struct VoteMoodIntent: AppIntent { static var title: LocalizedStringResource = "Vote Mood" static var description = IntentDescription("Record your mood for today") static var openAppWhenRun: Bool = false @Parameter(title: "Mood") var moodValue: Int init() { self.moodValue = 2 } init(mood: Mood) { self.moodValue = mood.rawValue } @MainActor func perform() async throws -> some IntentResult { let mood = Mood(rawValue: moodValue) ?? .average #if os(watchOS) // Watch: Send to iPhone via WatchConnectivity let date = Date() _ = WatchConnectivityManager.shared.sendMoodToPhone(mood: moodValue, date: date) WidgetCenter.shared.reloadAllTimelines() #elseif WIDGET_EXTENSION // Widget: Save to shared container, main app handles side effects let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) WidgetMoodSaver.save(mood: mood, date: votingDate) WidgetCenter.shared.reloadAllTimelines() #else // Main app: Full logging with all side effects let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) MoodLogger.shared.logMood(mood, for: votingDate, entryType: .widget) let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: votingDate)) GroupUserDefaults.groupDefaults.set(dateString, forKey: UserDefaultsStore.Keys.lastVotedDate.rawValue) #endif return .result() } } // MARK: - Main App: Run widget intents in app process #if !WIDGET_EXTENSION && !os(watchOS) extension VoteMoodIntent: ForegroundContinuableIntent {} #endif // MARK: - Widget: Save to shared container #if WIDGET_EXTENSION enum WidgetMoodSaver { private static let logger = Logger(subsystem: "com.88oakapps.reflect.widget", category: "WidgetMoodSaver") private static var cachedContainer: ModelContainer? @MainActor static func save(mood: Mood, date: Date) { do { let container = try getOrCreateContainer() try performSave(mood: mood, date: date, container: container) } catch { // Container may be stale or corrupted — discard cache and retry once logger.warning("First save attempt failed, retrying with fresh container: \(error.localizedDescription)") cachedContainer = nil do { let container = try getOrCreateContainer() try performSave(mood: mood, date: date, container: container) } catch { logger.error("Failed to save mood after retry: \(error.localizedDescription)") } } } private static func getOrCreateContainer() throws -> ModelContainer { if let existing = cachedContainer { return existing } let schema = Schema([MoodEntryModel.self]) let appGroupID = Constants.currentGroupShareId guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else { logger.error("App Group not available") throw WidgetMoodSaverError.appGroupUnavailable } #if DEBUG let storeURL = containerURL.appendingPathComponent("Reflect-Debug.store") #else let storeURL = containerURL.appendingPathComponent("Reflect.store") #endif let config = ModelConfiguration(schema: schema, url: storeURL, cloudKitDatabase: .none) let container = try ModelContainer(for: schema, configurations: [config]) cachedContainer = container return container } @MainActor private static func performSave(mood: Mood, date: Date, container: ModelContainer) throws { let context = container.mainContext // Delete existing entry for this date let startDate = Calendar.current.startOfDay(for: date) let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)! let descriptor = FetchDescriptor( predicate: #Predicate { entry in entry.forDate >= startDate && entry.forDate < endDate } ) let existing = try context.fetch(descriptor) if !existing.isEmpty { for entry in existing { context.delete(entry) } try context.save() } // Create new entry let entry = MoodEntryModel(forDate: date, mood: mood, entryType: .widget) context.insert(entry) try context.save() logger.info("Saved mood \(mood.rawValue) from widget") // Note: The widget cannot run full side effects (HealthKit, streaks, analytics, etc.) // because it runs in a separate extension process without access to MoodLogger. // When the main app returns to the foreground, it calls // MoodLogger.shared.processPendingSideEffects() to catch up on any side effects // that were missed from widget or watch entries. } enum WidgetMoodSaverError: LocalizedError { case appGroupUnavailable var errorDescription: String? { switch self { case .appGroupUnavailable: return "App Group container is not available" } } } } #endif