1.6.0 #10 ocultar saldos + Face ID · #11 widget interactivo + Siri

#10 Ocultar saldos: BalancePrivacyManager (balancesHidden + requireBiometricToReveal),
modifier .hiddenBalance() (blur + máscara de bullets, ancho estable), botón ojo en Dashboard,
revelar con Face ID vía AppLockService, sección Privacidad en Settings. Aplicado a total
portfolio, forecast, sources, goals, monthly summary.

#11 Siri/App Intents: NetWorthIntent ('¿cuál es mi patrimonio?' → 'Tu patrimonio es X'),
StreakIntent, 5 AppShortcuts con frases. Widget interactivo iOS 17: botón Refresh
(Button(intent:) → reloadAllTimelines, in-process) en medium/large, gated #available(iOS 17).
Reader read-only del AppGroup. Dialogs Siri refactorizados a String(localized:).

Localización: strings de #10 y de Siri en los 7 idiomas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
This commit is contained in:
alexandrev-tibco
2026-07-25 17:52:09 +02:00
parent 648c2bd805
commit 326be6db04
20 changed files with 587 additions and 8 deletions
@@ -7,6 +7,7 @@ struct DashboardView: View {
@EnvironmentObject var iapService: IAPService
@EnvironmentObject var accountStore: AccountStore
@EnvironmentObject var tabSelection: TabSelectionStore
@EnvironmentObject var balancePrivacy: BalancePrivacyManager
@StateObject private var viewModel: DashboardViewModel
@StateObject private var goalsViewModel = GoalsViewModel()
@State private var showingImport = false
@@ -84,6 +85,14 @@ struct DashboardView: View {
}
.popoverTip(QuickUpdateTip())
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
balancePrivacy.toggleHidden()
} label: {
Image(systemName: balancePrivacy.balancesHidden ? "eye.slash" : "eye")
}
.accessibilityLabel(balancePrivacy.balancesHidden ? Text("Show balances") : Text("Hide balances"))
}
ToolbarItem(placement: .navigationBarTrailing) {
accountFilterMenu
}
@@ -287,7 +296,7 @@ struct DashboardView: View {
switch section {
case .totalValue:
if config.isCollapsed {
CompactCard(title: "Portfolio", subtitle: viewModel.portfolioSummary.formattedTotalValue)
CompactCard(title: "Portfolio", subtitle: viewModel.portfolioSummary.formattedTotalValue, subtitleIsBalance: true)
} else {
TotalValueCard(
totalValue: viewModel.portfolioSummary.formattedTotalValue,
@@ -312,7 +321,8 @@ struct DashboardView: View {
sinceInceptionChange: viewModel.portfolioSummary.formattedAllTimeReturn
)
},
sparklineData: viewModel.evolutionData
sparklineData: viewModel.evolutionData,
balancesHidden: balancePrivacy.balancesHidden
)
}
case .monthlyCheckIn:
@@ -444,6 +454,7 @@ struct TotalValueCard: View {
var isSinceInceptionPositive: Bool = true
var onShareTap: (() -> Void)?
var sparklineData: [(date: Date, value: Decimal)] = []
var balancesHidden: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -466,6 +477,7 @@ struct TotalValueCard: View {
.foregroundColor(.primary)
.minimumScaleFactor(0.6)
.lineLimit(1)
.hiddenBalance(balancesHidden)
HStack(spacing: 8) {
HStack(spacing: 4) {
@@ -505,6 +517,7 @@ struct TotalValueCard: View {
value: forecast.formattedForecastValue,
positive: forecast.isPositiveGrowth
)
.hiddenBalance(balancesHidden)
} else {
Button {
onUnlockTap?()
@@ -1164,6 +1177,7 @@ struct MonthlySummaryCard: View {
.foregroundColor(.secondary)
Text(summary.formattedContributions)
.font(.subheadline.weight(.semibold))
.hiddenBalance()
}
Spacer()
@@ -1175,6 +1189,7 @@ struct MonthlySummaryCard: View {
Text("\(summary.formattedNetPerformance) (\(summary.formattedNetPerformancePercentage))")
.font(.subheadline.weight(.semibold))
.foregroundColor(summary.netPerformance >= 0 ? .positiveGreen : .negativeRed)
.hiddenBalance()
}
}
}
@@ -1245,14 +1260,22 @@ struct DashboardCustomizeView: View {
struct CompactCard: View {
let title: String
let subtitle: String
var subtitleIsBalance: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
if subtitleIsBalance {
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.hiddenBalance()
} else {
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)