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:
alexandrev-tibco
2026-03-25 11:54:35 +01:00
parent 7cb5f92cf4
commit 10f6d0ca20
108 changed files with 4069 additions and 238 deletions
@@ -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()
}