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")) } }