From 0b01d6f563cb4f94f9cba4eae1a64c2b2f421335 Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Sun, 24 May 2026 08:42:22 +0200 Subject: [PATCH] Fix iCloud sync: chart and next check-in date stale on secondary device (build 32) - completedMonthKeys: remove UserDefaults-based MonthlyCheckInStore check (not synced via iCloud) - MonthlyCheckInCard.effectiveLastCheckInDate: use max() instead of ?? to prefer newer synced snapshot date - MonthlyCheckInView.lastCompletionDate: use max() of local + latest CoreData snapshot via @FetchRequest - FirebaseService.logPaywallShown: fix parameter key to paywall_trigger (matches GA4 custom dimension) Co-Authored-By: Claude Sonnet 4.6 --- PortfolioJournal.xcodeproj/project.pbxproj | 16 ++++++++-------- PortfolioJournal/Services/FirebaseService.swift | 2 +- .../ViewModels/DashboardViewModel.swift | 5 ++++- .../Views/Dashboard/DashboardView.swift | 5 ++++- .../Views/Dashboard/MonthlyCheckInView.swift | 15 ++++++++++++++- 5 files changed, 31 insertions(+), 12 deletions(-) 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 {