diff --git a/PortfolioJournal.xcodeproj/project.pbxproj b/PortfolioJournal.xcodeproj/project.pbxproj index 7e26ebe..6e14595 100644 --- a/PortfolioJournal.xcodeproj/project.pbxproj +++ b/PortfolioJournal.xcodeproj/project.pbxproj @@ -459,7 +459,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournalDebug.entitlements; ENABLE_USER_SCRIPT_SANDBOXING = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -498,7 +498,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournal.entitlements; ENABLE_USER_SCRIPT_SANDBOXING = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -655,7 +655,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -688,7 +688,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -719,7 +719,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -743,7 +743,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -766,7 +766,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -789,7 +789,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; diff --git a/PortfolioJournal/Services/FirebaseService.swift b/PortfolioJournal/Services/FirebaseService.swift index 7b26da1..20f5a8f 100644 --- a/PortfolioJournal/Services/FirebaseService.swift +++ b/PortfolioJournal/Services/FirebaseService.swift @@ -70,7 +70,7 @@ class FirebaseService { func logPaywallShown(trigger: String) { guard isConfigured else { return } Analytics.logEvent("paywall_shown", parameters: [ - "trigger": trigger + "paywall_trigger": trigger ]) } diff --git a/PortfolioJournal/ViewModels/DashboardViewModel.swift b/PortfolioJournal/ViewModels/DashboardViewModel.swift index a12d031..e3fc29a 100644 --- a/PortfolioJournal/ViewModels/DashboardViewModel.swift +++ b/PortfolioJournal/ViewModels/DashboardViewModel.swift @@ -353,7 +353,10 @@ class DashboardViewModel: ObservableObject { for (key, monthSnapshots) in groupedByMonth { guard let monthDate = Calendar.current.date(from: key) else { continue } guard monthDate > cutoff else { continue } - guard MonthlyCheckInStore.completionDate(for: monthDate) != nil else { continue } + // A month is "complete" if all active sources have snapshot data for it. + // We intentionally do NOT require a MonthlyCheckInStore entry here because + // that store lives in UserDefaults (device-local) and is not synced via iCloud. + // Snapshot data from CoreData is the authoritative cross-device source of truth. let monthSourceIds = Set(monthSnapshots.compactMap { $0.source?.id }) if sourceIds.isSubset(of: monthSourceIds) { completed.insert(key) diff --git a/PortfolioJournal/Views/Dashboard/DashboardView.swift b/PortfolioJournal/Views/Dashboard/DashboardView.swift index 5ee8b25..743ba92 100644 --- a/PortfolioJournal/Views/Dashboard/DashboardView.swift +++ b/PortfolioJournal/Views/Dashboard/DashboardView.swift @@ -599,7 +599,10 @@ struct MonthlyCheckInCard: View { @State private var cachedCompletionDate: Date? private var effectiveLastCheckInDate: Date? { - cachedCompletionDate ?? lastUpdatedDate + // Use whichever is more recent: local completion record or latest snapshot date from CoreData. + // cachedCompletionDate lives in UserDefaults (device-local, not iCloud-synced), so on a + // secondary device after iCloud sync the snapshot date may be newer than the local record. + [cachedCompletionDate, lastUpdatedDate].compactMap { $0 }.max() } private var checkInProgress: Double { diff --git a/PortfolioJournal/Views/Dashboard/MonthlyCheckInView.swift b/PortfolioJournal/Views/Dashboard/MonthlyCheckInView.swift index 37a5304..95a24a1 100644 --- a/PortfolioJournal/Views/Dashboard/MonthlyCheckInView.swift +++ b/PortfolioJournal/Views/Dashboard/MonthlyCheckInView.swift @@ -1,9 +1,17 @@ import SwiftUI +import CoreData struct MonthlyCheckInView: View { @Environment(\.openURL) private var openURL @EnvironmentObject var accountStore: AccountStore @StateObject private var viewModel = MonthlyCheckInViewModel() + + // Provides the latest snapshot date from CoreData (synced via iCloud) as fallback + // for lastCompletionDate when local MonthlyCheckInStore records are stale/missing. + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Snapshot.date, ascending: false)], + animation: .none + ) private var latestSnapshots: FetchedResults @State private var referenceDate: Date @State private var monthlyNote: String @@ -24,7 +32,12 @@ struct MonthlyCheckInView: View { } private var lastCompletionDate: Date? { - MonthlyCheckInStore.latestCompletionDate() + // MonthlyCheckInStore lives in UserDefaults (device-local, not iCloud-synced). + // On a secondary device after iCloud sync the latest snapshot from CoreData may be + // newer than the local check-in record. Use whichever is more recent. + let localCompletion = MonthlyCheckInStore.latestCompletionDate() + let latestSnapshotDate = latestSnapshots.first?.date + return [localCompletion, latestSnapshotDate].compactMap { $0 }.max() } private var checkInProgress: Double {