@multiplayer-app/session-recorder-react-native 0.0.1-beta.9 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +708 -83
- package/SessionRecorderNative.podspec +26 -0
- package/android/build.gradle +2 -0
- package/copy-react-native-dist.sh +33 -9
- package/dist/components/ScreenRecorderView/ScreenRecorderView.js +1 -1
- package/dist/components/ScreenRecorderView/ScreenRecorderView.js.map +1 -1
- package/dist/components/SessionRecorderWidget/ErrorBanner.d.ts +7 -0
- package/dist/components/SessionRecorderWidget/ErrorBanner.js +1 -0
- package/dist/components/SessionRecorderWidget/ErrorBanner.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.d.ts +4 -3
- package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -1
- package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -1
- package/dist/components/SessionRecorderWidget/InitialPopover.d.ts +6 -3
- package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -1
- package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -1
- package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -1
- package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -1
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js +1 -1
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js.map +1 -1
- package/dist/components/SessionRecorderWidget/styles.d.ts +27 -7
- package/dist/components/SessionRecorderWidget/styles.js +1 -1
- package/dist/components/SessionRecorderWidget/styles.js.map +1 -1
- package/dist/components/index.d.ts +0 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/config/constants.js +1 -1
- package/dist/config/constants.js.map +1 -1
- package/dist/config/defaults.d.ts +4 -4
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/masking.d.ts +2 -2
- package/dist/config/masking.js +1 -1
- package/dist/config/masking.js.map +1 -1
- package/dist/config/session-recorder.js +1 -1
- package/dist/config/session-recorder.js.map +1 -1
- package/dist/config/validators.d.ts +1 -1
- package/dist/config/validators.js +1 -1
- package/dist/config/validators.js.map +1 -1
- package/dist/config/widget.d.ts +9 -0
- package/dist/config/widget.js +1 -0
- package/dist/config/widget.js.map +1 -0
- package/dist/context/SessionRecorderContext.d.ts +11 -4
- package/dist/context/SessionRecorderContext.js +1 -1
- package/dist/context/SessionRecorderContext.js.map +1 -1
- package/dist/context/SessionRecorderStore.d.ts +12 -0
- package/dist/context/SessionRecorderStore.js +1 -0
- package/dist/context/SessionRecorderStore.js.map +1 -0
- package/dist/context/useSessionRecorderStore.d.ts +8 -0
- package/dist/context/useSessionRecorderStore.js +1 -0
- package/dist/context/useSessionRecorderStore.js.map +1 -0
- package/dist/context/useStoreSelector.d.ts +4 -0
- package/dist/context/useStoreSelector.js +1 -0
- package/dist/context/useStoreSelector.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/native/GestureRecorderNative.d.ts +57 -0
- package/dist/native/GestureRecorderNative.js +1 -0
- package/dist/native/GestureRecorderNative.js.map +1 -0
- package/dist/native/SessionRecorderNative.d.ts +17 -5
- package/dist/native/SessionRecorderNative.js +1 -1
- package/dist/native/SessionRecorderNative.js.map +1 -1
- package/dist/native/index.d.ts +2 -0
- package/dist/native/index.js +1 -0
- package/dist/native/index.js.map +1 -0
- package/dist/otel/index.js +1 -1
- package/dist/otel/index.js.map +1 -1
- package/dist/patch/xhr.js +1 -1
- package/dist/patch/xhr.js.map +1 -1
- package/dist/recorder/eventExporter.d.ts +4 -1
- package/dist/recorder/eventExporter.js +1 -1
- package/dist/recorder/eventExporter.js.map +1 -1
- package/dist/recorder/gestureRecorder.d.ts +28 -62
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +2 -0
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.js.map +1 -1
- package/dist/recorder/navigationTracker.d.ts +4 -19
- package/dist/recorder/navigationTracker.js +1 -1
- package/dist/recorder/navigationTracker.js.map +1 -1
- package/dist/recorder/screenRecorder.d.ts +10 -5
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/services/api.service.d.ts +12 -3
- package/dist/services/api.service.js +1 -1
- package/dist/services/api.service.js.map +1 -1
- package/dist/services/network.service.d.ts +46 -0
- package/dist/services/network.service.js +1 -0
- package/dist/services/network.service.js.map +1 -0
- package/dist/services/screenMaskingService.d.ts +13 -5
- package/dist/services/screenMaskingService.js +1 -1
- package/dist/services/screenMaskingService.js.map +1 -1
- package/dist/services/storage.service.js +1 -1
- package/dist/services/storage.service.js.map +1 -1
- package/dist/session-recorder.d.ts +17 -33
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/configs.d.ts +85 -0
- package/dist/types/configs.js +1 -0
- package/dist/types/configs.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/session-recorder.d.ts +105 -138
- package/dist/types/session-recorder.js +1 -1
- package/dist/types/session-recorder.js.map +1 -1
- package/dist/utils/constants.optional.d.ts +21 -0
- package/dist/utils/constants.optional.expo.d.ts +3 -0
- package/dist/utils/constants.optional.expo.js +1 -0
- package/dist/utils/constants.optional.expo.js.map +1 -0
- package/dist/utils/constants.optional.js +1 -0
- package/dist/utils/constants.optional.js.map +1 -0
- package/dist/utils/createStore.d.ts +8 -0
- package/dist/utils/createStore.js +1 -0
- package/dist/utils/createStore.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -7
- package/dist/utils/logger.js +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/platform.d.ts +8 -0
- package/dist/utils/platform.js +1 -1
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/rrweb-events.d.ts +4 -3
- package/dist/utils/rrweb-events.js +1 -1
- package/dist/utils/rrweb-events.js.map +1 -1
- package/dist/utils/session.d.ts +2 -1
- package/dist/utils/session.js +1 -1
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/shallowEqual.d.ts +1 -0
- package/dist/utils/shallowEqual.js +1 -0
- package/dist/utils/shallowEqual.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/AUTO_METADATA_DETECTION.md +108 -0
- package/ios/GestureRecorderNative.m +21 -0
- package/ios/GestureRecorderNative.swift +316 -0
- package/ios/SessionRecorderNative.m +5 -0
- package/ios/SessionRecorderNative.swift +478 -84
- package/package.json +13 -27
- package/react-native.config.js +7 -10
- package/app.plugin.js +0 -42
- package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.d.ts +0 -6
- package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js +0 -1
- package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js.map +0 -1
- package/dist/components/GestureCaptureWrapper/index.d.ts +0 -1
- package/dist/components/GestureCaptureWrapper/index.js +0 -1
- package/dist/components/GestureCaptureWrapper/index.js.map +0 -1
- package/dist/components/MaskableComponent.d.ts +0 -22
- package/dist/components/MaskableComponent.js +0 -1
- package/dist/components/MaskableComponent.js.map +0 -1
- package/dist/components/MaskableTextInput.d.ts +0 -14
- package/dist/components/MaskableTextInput.js +0 -1
- package/dist/components/MaskableTextInput.js.map +0 -1
- package/dist/native/ScreenMasking.d.ts +0 -21
- package/dist/native/ScreenMasking.js +0 -1
- package/dist/native/ScreenMasking.js.map +0 -1
- package/dist/recorder/screenshotManager.d.ts +0 -10
- package/dist/recorder/screenshotManager.js +0 -1
- package/dist/recorder/screenshotManager.js.map +0 -1
- package/dist/utils/componentRegistry.d.ts +0 -64
- package/dist/utils/componentRegistry.js +0 -1
- package/dist/utils/componentRegistry.js.map +0 -1
- package/dist/utils/nativeModuleTest.d.ts +0 -8
- package/dist/utils/nativeModuleTest.js +0 -1
- package/dist/utils/nativeModuleTest.js.map +0 -1
- package/dist/utils/reactNativeHierarchyExtractor.d.ts +0 -38
- package/dist/utils/reactNativeHierarchyExtractor.js +0 -1
- package/dist/utils/reactNativeHierarchyExtractor.js.map +0 -1
- package/dist/utils/screenshotMasker.d.ts +0 -96
- package/dist/utils/screenshotMasker.js +0 -1
- package/dist/utils/screenshotMasker.js.map +0 -1
- package/dist/utils/viewHierarchyTracker.d.ts +0 -89
- package/dist/utils/viewHierarchyTracker.js +0 -1
- package/dist/utils/viewHierarchyTracker.js.map +0 -1
- package/docs/NATIVE_MODULE_SETUP.md +0 -177
- package/plugin/package.json +0 -20
- package/plugin/src/index.js +0 -42
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
import React
|
|
3
|
+
import WebKit
|
|
3
4
|
|
|
4
5
|
@objc(SessionRecorderNative)
|
|
5
6
|
class SessionRecorderNative: NSObject {
|
|
6
7
|
|
|
8
|
+
// Configuration options
|
|
9
|
+
private var maskTextInputs: Bool = true
|
|
10
|
+
private var maskImages: Bool = false
|
|
11
|
+
private var maskButtons: Bool = false
|
|
12
|
+
private var maskLabels: Bool = false
|
|
13
|
+
private var maskWebViews: Bool = false
|
|
14
|
+
private var maskSandboxedViews: Bool = false
|
|
15
|
+
private var imageQuality: CGFloat = 0.05
|
|
16
|
+
private var scale: CGFloat = 1.0
|
|
17
|
+
|
|
18
|
+
// React Native view types
|
|
19
|
+
private let reactNativeTextView: AnyClass? = NSClassFromString("RCTTextView")
|
|
20
|
+
private let reactNativeImageView: AnyClass? = NSClassFromString("RCTImageView")
|
|
21
|
+
private let reactNativeTextInput: AnyClass? = NSClassFromString("RCTUITextField")
|
|
22
|
+
private let reactNativeTextInputView: AnyClass? = NSClassFromString("RCTUITextView")
|
|
23
|
+
|
|
24
|
+
// System sandboxed views (usually sensitive)
|
|
25
|
+
private let systemSandboxedView: AnyClass? = NSClassFromString("_UIRemoteView")
|
|
26
|
+
|
|
27
|
+
// SwiftUI view types
|
|
28
|
+
private let swiftUITextBasedViewTypes = [
|
|
29
|
+
"SwiftUI.CGDrawingView", // Text, Button
|
|
30
|
+
"SwiftUI.TextEditorTextView", // TextEditor
|
|
31
|
+
"SwiftUI.VerticalTextView", // TextField, vertical axis
|
|
32
|
+
].compactMap(NSClassFromString)
|
|
33
|
+
|
|
34
|
+
private let swiftUIImageLayerTypes = [
|
|
35
|
+
"SwiftUI.ImageLayer",
|
|
36
|
+
].compactMap(NSClassFromString)
|
|
37
|
+
|
|
38
|
+
private let swiftUIGenericTypes = [
|
|
39
|
+
"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView",
|
|
40
|
+
].compactMap(NSClassFromString)
|
|
41
|
+
|
|
42
|
+
// Safe layer types that shouldn't be masked
|
|
43
|
+
private let swiftUISafeLayerTypes: [AnyClass] = [
|
|
44
|
+
"SwiftUI.GradientLayer", // Views like LinearGradient, RadialGradient, or AngularGradient
|
|
45
|
+
].compactMap(NSClassFromString)
|
|
46
|
+
|
|
7
47
|
@objc func captureAndMask(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
8
48
|
DispatchQueue.main.async {
|
|
9
49
|
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
|
@@ -24,9 +64,15 @@ class SessionRecorderNative: NSObject {
|
|
|
24
64
|
// Apply masking to sensitive elements
|
|
25
65
|
let maskedImage = self.applyMasking(to: image, in: window)
|
|
26
66
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
// Apply optional scaling (resolution downsample)
|
|
68
|
+
let finalImage = self.scale < 1.0 ? self.resizeImage(maskedImage, scale: self.scale) : maskedImage
|
|
69
|
+
|
|
70
|
+
// Debug logging
|
|
71
|
+
print("SessionRecorder captureAndMask: Scale = \(self.scale), Original size = \(maskedImage.size), Final size = \(finalImage.size)")
|
|
72
|
+
|
|
73
|
+
if let data = finalImage.jpegData(compressionQuality: self.imageQuality) {
|
|
74
|
+
let base64 = data.base64EncodedString()
|
|
75
|
+
resolve(base64)
|
|
30
76
|
} else {
|
|
31
77
|
reject("ENCODING_FAILED", "Failed to encode image", nil)
|
|
32
78
|
}
|
|
@@ -35,6 +81,9 @@ class SessionRecorderNative: NSObject {
|
|
|
35
81
|
|
|
36
82
|
@objc func captureAndMaskWithOptions(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
37
83
|
DispatchQueue.main.async {
|
|
84
|
+
// Update configuration from options
|
|
85
|
+
self.updateConfiguration(from: options)
|
|
86
|
+
|
|
38
87
|
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
|
39
88
|
reject("NO_WINDOW", "Unable to get key window", nil)
|
|
40
89
|
return
|
|
@@ -53,7 +102,13 @@ class SessionRecorderNative: NSObject {
|
|
|
53
102
|
// Apply masking with custom options
|
|
54
103
|
let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
|
|
55
104
|
|
|
56
|
-
|
|
105
|
+
// Apply optional scaling (resolution downsample)
|
|
106
|
+
let finalImage = self.scale < 1.0 ? self.resizeImage(maskedImage, scale: self.scale) : maskedImage
|
|
107
|
+
|
|
108
|
+
// Debug logging
|
|
109
|
+
print("SessionRecorder captureAndMaskWithOptions: Scale = \(self.scale), Original size = \(maskedImage.size), Final size = \(finalImage.size)")
|
|
110
|
+
|
|
111
|
+
if let data = finalImage.jpegData(compressionQuality: self.imageQuality) {
|
|
57
112
|
let base64 = data.base64EncodedString()
|
|
58
113
|
resolve(base64)
|
|
59
114
|
} else {
|
|
@@ -62,6 +117,42 @@ class SessionRecorderNative: NSObject {
|
|
|
62
117
|
}
|
|
63
118
|
}
|
|
64
119
|
|
|
120
|
+
private func updateConfiguration(from options: NSDictionary) {
|
|
121
|
+
print("SessionRecorder: updateConfiguration called with options: \(options)")
|
|
122
|
+
|
|
123
|
+
if let maskTextInputs = options["maskTextInputs"] as? Bool {
|
|
124
|
+
self.maskTextInputs = maskTextInputs
|
|
125
|
+
}
|
|
126
|
+
if let maskImages = options["maskImages"] as? Bool {
|
|
127
|
+
self.maskImages = maskImages
|
|
128
|
+
}
|
|
129
|
+
if let maskSandboxedViews = options["maskSandboxedViews"] as? Bool {
|
|
130
|
+
self.maskSandboxedViews = maskSandboxedViews
|
|
131
|
+
}
|
|
132
|
+
if let maskButtons = options["maskButtons"] as? Bool {
|
|
133
|
+
self.maskButtons = maskButtons
|
|
134
|
+
}
|
|
135
|
+
if let maskLabels = options["maskLabels"] as? Bool {
|
|
136
|
+
self.maskLabels = maskLabels
|
|
137
|
+
}
|
|
138
|
+
if let maskWebViews = options["maskWebViews"] as? Bool {
|
|
139
|
+
self.maskWebViews = maskWebViews
|
|
140
|
+
}
|
|
141
|
+
if let quality = options["quality"] as? NSNumber {
|
|
142
|
+
self.imageQuality = CGFloat(quality.floatValue)
|
|
143
|
+
}
|
|
144
|
+
if let scale = options["scale"] as? NSNumber {
|
|
145
|
+
self.scale = CGFloat(scale.floatValue)
|
|
146
|
+
print("SessionRecorder: Scale updated to \(self.scale) (from NSNumber: \(scale))")
|
|
147
|
+
} else if let scale = options["scale"] as? Double {
|
|
148
|
+
self.scale = CGFloat(scale)
|
|
149
|
+
print("SessionRecorder: Scale updated to \(self.scale) (from Double: \(scale))")
|
|
150
|
+
} else if let scale = options["scale"] as? Float {
|
|
151
|
+
self.scale = CGFloat(scale)
|
|
152
|
+
print("SessionRecorder: Scale updated to \(self.scale) (from Float: \(scale))")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
65
156
|
private func applyMasking(to image: UIImage, in window: UIWindow) -> UIImage {
|
|
66
157
|
return applyMaskingWithOptions(to: image, in: window, options: [:])
|
|
67
158
|
}
|
|
@@ -73,23 +164,26 @@ class SessionRecorderNative: NSObject {
|
|
|
73
164
|
// Draw the original image
|
|
74
165
|
image.draw(in: CGRect(origin: .zero, size: image.size))
|
|
75
166
|
|
|
76
|
-
// Find and mask sensitive elements
|
|
77
|
-
let sensitiveElements = findSensitiveElements(in: window)
|
|
78
167
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
168
|
+
var maskableWidgets: [CGRect] = []
|
|
169
|
+
var maskChildren = false
|
|
170
|
+
findMaskableWidgets(window, window, &maskableWidgets, &maskChildren)
|
|
82
171
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
break
|
|
172
|
+
for frame in maskableWidgets {
|
|
173
|
+
// Skip zero rects (which indicate invalid coordinates)
|
|
174
|
+
if frame == CGRect.zero { continue }
|
|
175
|
+
|
|
176
|
+
// Validate frame dimensions before processing
|
|
177
|
+
guard frame.size.width.isFinite && frame.size.height.isFinite &&
|
|
178
|
+
frame.origin.x.isFinite && frame.origin.y.isFinite else {
|
|
179
|
+
continue
|
|
92
180
|
}
|
|
181
|
+
|
|
182
|
+
// Clip the frame to the image bounds to avoid drawing outside context
|
|
183
|
+
let clippedFrame = frame.intersection(CGRect(origin: .zero, size: image.size))
|
|
184
|
+
if clippedFrame.isNull || clippedFrame.isEmpty { continue }
|
|
185
|
+
|
|
186
|
+
applyCleanMask(in: context, frame: clippedFrame)
|
|
93
187
|
}
|
|
94
188
|
|
|
95
189
|
let maskedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
|
|
@@ -98,102 +192,322 @@ class SessionRecorderNative: NSObject {
|
|
|
98
192
|
return maskedImage
|
|
99
193
|
}
|
|
100
194
|
|
|
101
|
-
private func
|
|
102
|
-
|
|
195
|
+
private func findMaskableWidgets(_ view: UIView, _ window: UIWindow, _ maskableWidgets: inout [CGRect], _ maskChildren: inout Bool) {
|
|
196
|
+
// Skip hidden or transparent views
|
|
197
|
+
if !view.isVisible() {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for UITextView (TextEditor, SwiftUI.TextEditorTextView, SwiftUI.UIKitTextView)
|
|
202
|
+
if let textView = view as? UITextView {
|
|
203
|
+
if isTextViewSensitive(textView) {
|
|
204
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for UITextField (SwiftUI: TextField, SecureField)
|
|
210
|
+
if let textField = view as? UITextField {
|
|
211
|
+
if isTextFieldSensitive(textField) {
|
|
212
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// React Native text views
|
|
218
|
+
if let reactNativeTextView = reactNativeTextView {
|
|
219
|
+
if view.isKind(of: reactNativeTextView), maskTextInputs {
|
|
220
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// React Native text inputs
|
|
226
|
+
if let reactNativeTextInput = reactNativeTextInput {
|
|
227
|
+
if view.isKind(of: reactNativeTextInput), maskTextInputs {
|
|
228
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if let reactNativeTextInputView = reactNativeTextInputView {
|
|
234
|
+
if view.isKind(of: reactNativeTextInputView), maskTextInputs {
|
|
235
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// UIImageView (SwiftUI: Some control images like the ones in Picker view)
|
|
241
|
+
if let imageView = view as? UIImageView {
|
|
242
|
+
if isImageViewSensitive(imageView) {
|
|
243
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// React Native image views
|
|
249
|
+
if let reactNativeImageView = reactNativeImageView {
|
|
250
|
+
if view.isKind(of: reactNativeImageView), maskImages {
|
|
251
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// UILabel (Text, this code might never be reachable in SwiftUI)
|
|
257
|
+
if let label = view as? UILabel {
|
|
258
|
+
if isLabelSensitive(label) {
|
|
259
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// WKWebView (Link, this code might never be reachable in SwiftUI)
|
|
265
|
+
if let webView = view as? WKWebView {
|
|
266
|
+
if isAnyInputSensitive(webView) {
|
|
267
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// UIButton (SwiftUI: SwiftUI.UIKitIconPreferringButton and other subclasses)
|
|
273
|
+
if let button = view as? UIButton {
|
|
274
|
+
if isButtonSensitive(button) {
|
|
275
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// UISwitch (SwiftUI: Toggle)
|
|
281
|
+
if let theSwitch = view as? UISwitch {
|
|
282
|
+
if isSwitchSensitive(theSwitch) {
|
|
283
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
}
|
|
103
287
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
|
|
288
|
+
// UIPickerView (SwiftUI: Picker with .pickerStyle(.wheel))
|
|
289
|
+
if let picker = view as? UIPickerView {
|
|
290
|
+
if isTextInputSensitive(picker), !view.subviews.isEmpty {
|
|
291
|
+
maskableWidgets.append(picker.toAbsoluteRect(window))
|
|
292
|
+
return
|
|
108
293
|
}
|
|
294
|
+
}
|
|
109
295
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
296
|
+
// Detect any views that don't belong to the current process (likely system views)
|
|
297
|
+
if maskSandboxedViews,
|
|
298
|
+
let systemSandboxedView,
|
|
299
|
+
view.isKind(of: systemSandboxedView) {
|
|
300
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let hasSubViews = !view.subviews.isEmpty
|
|
305
|
+
|
|
306
|
+
// SwiftUI: Text based views like Text, Button, TextEditor
|
|
307
|
+
if swiftUITextBasedViewTypes.contains(where: view.isKind(of:)) {
|
|
308
|
+
if isTextInputSensitive(view), !hasSubViews {
|
|
309
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
310
|
+
return
|
|
113
311
|
}
|
|
114
312
|
}
|
|
115
313
|
|
|
116
|
-
|
|
117
|
-
|
|
314
|
+
// SwiftUI: Image based views like Image, AsyncImage
|
|
315
|
+
if swiftUIImageLayerTypes.contains(where: view.layer.isKind(of:)) {
|
|
316
|
+
if isSwiftUIImageSensitive(view), !hasSubViews {
|
|
317
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Generic SwiftUI types
|
|
323
|
+
if swiftUIGenericTypes.contains(where: { view.isKind(of: $0) }), !isSwiftUILayerSafe(view.layer) {
|
|
324
|
+
if isTextInputSensitive(view), !hasSubViews {
|
|
325
|
+
maskableWidgets.append(view.toAbsoluteRect(window))
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Recursively check subviews
|
|
331
|
+
if !view.subviews.isEmpty {
|
|
332
|
+
for child in view.subviews {
|
|
333
|
+
if !child.isVisible() {
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
findMaskableWidgets(child, window, &maskableWidgets, &maskChildren)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
maskChildren = false
|
|
118
340
|
}
|
|
119
341
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
342
|
+
// MARK: - Sensitive Content Detection Methods
|
|
343
|
+
|
|
344
|
+
private func isAnyInputSensitive(_ view: UIView) -> Bool {
|
|
345
|
+
return isTextInputSensitive(view) || maskImages
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private func isTextInputSensitive(_ view: UIView) -> Bool {
|
|
349
|
+
return maskTextInputs || view.isNoCapture()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private func isLabelSensitive(_ view: UILabel) -> Bool {
|
|
353
|
+
return isTextInputSensitive(view) && hasText(view.text)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private func isButtonSensitive(_ view: UIButton) -> Bool {
|
|
357
|
+
return isTextInputSensitive(view) && hasText(view.titleLabel?.text)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private func isTextViewSensitive(_ view: UITextView) -> Bool {
|
|
361
|
+
return (isTextInputSensitive(view) || view.isSensitiveText()) && hasText(view.text)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private func isSwitchSensitive(_ view: UISwitch) -> Bool {
|
|
365
|
+
var containsText = true
|
|
366
|
+
if #available(iOS 14.0, *) {
|
|
367
|
+
containsText = hasText(view.title)
|
|
368
|
+
}
|
|
369
|
+
return isTextInputSensitive(view) && containsText
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private func isTextFieldSensitive(_ view: UITextField) -> Bool {
|
|
373
|
+
return (isTextInputSensitive(view) || view.isSensitiveText()) && (hasText(view.text) || hasText(view.placeholder))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private func isSwiftUILayerSafe(_ layer: CALayer) -> Bool {
|
|
377
|
+
return swiftUISafeLayerTypes.contains(where: { layer.isKind(of: $0) })
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private func hasText(_ text: String?) -> Bool {
|
|
381
|
+
if let text = text, !text.isEmpty {
|
|
123
382
|
return true
|
|
383
|
+
} else {
|
|
384
|
+
// if there's no text, there's nothing to mask
|
|
385
|
+
return false
|
|
124
386
|
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private func isSwiftUIImageSensitive(_ view: UIView) -> Bool {
|
|
390
|
+
// No way of checking if this is an asset image or not
|
|
391
|
+
// No way of checking if there's actual content in the image or not
|
|
392
|
+
return maskImages || view.isNoCapture()
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private func isImageViewSensitive(_ view: UIImageView) -> Bool {
|
|
396
|
+
// if there's no image, there's nothing to mask
|
|
397
|
+
guard let image = view.image else { return false }
|
|
125
398
|
|
|
126
|
-
//
|
|
127
|
-
if view
|
|
399
|
+
// sensitive, regardless
|
|
400
|
+
if view.isNoCapture() {
|
|
128
401
|
return true
|
|
129
402
|
}
|
|
130
403
|
|
|
131
|
-
|
|
404
|
+
// asset images are probably not sensitive
|
|
405
|
+
if isAssetsImage(image) {
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// symbols are probably not sensitive
|
|
410
|
+
if image.isSymbolImage {
|
|
411
|
+
return false
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return maskImages
|
|
132
415
|
}
|
|
133
416
|
|
|
134
|
-
private func
|
|
135
|
-
//
|
|
136
|
-
|
|
417
|
+
private func isAssetsImage(_ image: UIImage) -> Bool {
|
|
418
|
+
// https://github.com/daydreamboy/lldb_scripts#9-pimage
|
|
419
|
+
// do not mask if its an asset image, likely not PII anyway
|
|
420
|
+
return image.imageAsset?.value(forKey: "_containingBundle") != nil
|
|
137
421
|
}
|
|
138
422
|
|
|
139
|
-
private func
|
|
140
|
-
//
|
|
141
|
-
|
|
423
|
+
private func applyCleanMask(in context: CGContext, frame: CGRect) {
|
|
424
|
+
// Final validation before drawing to prevent CoreGraphics NaN errors
|
|
425
|
+
guard frame.size.width.isFinite && frame.size.height.isFinite &&
|
|
426
|
+
frame.origin.x.isFinite && frame.origin.y.isFinite &&
|
|
427
|
+
frame.size.width > 0 && frame.size.height > 0 else {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Clean, consistent solid color masking approach
|
|
432
|
+
// Use system gray colors that adapt to light/dark mode
|
|
433
|
+
context.setFillColor(UIColor.systemGray5.cgColor)
|
|
142
434
|
context.fill(frame)
|
|
143
435
|
|
|
144
|
-
// Add
|
|
145
|
-
context.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let randomY = frame.origin.y + CGFloat.random(in: 0...frame.height)
|
|
149
|
-
let randomSize = CGFloat.random(in: 2...8)
|
|
150
|
-
context.fillEllipse(in: CGRect(x: randomX, y: randomY, width: randomSize, height: randomSize))
|
|
151
|
-
}
|
|
436
|
+
// Add subtle border for visual definition
|
|
437
|
+
context.setStrokeColor(UIColor.systemGray4.cgColor)
|
|
438
|
+
context.setLineWidth(0.5)
|
|
439
|
+
context.stroke(frame)
|
|
152
440
|
}
|
|
153
441
|
|
|
154
|
-
private func
|
|
155
|
-
//
|
|
156
|
-
|
|
442
|
+
private func applyBlurMask(in context: CGContext, frame: CGRect) {
|
|
443
|
+
// Final validation before drawing to prevent CoreGraphics NaN errors
|
|
444
|
+
guard frame.size.width.isFinite && frame.size.height.isFinite &&
|
|
445
|
+
frame.origin.x.isFinite && frame.origin.y.isFinite &&
|
|
446
|
+
frame.size.width > 0 && frame.size.height > 0 else {
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Clean solid color masking
|
|
451
|
+
// Use a consistent gray color for consistent appearance
|
|
452
|
+
context.setFillColor(UIColor.systemGray5.cgColor)
|
|
157
453
|
context.fill(frame)
|
|
158
454
|
|
|
159
|
-
// Add
|
|
160
|
-
context.
|
|
161
|
-
|
|
162
|
-
|
|
455
|
+
// Add subtle border for visual definition
|
|
456
|
+
context.setStrokeColor(UIColor.systemGray4.cgColor)
|
|
457
|
+
context.setLineWidth(1.0)
|
|
458
|
+
context.stroke(frame)
|
|
459
|
+
}
|
|
163
460
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
461
|
+
private func applyRectangleMask(in context: CGContext, frame: CGRect) {
|
|
462
|
+
// Final validation before drawing to prevent CoreGraphics NaN errors
|
|
463
|
+
guard frame.size.width.isFinite && frame.size.height.isFinite &&
|
|
464
|
+
frame.origin.x.isFinite && frame.origin.y.isFinite &&
|
|
465
|
+
frame.size.width > 0 && frame.size.height > 0 else {
|
|
466
|
+
return
|
|
168
467
|
}
|
|
468
|
+
|
|
469
|
+
// Clean solid rectangle masking
|
|
470
|
+
context.setFillColor(UIColor.systemGray5.cgColor)
|
|
471
|
+
context.fill(frame)
|
|
472
|
+
|
|
473
|
+
// Add subtle border
|
|
474
|
+
context.setStrokeColor(UIColor.systemGray4.cgColor)
|
|
475
|
+
context.setLineWidth(1.0)
|
|
476
|
+
context.stroke(frame)
|
|
169
477
|
}
|
|
170
478
|
|
|
171
479
|
private func applyPixelateMask(in context: CGContext, frame: CGRect, image: UIImage) {
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
for x in 0..<pixelCountX {
|
|
178
|
-
for y in 0..<pixelCountY {
|
|
179
|
-
let pixelFrame = CGRect(
|
|
180
|
-
x: frame.origin.x + CGFloat(x) * pixelSize,
|
|
181
|
-
y: frame.origin.y + CGFloat(y) * pixelSize,
|
|
182
|
-
width: pixelSize,
|
|
183
|
-
height: pixelSize
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
// Use a random color for each pixel
|
|
187
|
-
let randomColor = UIColor(
|
|
188
|
-
red: CGFloat.random(in: 0...1),
|
|
189
|
-
green: CGFloat.random(in: 0...1),
|
|
190
|
-
blue: CGFloat.random(in: 0...1),
|
|
191
|
-
alpha: 1.0
|
|
192
|
-
)
|
|
193
|
-
context.setFillColor(randomColor.cgColor)
|
|
194
|
-
context.fill(pixelFrame)
|
|
195
|
-
}
|
|
480
|
+
// Final validation before drawing to prevent CoreGraphics NaN errors
|
|
481
|
+
guard frame.size.width.isFinite && frame.size.height.isFinite &&
|
|
482
|
+
frame.origin.x.isFinite && frame.origin.y.isFinite &&
|
|
483
|
+
frame.size.width > 0 && frame.size.height > 0 else {
|
|
484
|
+
return
|
|
196
485
|
}
|
|
486
|
+
|
|
487
|
+
// Clean solid color masking (consistent with other methods)
|
|
488
|
+
context.setFillColor(UIColor.systemGray5.cgColor)
|
|
489
|
+
context.fill(frame)
|
|
490
|
+
|
|
491
|
+
// Add subtle border
|
|
492
|
+
context.setStrokeColor(UIColor.systemGray4.cgColor)
|
|
493
|
+
context.setLineWidth(1.0)
|
|
494
|
+
context.stroke(frame)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func resizeImage(_ image: UIImage, scale: CGFloat) -> UIImage {
|
|
498
|
+
// Simple approach: scale the logical size directly
|
|
499
|
+
let newSize = CGSize(
|
|
500
|
+
width: max(1.0, image.size.width * scale),
|
|
501
|
+
height: max(1.0, image.size.height * scale)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
// Use the same scale as the original image to maintain quality
|
|
506
|
+
UIGraphicsBeginImageContextWithOptions(newSize, false, image.scale)
|
|
507
|
+
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
508
|
+
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
509
|
+
UIGraphicsEndImageContext()
|
|
510
|
+
return newImage ?? image
|
|
197
511
|
}
|
|
198
512
|
}
|
|
199
513
|
|
|
@@ -203,3 +517,83 @@ private enum MaskingType {
|
|
|
203
517
|
case pixelate
|
|
204
518
|
case none
|
|
205
519
|
}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
extension UIView {
|
|
523
|
+
func isVisible() -> Bool {
|
|
524
|
+
// Check for NaN values in frame dimensions
|
|
525
|
+
let frame = self.frame
|
|
526
|
+
let width = frame.size.width
|
|
527
|
+
let height = frame.size.height
|
|
528
|
+
|
|
529
|
+
// Validate that dimensions are finite numbers (not NaN or infinite)
|
|
530
|
+
guard width.isFinite && height.isFinite else {
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return !isHidden && alpha > 0.01 && width > 0 && height > 0
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
func toAbsoluteRect(_ window: UIWindow) -> CGRect {
|
|
538
|
+
let bounds = self.bounds
|
|
539
|
+
|
|
540
|
+
// Validate bounds before conversion to prevent NaN values
|
|
541
|
+
guard bounds.size.width.isFinite && bounds.size.height.isFinite &&
|
|
542
|
+
bounds.origin.x.isFinite && bounds.origin.y.isFinite else {
|
|
543
|
+
// Return a zero rect if bounds contain NaN values
|
|
544
|
+
return CGRect.zero
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let convertedRect = convert(bounds, to: window)
|
|
548
|
+
|
|
549
|
+
// Validate the converted rect to ensure no NaN values were introduced
|
|
550
|
+
guard convertedRect.size.width.isFinite && convertedRect.size.height.isFinite &&
|
|
551
|
+
convertedRect.origin.x.isFinite && convertedRect.origin.y.isFinite else {
|
|
552
|
+
// Return a zero rect if conversion resulted in NaN values
|
|
553
|
+
return CGRect.zero
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return convertedRect
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
func isNoCapture() -> Bool {
|
|
560
|
+
// Check for common patterns that indicate sensitive content
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
// Check accessibility label for sensitive keywords
|
|
564
|
+
if let accessibilityLabel = accessibilityLabel?.lowercased() {
|
|
565
|
+
let sensitiveKeywords = ["password", "secret", "private", "sensitive", "confidential"]
|
|
566
|
+
if sensitiveKeywords.contains(where: accessibilityLabel.contains) {
|
|
567
|
+
return true
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Check for secure text entry
|
|
572
|
+
if let textField = self as? UITextField {
|
|
573
|
+
return textField.isSecureTextEntry
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Check for password-related class names or accessibility identifiers
|
|
577
|
+
if let accessibilityIdentifier = accessibilityIdentifier?.lowercased() {
|
|
578
|
+
let sensitiveIdentifiers = ["password", "secret", "private", "sensitive", "confidential"]
|
|
579
|
+
if sensitiveIdentifiers.contains(where: accessibilityIdentifier.contains) {
|
|
580
|
+
return true
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return false
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func isSensitiveText() -> Bool {
|
|
588
|
+
// Check if this view contains sensitive text content
|
|
589
|
+
if let textField = self as? UITextField {
|
|
590
|
+
return textField.isSecureTextEntry || isNoCapture()
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if let textView = self as? UITextView {
|
|
594
|
+
return isNoCapture()
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return false
|
|
598
|
+
}
|
|
599
|
+
}
|