Release 1.2.1: iCloud sync improvements + ASO multilingual metadata
iCloud sync: - Force viewContext.refreshAllObjects() on remote change notifications so data from other devices is picked up immediately without app restart - Call refreshFromCloudKit() on foreground to merge any changes made while the device was inactive - Wait up to 10s for initial CloudKit sync on launch before showing onboarding (shows "Checking iCloud..." during the wait) - New OnboardingICloudCheckView: shown on fresh installs with iCloud available, lets user restore from iCloud before starting onboarding from scratch Localization: - Added de, fr, it, ja, pt-BR lproj folders - New iCloud onboarding strings in en + es-ES (+ button literals) ASO metadata (fastlane): - Updated en-US: new subtitle, keywords, description (fixed "no cloud sync" claim), promotional text, release notes - Added full metadata for es-ES, de-DE, fr-FR, it, ja, pt-BR (63 files total) - All keyword fields validated ≤100 Unicode chars Infrastructure: - Gemfile + Gemfile.lock for fastlane - Scripts/archive_and_upload_appstore.sh for CI/CD Version: 1.2.1 (build 7) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -413,9 +413,7 @@ struct TotalValueCard: View {
|
||||
struct MonthlyCheckInCard: View {
|
||||
let lastUpdated: String
|
||||
let lastUpdatedDate: Date?
|
||||
@State private var showingStartOptions = false
|
||||
@State private var startDestinationActive = false
|
||||
@State private var shouldDuplicatePrevious = false
|
||||
@State private var cachedCompletionDate: Date?
|
||||
|
||||
private var effectiveLastCheckInDate: Date? {
|
||||
@@ -466,11 +464,46 @@ struct MonthlyCheckInCard: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// True during the first half of the month (before mid-month threshold) when the previous
|
||||
/// period's check-in should be offered for update instead of starting a new one.
|
||||
private var isBeforeMidMonth: Bool {
|
||||
guard effectiveLastCheckInDate != nil else { return false }
|
||||
let cal = Calendar.current
|
||||
let day = cal.component(.day, from: Date())
|
||||
let daysInMonth = cal.range(of: .day, in: .month, for: Date())?.count ?? 30
|
||||
return day < daysInMonth / 2
|
||||
}
|
||||
|
||||
/// Reference date to pass to MonthlyCheckInView depending on the current phase.
|
||||
private var navigationReferenceDate: Date {
|
||||
if isBeforeMidMonth {
|
||||
return Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date()
|
||||
}
|
||||
// Use day 25 so effectiveMonth always resolves to the current calendar month
|
||||
var comps = Calendar.current.dateComponents([.year, .month], from: Date())
|
||||
comps.day = 25
|
||||
return Calendar.current.date(from: comps) ?? Date()
|
||||
}
|
||||
|
||||
private var buttonLabel: String {
|
||||
if isBeforeMidMonth {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMMM"
|
||||
formatter.locale = .current
|
||||
let prev = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date()
|
||||
return String(format: NSLocalizedString("checkin_update_month", comment: ""), formatter.string(from: prev))
|
||||
}
|
||||
return NSLocalizedString("checkin_start_new", comment: "")
|
||||
}
|
||||
|
||||
private var currentMonthLabel: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "LLLL yyyy"
|
||||
let effectiveMonth = MonthlyCheckInStore.effectiveMonth(for: Date(), relativeTo: Date())
|
||||
return formatter.string(from: effectiveMonth)
|
||||
if isBeforeMidMonth {
|
||||
let prev = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date()
|
||||
return formatter.string(from: prev)
|
||||
}
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -521,9 +554,9 @@ struct MonthlyCheckInCard: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
showingStartOptions = true
|
||||
startDestinationActive = true
|
||||
} label: {
|
||||
Text("Start")
|
||||
Text(buttonLabel)
|
||||
.font(.headline.weight(.semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
@@ -535,7 +568,7 @@ struct MonthlyCheckInCard: View {
|
||||
NavigationLink(
|
||||
isActive: $startDestinationActive
|
||||
) {
|
||||
MonthlyCheckInView(duplicatePrevious: shouldDuplicatePrevious)
|
||||
MonthlyCheckInView(referenceDate: navigationReferenceDate)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -544,47 +577,6 @@ struct MonthlyCheckInCard: View {
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
.sheet(isPresented: $showingStartOptions) {
|
||||
VStack(spacing: 16) {
|
||||
Text("Start Monthly Check-in")
|
||||
.font(.title2.weight(.bold))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top, 24)
|
||||
|
||||
Button {
|
||||
showingStartOptions = false
|
||||
shouldDuplicatePrevious = false
|
||||
startDestinationActive = true
|
||||
} label: {
|
||||
Text("Start from scratch")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(Color(.systemGray5))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
}
|
||||
|
||||
Button {
|
||||
showingStartOptions = false
|
||||
shouldDuplicatePrevious = true
|
||||
startDestinationActive = true
|
||||
} label: {
|
||||
Text("Duplicate previous month")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(Color(.systemGray5))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
}
|
||||
|
||||
Button("Cancel", role: .cancel) {
|
||||
showingStartOptions = false
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.presentationDetents([.height(280)])
|
||||
}
|
||||
.onAppear {
|
||||
cachedCompletionDate = MonthlyCheckInStore.latestCompletionDate()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user