Home: carrusel de charts con gating premium (candado) + aviso de próximo check-in

- EvolutionChartCard: swipe entre 7 charts (evolution, allocation libres; performance,
  drawdown, volatility, prediction, year-over-year premium). No premium ve las premium
  con candado + CTA de unlock; premium las ve reales. Reutiliza ChartsViewModel (computa
  solo la página visible) y las vistas del feature Charts.
- MonthlyCheckInCard: aviso de próximo check-in siempre visible (fecha + badge de
  días restantes/atrasado), con fallback cuando no hay check-in previo.
- Localización de nuevas cadenas en los 7 idiomas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
This commit is contained in:
alexandrev-tibco
2026-07-21 09:39:26 +02:00
parent 2a5ffca48f
commit 95c689d2b8
9 changed files with 219 additions and 22 deletions
@@ -309,7 +309,9 @@ struct DashboardView: View {
goals: goalsViewModel.goals,
allocation: viewModel.categoryMetrics.map {
(category: $0.categoryName, value: $0.totalValue, color: $0.colorHex)
}
},
iapService: iapService,
onRequestUpgrade: { viewModel.showingPaywall = true }
)
}
case .categoryBreakdown:
@@ -572,9 +574,22 @@ struct MonthlyCheckInCard: View {
}
private var nextCheckInDate: Date? {
guard let last = effectiveLastCheckInDate else { return nil }
let effective = MonthlyCheckInStore.effectiveMonth(for: last, relativeTo: last)
return effective.adding(months: 1).endOfMonth
if let last = effectiveLastCheckInDate {
let effective = MonthlyCheckInStore.effectiveMonth(for: last, relativeTo: last)
return effective.adding(months: 1).endOfMonth
}
// No prior check-in yet still surface a due date (end of the current
// effective month) so the reminder is always visible on the Home card.
let effectiveNow = MonthlyCheckInStore.effectiveMonth(for: Date(), relativeTo: Date())
return effectiveNow.endOfMonth
}
/// Short urgency badge shown next to the next check-in date.
private var deadlineBadgeText: String {
guard let days = daysUntilDeadline else { return "" }
if days < 0 { return String(localized: "checkin_overdue") }
if days == 0 { return String(localized: "checkin_due_today") }
return String(format: String(localized: "checkin_days_left"), days)
}
private var isOverdue: Bool {
@@ -785,9 +800,28 @@ struct MonthlyCheckInCard: View {
}
if let nextDate = nextCheckInDate {
Text("Next check-in: \(nextDate.mediumDateString)")
.font(.caption)
.foregroundColor(isOverdue ? .red : .secondary)
HStack(spacing: 6) {
Image(systemName: "calendar")
.font(.caption)
.foregroundColor(isOverdue ? .red : .secondary)
Text(String(format: String(localized: "checkin_next_due"), nextDate.mediumDateString))
.font(.caption)
.foregroundColor(isOverdue ? .red : .secondary)
Spacer()
if !deadlineBadgeText.isEmpty {
Text(deadlineBadgeText)
.font(.caption2.weight(.semibold))
.foregroundColor(progressBarTint)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(progressBarTint.opacity(0.12))
.clipShape(Capsule())
}
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(Color(.tertiarySystemFill))
.cornerRadius(10)
}
Button {