#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:
@@ -0,0 +1,78 @@
|
||||
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"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user