From 62cdc3dd8fa34b951ad9fbf4d10c56e00d50d7f1 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Tue, 3 Feb 2026 12:43:17 +0100 Subject: [PATCH] feat(ui): #314 prevent mnemonic screenshots --- Bitkit/Components/ScreenshotPreventMask.swift | 48 +++++++++++++++++++ Bitkit/Views/Backup/BackupMnemonic.swift | 2 +- .../Views/Onboarding/RestoreWalletView.swift | 9 +++- .../Recovery/RecoveryMnemonicScreen.swift | 2 +- 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 Bitkit/Components/ScreenshotPreventMask.swift diff --git a/Bitkit/Components/ScreenshotPreventMask.swift b/Bitkit/Components/ScreenshotPreventMask.swift new file mode 100644 index 00000000..f47a6230 --- /dev/null +++ b/Bitkit/Components/ScreenshotPreventMask.swift @@ -0,0 +1,48 @@ +import SwiftUI +import UIKit + +struct ScreenShotPreventMask: UIViewRepresentable { + func makeUIView(context: Context) -> UIView { + let view = UITextField() + view.isSecureTextEntry = true + view.text = "" + view.isUserInteractionEnabled = false + + if let autoHideLayer = findAutoHideLayer(in: view) { + autoHideLayer.backgroundColor = UIColor.white.cgColor + } else { + view.layer.sublayers?.last?.backgroundColor = UIColor.white.cgColor + } + + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} + + private func findAutoHideLayer(in view: UIView) -> CALayer? { + if let layers = view.layer.sublayers { + if let layer = layers.first(where: { layer in + layer.delegate.debugDescription.contains("UITextLayoutCanvasView") + }) { + return layer + } + } + + return nil + } +} + +extension View { + @ViewBuilder + func screenshotPreventMask(_ isEnabled: Bool) -> some View { + if isEnabled { + mask( + ScreenShotPreventMask() + .ignoresSafeArea() + ) + .background(EmptyView()) + } else { + self + } + } +} diff --git a/Bitkit/Views/Backup/BackupMnemonic.swift b/Bitkit/Views/Backup/BackupMnemonic.swift index fa81fd71..e0190cea 100644 --- a/Bitkit/Views/Backup/BackupMnemonic.swift +++ b/Bitkit/Views/Backup/BackupMnemonic.swift @@ -53,7 +53,7 @@ struct BackupMnemonicView: View { .padding(32) .background(Color.gray6) .blur(radius: showMnemonic ? 0 : 5) - .privacySensitive() + .screenshotPreventMask(true) .accessibilityElement(children: .ignore) .accessibilityIdentifier("SeedContainer") .accessibilityLabel(mnemonicAccessibilityLabel) diff --git a/Bitkit/Views/Onboarding/RestoreWalletView.swift b/Bitkit/Views/Onboarding/RestoreWalletView.swift index 7123f062..86051f63 100644 --- a/Bitkit/Views/Onboarding/RestoreWalletView.swift +++ b/Bitkit/Views/Onboarding/RestoreWalletView.swift @@ -105,8 +105,13 @@ struct RestoreWalletView: View { ScrollView(showsIndicators: false) { VStack(spacing: 0) { headerSection - wordInputSection - passphraseSection + + Group { + wordInputSection + passphraseSection + } + .screenshotPreventMask(true) + validationSection Spacer(minLength: 16) buttonSection diff --git a/Bitkit/Views/Recovery/RecoveryMnemonicScreen.swift b/Bitkit/Views/Recovery/RecoveryMnemonicScreen.swift index c9ba8a27..6325fd6e 100644 --- a/Bitkit/Views/Recovery/RecoveryMnemonicScreen.swift +++ b/Bitkit/Views/Recovery/RecoveryMnemonicScreen.swift @@ -54,7 +54,7 @@ struct RecoveryMnemonicScreen: View { .padding(32) .background(Color.white10) .cornerRadius(16) - .privacySensitive() + .screenshotPreventMask(true) // Passphrase section (if available) if !passphrase.isEmpty {