diff --git a/README.md b/README.md index 344f1c9..f90ff03 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ let config = { () -> LDConfig in isEnabled: true, privacy: .init( maskTextInputs: true, + maskWebViews: false, maskImages: false, maskAccessibilityIdentifiers: ["email-field", "password-field"] ) @@ -159,6 +160,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate { Configure privacy settings to control what data is captured: - **maskTextInputs**: Mask all text input fields (default: `true`) +- **maskWebViews**: Mask contents of Web Views (default: `false`) - **maskLabels**: Mask all text labels (default: `false`) - **maskImages**: Mask all images (default: `false`) - **maskAccessibilityIdentifiers**: Array of accessibility identifiers to mask diff --git a/Sources/SessionReplay/API/SessionReplayOptions.swift b/Sources/SessionReplay/API/SessionReplayOptions.swift index a048ab4..3e5a70d 100644 --- a/Sources/SessionReplay/API/SessionReplayOptions.swift +++ b/Sources/SessionReplay/API/SessionReplayOptions.swift @@ -5,6 +5,7 @@ public struct SessionReplayOptions { public struct PrivacyOptions { public var maskTextInputs: Bool + public var maskWebViews: Bool public var maskLabels: Bool public var maskImages: Bool @@ -17,6 +18,7 @@ public struct SessionReplayOptions { public var minimumAlpha: CGFloat public init(maskTextInputs: Bool = true, + maskWebViews: Bool = false, maskLabels: Bool = false, maskImages: Bool = false, maskUIViews: [AnyClass] = [], @@ -25,6 +27,7 @@ public struct SessionReplayOptions { ignoreAccessibilityIdentifiers: [String] = [], minimumAlpha: CGFloat = 0.02) { self.maskTextInputs = maskTextInputs + self.maskWebViews = maskWebViews self.maskLabels = maskLabels self.maskImages = maskImages self.maskUIViews = maskUIViews diff --git a/Sources/SessionReplay/ScreenCapture/MaskCollector.swift b/Sources/SessionReplay/ScreenCapture/MaskCollector.swift index eb1196a..fc1fdc3 100644 --- a/Sources/SessionReplay/ScreenCapture/MaskCollector.swift +++ b/Sources/SessionReplay/ScreenCapture/MaskCollector.swift @@ -1,4 +1,5 @@ import Foundation +import WebKit import UIKit import SwiftUI import Common @@ -14,6 +15,7 @@ final class MaskCollector { struct Settings { var maskiOS26ViewTypes: Set var maskTextInputs: Bool + var maskWebViews: Bool var maskImages: Bool var minimumAlpha: CGFloat var maskClasses: Set @@ -23,6 +25,7 @@ final class MaskCollector { init(privacySettings: PrivacySettings) { self.maskiOS26ViewTypes = Constants.maskiOS26ViewTypes self.maskTextInputs = privacySettings.maskTextInputs + self.maskWebViews = privacySettings.maskWebViews self.maskImages = privacySettings.maskImages self.minimumAlpha = privacySettings.minimumAlpha self.maskClasses = privacySettings.buildMaskClasses() @@ -31,16 +34,42 @@ final class MaskCollector { } func shouldMask(_ view: UIView) -> Bool { - if maskiOS26ViewTypes.contains(String(describing: type(of: view))) { + if let shouldUnmask = SessionReplayAssociatedObjects.shouldMaskUIView(view), + !shouldUnmask { + return false + } + + if let accessibilityIdentifier = view.accessibilityIdentifier, + ignoreAccessibilityIdentifiers.contains(accessibilityIdentifier) { + return false + } + + let viewType = type(of: view) + let stringViewType = String(describing: viewType) + + if maskiOS26ViewTypes.contains(stringViewType) { return true } - if maskTextInputs, let _ = view as? UITextInput { - return SessionReplayAssociatedObjects.shouldMaskUIView(view) ?? true + if maskWebViews { + if let wkWebView = view as? WKWebView { + return true + } + if let uiWebView = view as? UIWebView { + return true + } + } + + if maskTextInputs { + if let textInput = view as? UITextInput { + if stringViewType != "WKContentView" { + return true + } + } } if maskImages, let imageView = view as? UIImageView { - return SessionReplayAssociatedObjects.shouldMaskUIView(imageView) ?? true + return true } if SessionReplayAssociatedObjects.shouldMaskSwiftUI(view) ?? false { @@ -51,6 +80,11 @@ final class MaskCollector { return true } + if let accessibilityIdentifier = view.accessibilityIdentifier, + maskAccessibilityIdentifiers.contains(accessibilityIdentifier) { + return true + } + return false } } diff --git a/TestApp/Sources/AppDelegate.swift b/TestApp/Sources/AppDelegate.swift index 9bacea4..3a04f51 100644 --- a/TestApp/Sources/AppDelegate.swift +++ b/TestApp/Sources/AppDelegate.swift @@ -36,8 +36,9 @@ let config = { () -> LDConfig in isEnabled: true, privacy: .init( maskTextInputs: true, + maskWebViews: false, maskImages: false, - maskAccessibilityIdentifiers: ["email-field", "password-field"], + maskAccessibilityIdentifiers: ["email-field", "password-field", "card-brand-chip"], ) )) ] diff --git a/TestApp/Sources/ContentView.swift b/TestApp/Sources/ContentView.swift index cae0710..7d95023 100644 --- a/TestApp/Sources/ContentView.swift +++ b/TestApp/Sources/ContentView.swift @@ -21,6 +21,8 @@ struct ContentView: View { @State private var isMaskingUIKitCreditCardEnabled: Bool = false @State private var isNumberPadEnabled: Bool = false @State private var isNotebookEnabled: Bool = false + @State private var isStoryboardEnabled: Bool = false + @State private var isWebviewEnabled: Bool = false @State private var buttonPressed: Bool = false @State private var errorPressed: Bool = false @@ -53,27 +55,35 @@ struct ContentView: View { #endif FauxLinkToggleRow(title: "Notebook (SwiftUI)", isOn: $isNotebookEnabled) + FauxLinkToggleRow(title: "Storyboad (UIKit)", isOn: $isStoryboardEnabled) + FauxLinkToggleRow(title: "WebView (WebKit)", isOn: $isWebviewEnabled) - Button { - buttonPressed.toggle() - } label: { - Text("span") - } - .buttonStyle(.borderedProminent) - - Button { - logsPressed.toggle() - } label: { - Text("logs") + NavigationLink(destination: SystemUnderPressureView()) { + Text("Simulate System Under Pressure") } - .buttonStyle(.borderedProminent) - Button { - counterMetricPressed.toggle() - } label: { - Text("metric: counter") + HStack { + Button { + buttonPressed.toggle() + } label: { + Text("span") + } + .buttonStyle(.borderedProminent) + + Button { + logsPressed.toggle() + } label: { + Text("logs") + } + .buttonStyle(.borderedProminent) + + Button { + counterMetricPressed.toggle() + } label: { + Text("metric: counter") + } + .buttonStyle(.borderedProminent) } - .buttonStyle(.borderedProminent) Button { networkPressed.toggle() @@ -88,27 +98,27 @@ struct ContentView: View { } .buttonStyle(.borderedProminent) .disabled(networkPressed) - - Button { - errorPressed.toggle() - } label: { - Text("error") + + HStack { + Button { + errorPressed.toggle() + } label: { + Text("error") + } + .buttonStyle(.borderedProminent) + .tint(.red) + + Button { + crashPressed.toggle() + } label: { + Text("Crash") + } + .buttonStyle(.borderedProminent) + .tint(.red) } - .buttonStyle(.borderedProminent) - .tint(.red) - Button { - crashPressed.toggle() - } label: { - Text("Crash") - } - .buttonStyle(.borderedProminent) - .tint(.red) - NavigationLink(destination: SystemUnderPressureView()) { - Text("Simulate System Under Pressure") - } - + }.background(Color.clear) } .task(id: errorPressed) { @@ -180,6 +190,10 @@ struct ContentView: View { MaskingElementsSimpleUIKitView() }.sheet(isPresented: $isNumberPadEnabled) { NumberPadView() + }.sheet(isPresented: $isStoryboardEnabled) { + StoryboardRootView() + }.sheet(isPresented: $isWebviewEnabled) { + WebViewControllertView() } } } diff --git a/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift b/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift index d532bb9..d92a1ff 100644 --- a/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift +++ b/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift @@ -115,7 +115,8 @@ public final class CreditCardViewController: UIViewController { setupLayout() updateSaveButton() - //nameField.ldUnmask() + nameField.ldUnmask() + brandChip.accessibilityIdentifier = "card-brand-chip" } public override func viewDidAppear(_ animated: Bool) { diff --git a/TestApp/Sources/Storyboard/StoryboardRootView.swift b/TestApp/Sources/Storyboard/StoryboardRootView.swift new file mode 100644 index 0000000..a944e62 --- /dev/null +++ b/TestApp/Sources/Storyboard/StoryboardRootView.swift @@ -0,0 +1,11 @@ +import SwiftUI +import UIKit + +struct StoryboardRootView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + let sb = UIStoryboard(name: "StoryboardiOS", bundle: .main) + // Use the Initial VC, or use instantiateViewController(withIdentifier:) if you set an ID + return sb.instantiateInitialViewController()! + } + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/TestApp/Sources/Storyboard/StoryboardiOS.storyboard b/TestApp/Sources/Storyboard/StoryboardiOS.storyboard new file mode 100644 index 0000000..25e7210 --- /dev/null +++ b/TestApp/Sources/Storyboard/StoryboardiOS.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift new file mode 100644 index 0000000..d60dc1b --- /dev/null +++ b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift @@ -0,0 +1,8 @@ +import UIKit + +class StoryboardiOSViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + } +} diff --git a/TestApp/Sources/WebViews/WebViewController.swift b/TestApp/Sources/WebViews/WebViewController.swift new file mode 100644 index 0000000..96e680b --- /dev/null +++ b/TestApp/Sources/WebViews/WebViewController.swift @@ -0,0 +1,34 @@ +import UIKit +import WebKit +import SwiftUI + +class WebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate { + + private var webView: WKWebView! + + override func loadView() { + + // Configure the web view + let webConfiguration = WKWebViewConfiguration() + webView = WKWebView(frame: .zero, configuration: webConfiguration) + webView.uiDelegate = self + webView.navigationDelegate = self + view = webView + } + + override func viewDidLoad() { + super.viewDidLoad() + + // The notice should automatically get hidden in the web view as consent is passed from the mobile app to the website. However, it might happen that the notice gets displayed for a very short time before being hidden. You can disable the notice in your web view to make sure that it never shows by appending didomiConfig.notice.enable=false to the query string of the URL that you are loading + let myURL = URL(string:"https://launchdarkly.com/")! + let myRequest = URLRequest(url: myURL) + webView.load(myRequest) + } +} + +struct WebViewControllertView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + WebViewController() + } + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +}