326be6db04
#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
79 lines
2.9 KiB
Swift
79 lines
2.9 KiB
Swift
import SwiftUI
|
|
|
|
/// A view modifier that redacts monetary amounts when the "Hide balances"
|
|
/// privacy feature is active.
|
|
///
|
|
/// When `hidden` is true the underlying content is blurred and its value masked
|
|
/// with a bullet string, while keeping the original layout footprint so the UI
|
|
/// doesn't jump. When false, the real content shows through untouched.
|
|
///
|
|
/// Usage:
|
|
/// ```swift
|
|
/// Text(viewModel.formattedTotalValue)
|
|
/// .hiddenBalance() // observes BalancePrivacyManager.shared
|
|
/// // or explicitly:
|
|
/// Text(amount).hiddenBalance(isHidden)
|
|
/// ```
|
|
struct HiddenBalanceModifier: ViewModifier {
|
|
let hidden: Bool
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.opacity(hidden ? 0 : 1)
|
|
.overlay {
|
|
if hidden {
|
|
// Bullet mask sized to the underlying content so layout width
|
|
// stays roughly stable and the amount is fully obscured.
|
|
GeometryReader { proxy in
|
|
Text("••••••")
|
|
.redacted(reason: .placeholder)
|
|
.blur(radius: 6)
|
|
.frame(width: proxy.size.width, height: proxy.size.height)
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
}
|
|
.accessibilityLabel(hidden ? Text("Balance hidden") : Text(""))
|
|
}
|
|
}
|
|
|
|
/// A self-observing variant of `HiddenBalanceModifier` that subscribes to the
|
|
/// shared `BalancePrivacyManager` so any monetary label updates live when the
|
|
/// user toggles visibility — without threading the flag through view params.
|
|
struct ObservingHiddenBalanceModifier: ViewModifier {
|
|
@ObservedObject private var privacy = BalancePrivacyManager.shared
|
|
|
|
func body(content: Content) -> some View {
|
|
content.modifier(HiddenBalanceModifier(hidden: privacy.balancesHidden))
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// Redact this monetary label with an explicit hidden flag. Use inside views
|
|
/// that already observe `BalancePrivacyManager`.
|
|
func hiddenBalance(_ hidden: Bool) -> some View {
|
|
modifier(HiddenBalanceModifier(hidden: hidden))
|
|
}
|
|
|
|
/// Redact this monetary label, self-observing the shared privacy manager.
|
|
/// Use anywhere without threading the flag through view parameters.
|
|
func hiddenBalance() -> some View {
|
|
modifier(ObservingHiddenBalanceModifier())
|
|
}
|
|
}
|
|
|
|
/// A tap-to-reveal wrapper for the quick eye toggle. Renders an eye / eye.slash
|
|
/// button bound to the shared `BalancePrivacyManager`.
|
|
struct BalanceVisibilityToggle: View {
|
|
@ObservedObject var privacy = BalancePrivacyManager.shared
|
|
|
|
var body: some View {
|
|
Button {
|
|
privacy.toggleHidden()
|
|
} label: {
|
|
Image(systemName: privacy.balancesHidden ? "eye.slash" : "eye")
|
|
}
|
|
.accessibilityLabel(privacy.balancesHidden ? Text("Show balances") : Text("Hide balances"))
|
|
}
|
|
}
|