Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ let config = { () -> LDConfig in
isEnabled: true,
privacy: .init(
maskTextInputs: true,
maskWebViews: false,
maskImages: false,
maskAccessibilityIdentifiers: ["email-field", "password-field"]
)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Sources/SessionReplay/API/SessionReplayOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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] = [],
Expand All @@ -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
Expand Down
42 changes: 38 additions & 4 deletions Sources/SessionReplay/ScreenCapture/MaskCollector.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import WebKit
import UIKit
import SwiftUI
import Common
Expand All @@ -14,6 +15,7 @@ final class MaskCollector {
struct Settings {
var maskiOS26ViewTypes: Set<String>
var maskTextInputs: Bool
var maskWebViews: Bool
var maskImages: Bool
var minimumAlpha: CGFloat
var maskClasses: Set<ObjectIdentifier>
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -51,6 +80,11 @@ final class MaskCollector {
return true
}

if let accessibilityIdentifier = view.accessibilityIdentifier,
maskAccessibilityIdentifiers.contains(accessibilityIdentifier) {
return true
}

return false
}
}
Expand Down
3 changes: 2 additions & 1 deletion TestApp/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
))
]
Expand Down
50 changes: 30 additions & 20 deletions TestApp/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,27 +55,31 @@ struct ContentView: View {
#endif

FauxLinkToggleRow(title: "Notebook (SwiftUI)", isOn: $isNotebookEnabled)

Button {
buttonPressed.toggle()
} label: {
Text("span")
}
.buttonStyle(.borderedProminent)

Button {
logsPressed.toggle()
} label: {
Text("logs")
}
.buttonStyle(.borderedProminent)

Button {
counterMetricPressed.toggle()
} label: {
Text("metric: counter")
FauxLinkToggleRow(title: "Storyboad (UIKit)", isOn: $isStoryboardEnabled)
FauxLinkToggleRow(title: "WebView (WebKit)", isOn: $isWebviewEnabled)

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()
Expand Down Expand Up @@ -180,6 +186,10 @@ struct ContentView: View {
MaskingElementsSimpleUIKitView()
}.sheet(isPresented: $isNumberPadEnabled) {
NumberPadView()
}.sheet(isPresented: $isStoryboardEnabled) {
StoryboardRootView()
}.sheet(isPresented: $isWebviewEnabled) {
WebViewControllertView()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions TestApp/Sources/Storyboard/StoryboardRootView.swift
Original file line number Diff line number Diff line change
@@ -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) {}
}
48 changes: 48 additions & 0 deletions TestApp/Sources/Storyboard/StoryboardiOS.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24128" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--StoryboardiOS-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController title="StoryboardiOS" id="Y6W-OH-hqX" customClass="StoryboardiOSViewController" customModule="TestApp" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="tM9-Bg-Ve2">
<rect key="frame" x="148" y="118" width="97" height="34"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h3i-fX-rrY">
<rect key="frame" x="78" y="125" width="45" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="139" y="131"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
30 changes: 30 additions & 0 deletions TestApp/Sources/Storyboard/StoryboardiOSViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import UIKit

class StoryboardiOSViewController: UIViewController {
// MARK: - Outlets
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var nameTextField: UITextField!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Storyboard Outlets Not Connected

The nameLabel and nameTextField IBOutlets are declared as force-unwrapped optionals but aren't connected in the storyboard. They will be nil at runtime, leading to a crash if accessed.

Fix in Cursor Fix in Web

// @IBOutlet weak var passwordLabel: UILabel!
// @IBOutlet weak var passwordTextField: UITextField!
// @IBOutlet weak var loginButton: UIButton!

// MARK: - Actions
@IBAction func loginButtonTapped(_ sender: UIButton) {
// Example login validation logic (add your own as needed)
// let name = nameTextField.text ?? ""
// let password = passwordTextField.text ?? ""
// if name.isEmpty || password.isEmpty {
// // Present an alert or show an error
// let alert = UIAlertController(title: "Missing Info", message: "Please enter both name and password.", preferredStyle: .alert)
// alert.addAction(UIAlertAction(title: "OK", style: .default))
// present(alert, animated: true)
// return
// }
// Perform login logic here
}

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
}
34 changes: 34 additions & 0 deletions TestApp/Sources/WebViews/WebViewController.swift
Original file line number Diff line number Diff line change
@@ -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) {}
}