Release 1.4.0 (build 31): retención, quick update, streak, what's new

- 1A: Notificación mensual de resumen de portfolio (día 5 de cada mes)
- 1B: Badge de racha mensual en Dashboard (streak counter)
- 1C: Empty states mejorados en Goals y Journal con CTAs claros
- 2A: Quick Update sheet — actualiza todas las fuentes desde una pantalla
- 2B: Notificación batch update redirige al Quick Update sheet
- 2C: Widget deep link portfoliojournal://quickupdate abre Quick Update
- 3A: App Store version check banner en Settings
- 3C: What's New sheet en primer launch de versión nueva
- Localización completa en 7 idiomas para todas las nuevas strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-05-21 23:09:36 +02:00
parent 773da6800b
commit b48a47ce10
41 changed files with 2659 additions and 483 deletions
@@ -22,6 +22,7 @@ class DashboardViewModel: ObservableObject {
// MARK: - Chart Data
@Published var evolutionData: [(date: Date, value: Decimal)] = []
@Published var updateStreak: Int = 0
// MARK: - Portfolio Forecast
@@ -204,6 +205,9 @@ class DashboardViewModel: ObservableObject {
// Calculate portfolio forecast
updatePortfolioForecast()
// Compute update streak
updateStreak = computeStreak(from: evolutionData)
// Log screen view
FirebaseService.shared.logScreenView(screenName: "Dashboard")
}
@@ -535,6 +539,42 @@ class DashboardViewModel: ObservableObject {
return annualized
}
private func computeStreak(from evolutionData: [(date: Date, value: Decimal)]) -> Int {
guard !evolutionData.isEmpty else { return 0 }
let calendar = Calendar.current
// Group into (year, month) set
let monthSet: Set<DateComponents> = Set(evolutionData.map { point in
let comps = calendar.dateComponents([.year, .month], from: point.date)
return DateComponents(year: comps.year, month: comps.month)
})
// Get sorted unique months descending
let sortedMonths = monthSet
.compactMap { comps -> Date? in calendar.date(from: comps) }
.sorted(by: >)
guard let mostRecent = sortedMonths.first else { return 0 }
var streak = 1
var current = mostRecent
for i in 1..<sortedMonths.count {
guard let expected = calendar.date(byAdding: .month, value: -1, to: current) else { break }
let prev = sortedMonths[i]
let prevComps = calendar.dateComponents([.year, .month], from: prev)
let expectedComps = calendar.dateComponents([.year, .month], from: expected)
if prevComps.year == expectedComps.year && prevComps.month == expectedComps.month {
streak += 1
current = prev
} else {
break
}
}
return streak
}
// Virtual snapshot for forecast calculations
private struct VirtualSnapshot {
let date: Date