Consolidate extension data providers and add side effects catch-up

- Create unified ExtensionDataProvider for Widget and Watch targets
- Remove duplicate WatchDataProvider and WatchConnectivityManager from Watch App
- Add side effects catch-up mechanism in MoodLogger for widget votes
- Process pending side effects on app launch and midnight background task
- Reduce ~450 lines of duplicated code across targets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-21 22:50:23 -06:00
parent 83aad66f26
commit f7da61d6ca
12 changed files with 306 additions and 530 deletions

View File

@@ -7,6 +7,7 @@
import Foundation
import WidgetKit
import os.log
/// Centralized service for logging moods with all associated side effects.
/// All mood entry points should use this service to ensure consistent behavior.
@@ -14,6 +15,11 @@ import WidgetKit
final class MoodLogger {
static let shared = MoodLogger()
private static let logger = Logger(subsystem: "com.tt.ifeel", category: "MoodLogger")
/// Key for tracking the last date side effects were applied
private static let lastSideEffectsDateKey = "lastSideEffectsAppliedDate"
private init() {}
/// Log a mood entry with all associated side effects.
@@ -35,10 +41,30 @@ final class MoodLogger {
// 1. Add mood entry to data store
DataController.shared.add(mood: mood, forDate: date, entryType: entryType)
// Apply side effects and mark as complete
applySideEffects(mood: mood, for: date, syncHealthKit: syncHealthKit, updateTips: updateTips)
}
/// Apply side effects for a mood entry without saving the entry itself.
/// Used for catch-up when widget saved data but couldn't run side effects.
///
/// - Parameters:
/// - mood: The mood that was logged
/// - date: The date of the mood entry
/// - syncHealthKit: Whether to sync to HealthKit
/// - updateTips: Whether to update TipKit parameters
func applySideEffects(
mood: Mood,
for date: Date,
syncHealthKit: Bool = true,
updateTips: Bool = true
) {
// Skip side effects for placeholder/missing moods
guard mood != .missing && mood != .placeholder else { return }
// 2. Sync to HealthKit if enabled and requested
Self.logger.info("Applying side effects for mood \(mood.rawValue) on \(date)")
// 1. Sync to HealthKit if enabled and requested
if syncHealthKit {
let healthKitEnabled = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.healthKitEnabled.rawValue)
if healthKitEnabled {
@@ -48,27 +74,76 @@ final class MoodLogger {
}
}
// 3. Calculate current streak for Live Activity and TipKit
// 2. Calculate current streak for Live Activity and TipKit
let streak = calculateCurrentStreak()
// 4. Update Live Activity
// 3. Update Live Activity
LiveActivityManager.shared.updateActivity(streak: streak, mood: mood)
LiveActivityScheduler.shared.scheduleForNextDay()
// 5. Update TipKit parameters if requested
// 4. Update TipKit parameters if requested
if updateTips {
TipsManager.shared.onMoodLogged()
TipsManager.shared.updateStreak(streak)
}
// 6. Request app review at moments of delight
// 5. Request app review at moments of delight
ReviewRequestManager.shared.onMoodLogged(streak: streak)
// 7. Reload widgets
// 6. Reload widgets
WidgetCenter.shared.reloadAllTimelines()
// 8. Notify watch to refresh complications
// 7. Notify watch to refresh complications
WatchConnectivityManager.shared.notifyWatchToReload()
// 8. Mark side effects as applied for this date
markSideEffectsApplied(for: date)
}
/// Check for and process any pending side effects from widget/extension votes.
/// Call this when the app becomes active to ensure all side effects are applied.
func processPendingSideEffects() {
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
let dayStart = Calendar.current.startOfDay(for: votingDate)
let dayEnd = Calendar.current.date(byAdding: .day, value: 1, to: dayStart)!
// Check if there's an entry for the current voting date
guard let entry = DataController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first,
entry.mood != .missing && entry.mood != .placeholder else {
Self.logger.debug("No valid mood entry for today, skipping side effects catch-up")
return
}
// Check if side effects were already applied for this date
if sideEffectsApplied(for: votingDate) {
Self.logger.debug("Side effects already applied for \(votingDate)")
return
}
// Apply the missing side effects
Self.logger.info("Catching up side effects for widget/watch vote: \(entry.mood.rawValue)")
applySideEffects(mood: entry.mood, for: entry.forDate)
}
// MARK: - Side Effects Tracking
/// Mark that side effects have been applied for a given date
private func markSideEffectsApplied(for date: Date) {
let dateString = ISO8601DateFormatter().string(from: Calendar.current.startOfDay(for: date))
GroupUserDefaults.groupDefaults.set(dateString, forKey: Self.lastSideEffectsDateKey)
}
/// Check if side effects have been applied for a given date
private func sideEffectsApplied(for date: Date) -> Bool {
guard let lastDateString = GroupUserDefaults.groupDefaults.string(forKey: Self.lastSideEffectsDateKey),
let lastDate = ISO8601DateFormatter().date(from: lastDateString) else {
return false
}
let targetDay = Calendar.current.startOfDay(for: date)
let lastDay = Calendar.current.startOfDay(for: lastDate)
return targetDay == lastDay
}
/// Calculate the current mood streak