Files
InvestmentTrackerApp/PortfolioJournal/Utilities/BalancePrivacyManager.swift
T
alexandrev-tibco 326be6db04 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
2026-07-25 17:52:09 +02:00

67 lines
2.5 KiB
Swift

import SwiftUI
import Combine
/// Central store for the "Hide balances" privacy feature.
///
/// When `balancesHidden` is true, every monetary amount in the app renders
/// redacted (a blurred bullet mask) instead of the real value. This lets the
/// user open the app in public or take screenshots without exposing net worth.
///
/// If `requireBiometricToReveal` is enabled, un-hiding balances must first pass
/// Face ID / biometric auth (reusing `AppLockService`). On failure, balances
/// stay hidden.
///
/// State is persisted in `UserDefaults` so it survives relaunches, and mirrored
/// into `@Published` properties so SwiftUI views observing this object update
/// instantly when the user taps the quick toggle.
@MainActor
final class BalancePrivacyManager: ObservableObject {
static let shared = BalancePrivacyManager()
private enum Keys {
static let balancesHidden = "balancesHidden"
static let requireBiometricToReveal = "requireBiometricToReveal"
}
/// When true, all monetary amounts render redacted.
@Published var balancesHidden: Bool {
didSet { UserDefaults.standard.set(balancesHidden, forKey: Keys.balancesHidden) }
}
/// When true, revealing hidden balances requires biometric auth.
@Published var requireBiometricToReveal: Bool {
didSet { UserDefaults.standard.set(requireBiometricToReveal, forKey: Keys.requireBiometricToReveal) }
}
init() {
self.balancesHidden = UserDefaults.standard.bool(forKey: Keys.balancesHidden)
self.requireBiometricToReveal = UserDefaults.standard.bool(forKey: Keys.requireBiometricToReveal)
}
/// Toggle balance visibility. Hiding is always immediate. Revealing, when
/// `requireBiometricToReveal` is on and biometrics are available, runs a
/// Face ID prompt first and only reveals on success.
func toggleHidden() {
if balancesHidden {
reveal()
} else {
balancesHidden = true
}
}
/// Attempt to reveal balances, honoring the biometric requirement.
func reveal() {
guard balancesHidden else { return }
if requireBiometricToReveal && AppLockService.canUseBiometrics() {
AppLockService.authenticate(reason: "Reveal your balances") { [weak self] success in
if success {
self?.balancesHidden = false
}
// On failure, stay hidden do nothing.
}
} else {
balancesHidden = false
}
}
}