@multiplayer-app/session-recorder-react-native 0.0.1-beta.13 → 0.0.1-beta.14

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.
Files changed (31) hide show
  1. package/copy-react-native-dist.sh +4 -0
  2. package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -1
  3. package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -1
  4. package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js +1 -1
  5. package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js.map +1 -1
  6. package/dist/config/defaults.d.ts +1 -1
  7. package/dist/config/defaults.js +1 -1
  8. package/dist/config/defaults.js.map +1 -1
  9. package/dist/config/masking.js +1 -1
  10. package/dist/config/masking.js.map +1 -1
  11. package/dist/config/session-recorder.js +1 -1
  12. package/dist/config/session-recorder.js.map +1 -1
  13. package/dist/native/SessionRecorderNative.d.ts +13 -3
  14. package/dist/native/SessionRecorderNative.js.map +1 -1
  15. package/dist/recorder/screenRecorder.js +1 -1
  16. package/dist/recorder/screenRecorder.js.map +1 -1
  17. package/dist/services/api.service.js +1 -1
  18. package/dist/services/api.service.js.map +1 -1
  19. package/dist/services/screenMaskingService.d.ts +13 -5
  20. package/dist/services/screenMaskingService.js +1 -1
  21. package/dist/services/screenMaskingService.js.map +1 -1
  22. package/dist/session-recorder.js +1 -1
  23. package/dist/session-recorder.js.map +1 -1
  24. package/dist/types/session-recorder.d.ts +22 -2
  25. package/dist/types/session-recorder.js.map +1 -1
  26. package/dist/utils/platform.js +1 -1
  27. package/dist/utils/platform.js.map +1 -1
  28. package/dist/utils/rrweb-events.js +1 -1
  29. package/dist/utils/rrweb-events.js.map +1 -1
  30. package/ios/SessionRecorderNative.swift +435 -82
  31. package/package.json +6 -5
@@ -1,9 +1,48 @@
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.1
16
+
17
+ // React Native view types
18
+ private let reactNativeTextView: AnyClass? = NSClassFromString("RCTTextView")
19
+ private let reactNativeImageView: AnyClass? = NSClassFromString("RCTImageView")
20
+ private let reactNativeTextInput: AnyClass? = NSClassFromString("RCTUITextField")
21
+ private let reactNativeTextInputView: AnyClass? = NSClassFromString("RCTUITextView")
22
+
23
+ // System sandboxed views (usually sensitive)
24
+ private let systemSandboxedView: AnyClass? = NSClassFromString("_UIRemoteView")
25
+
26
+ // SwiftUI view types
27
+ private let swiftUITextBasedViewTypes = [
28
+ "SwiftUI.CGDrawingView", // Text, Button
29
+ "SwiftUI.TextEditorTextView", // TextEditor
30
+ "SwiftUI.VerticalTextView", // TextField, vertical axis
31
+ ].compactMap(NSClassFromString)
32
+
33
+ private let swiftUIImageLayerTypes = [
34
+ "SwiftUI.ImageLayer",
35
+ ].compactMap(NSClassFromString)
36
+
37
+ private let swiftUIGenericTypes = [
38
+ "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView",
39
+ ].compactMap(NSClassFromString)
40
+
41
+ // Safe layer types that shouldn't be masked
42
+ private let swiftUISafeLayerTypes: [AnyClass] = [
43
+ "SwiftUI.GradientLayer", // Views like LinearGradient, RadialGradient, or AngularGradient
44
+ ].compactMap(NSClassFromString)
45
+
7
46
  @objc func captureAndMask(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
8
47
  DispatchQueue.main.async {
9
48
  guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
@@ -24,7 +63,7 @@ class SessionRecorderNative: NSObject {
24
63
  // Apply masking to sensitive elements
25
64
  let maskedImage = self.applyMasking(to: image, in: window)
26
65
 
27
- if let data = maskedImage.jpegData(compressionQuality: 0.5) {
66
+ if let data = maskedImage.jpegData(compressionQuality: self.imageQuality) {
28
67
  let base64 = data.base64EncodedString()
29
68
  resolve(base64)
30
69
  } else {
@@ -35,6 +74,9 @@ class SessionRecorderNative: NSObject {
35
74
 
36
75
  @objc func captureAndMaskWithOptions(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
37
76
  DispatchQueue.main.async {
77
+ // Update configuration from options
78
+ self.updateConfiguration(from: options)
79
+
38
80
  guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
39
81
  reject("NO_WINDOW", "Unable to get key window", nil)
40
82
  return
@@ -53,7 +95,7 @@ class SessionRecorderNative: NSObject {
53
95
  // Apply masking with custom options
54
96
  let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
55
97
 
56
- if let data = maskedImage.jpegData(compressionQuality: 0.5) {
98
+ if let data = maskedImage.jpegData(compressionQuality: self.imageQuality) {
57
99
  let base64 = data.base64EncodedString()
58
100
  resolve(base64)
59
101
  } else {
@@ -62,6 +104,30 @@ class SessionRecorderNative: NSObject {
62
104
  }
63
105
  }
64
106
 
107
+ private func updateConfiguration(from options: NSDictionary) {
108
+ if let maskTextInputs = options["maskTextInputs"] as? Bool {
109
+ self.maskTextInputs = maskTextInputs
110
+ }
111
+ if let maskImages = options["maskImages"] as? Bool {
112
+ self.maskImages = maskImages
113
+ }
114
+ if let maskSandboxedViews = options["maskSandboxedViews"] as? Bool {
115
+ self.maskSandboxedViews = maskSandboxedViews
116
+ }
117
+ if let maskButtons = options["maskButtons"] as? Bool {
118
+ self.maskButtons = maskButtons
119
+ }
120
+ if let maskLabels = options["maskLabels"] as? Bool {
121
+ self.maskLabels = maskLabels
122
+ }
123
+ if let maskWebViews = options["maskWebViews"] as? Bool {
124
+ self.maskWebViews = maskWebViews
125
+ }
126
+ if let quality = options["quality"] as? NSNumber {
127
+ self.imageQuality = CGFloat(quality.floatValue)
128
+ }
129
+ }
130
+
65
131
  private func applyMasking(to image: UIImage, in window: UIWindow) -> UIImage {
66
132
  return applyMaskingWithOptions(to: image, in: window, options: [:])
67
133
  }
@@ -73,23 +139,26 @@ class SessionRecorderNative: NSObject {
73
139
  // Draw the original image
74
140
  image.draw(in: CGRect(origin: .zero, size: image.size))
75
141
 
76
- // Find and mask sensitive elements
77
- let sensitiveElements = findSensitiveElements(in: window)
78
142
 
79
- for element in sensitiveElements {
80
- let frame = element.frame
81
- let maskingType = getMaskingType(for: element)
143
+ var maskableWidgets: [CGRect] = []
144
+ var maskChildren = false
145
+ findMaskableWidgets(window, window, &maskableWidgets, &maskChildren)
146
+
147
+ for frame in maskableWidgets {
148
+ // Skip zero rects (which indicate invalid coordinates)
149
+ if frame == CGRect.zero { continue }
82
150
 
83
- switch maskingType {
84
- case .blur:
85
- applyBlurMask(in: context, frame: frame)
86
- case .rectangle:
87
- applyRectangleMask(in: context, frame: frame)
88
- case .pixelate:
89
- applyPixelateMask(in: context, frame: frame, image: image)
90
- case .none:
91
- break
151
+ // Validate frame dimensions before processing
152
+ guard frame.size.width.isFinite && frame.size.height.isFinite &&
153
+ frame.origin.x.isFinite && frame.origin.y.isFinite else {
154
+ continue
92
155
  }
156
+
157
+ // Clip the frame to the image bounds to avoid drawing outside context
158
+ let clippedFrame = frame.intersection(CGRect(origin: .zero, size: image.size))
159
+ if clippedFrame.isNull || clippedFrame.isEmpty { continue }
160
+
161
+ applyCleanMask(in: context, frame: clippedFrame)
93
162
  }
94
163
 
95
164
  let maskedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
@@ -98,102 +167,306 @@ class SessionRecorderNative: NSObject {
98
167
  return maskedImage
99
168
  }
100
169
 
101
- private func findSensitiveElements(in view: UIView) -> [UIView] {
102
- var sensitiveElements: [UIView] = []
170
+ private func findMaskableWidgets(_ view: UIView, _ window: UIWindow, _ maskableWidgets: inout [CGRect], _ maskChildren: inout Bool) {
171
+ // Skip hidden or transparent views
172
+ if !view.isVisible() {
173
+ return
174
+ }
103
175
 
104
- func traverseView(_ view: UIView) {
105
- // Check if this view should be masked
106
- if shouldMaskView(view) {
107
- sensitiveElements.append(view)
176
+ // Check for UITextView (TextEditor, SwiftUI.TextEditorTextView, SwiftUI.UIKitTextView)
177
+ if let textView = view as? UITextView {
178
+ if isTextViewSensitive(textView) {
179
+ maskableWidgets.append(view.toAbsoluteRect(window))
180
+ return
108
181
  }
182
+ }
109
183
 
110
- // Recursively check subviews
111
- for subview in view.subviews {
112
- traverseView(subview)
184
+ // Check for UITextField (SwiftUI: TextField, SecureField)
185
+ if let textField = view as? UITextField {
186
+ if isTextFieldSensitive(textField) {
187
+ maskableWidgets.append(view.toAbsoluteRect(window))
188
+ return
113
189
  }
114
190
  }
115
191
 
116
- traverseView(view)
117
- return sensitiveElements
192
+ // React Native text views
193
+ if let reactNativeTextView = reactNativeTextView {
194
+ if view.isKind(of: reactNativeTextView), maskTextInputs {
195
+ maskableWidgets.append(view.toAbsoluteRect(window))
196
+ return
197
+ }
198
+ }
199
+
200
+ // React Native text inputs
201
+ if let reactNativeTextInput = reactNativeTextInput {
202
+ if view.isKind(of: reactNativeTextInput), maskTextInputs {
203
+ maskableWidgets.append(view.toAbsoluteRect(window))
204
+ return
205
+ }
206
+ }
207
+
208
+ if let reactNativeTextInputView = reactNativeTextInputView {
209
+ if view.isKind(of: reactNativeTextInputView), maskTextInputs {
210
+ maskableWidgets.append(view.toAbsoluteRect(window))
211
+ return
212
+ }
213
+ }
214
+
215
+ // UIImageView (SwiftUI: Some control images like the ones in Picker view)
216
+ if let imageView = view as? UIImageView {
217
+ if isImageViewSensitive(imageView) {
218
+ maskableWidgets.append(view.toAbsoluteRect(window))
219
+ return
220
+ }
221
+ }
222
+
223
+ // React Native image views
224
+ if let reactNativeImageView = reactNativeImageView {
225
+ if view.isKind(of: reactNativeImageView), maskImages {
226
+ maskableWidgets.append(view.toAbsoluteRect(window))
227
+ return
228
+ }
229
+ }
230
+
231
+ // UILabel (Text, this code might never be reachable in SwiftUI)
232
+ if let label = view as? UILabel {
233
+ if isLabelSensitive(label) {
234
+ maskableWidgets.append(view.toAbsoluteRect(window))
235
+ return
236
+ }
237
+ }
238
+
239
+ // WKWebView (Link, this code might never be reachable in SwiftUI)
240
+ if let webView = view as? WKWebView {
241
+ if isAnyInputSensitive(webView) {
242
+ maskableWidgets.append(view.toAbsoluteRect(window))
243
+ return
244
+ }
245
+ }
246
+
247
+ // UIButton (SwiftUI: SwiftUI.UIKitIconPreferringButton and other subclasses)
248
+ if let button = view as? UIButton {
249
+ if isButtonSensitive(button) {
250
+ maskableWidgets.append(view.toAbsoluteRect(window))
251
+ return
252
+ }
253
+ }
254
+
255
+ // UISwitch (SwiftUI: Toggle)
256
+ if let theSwitch = view as? UISwitch {
257
+ if isSwitchSensitive(theSwitch) {
258
+ maskableWidgets.append(view.toAbsoluteRect(window))
259
+ return
260
+ }
261
+ }
262
+
263
+ // UIPickerView (SwiftUI: Picker with .pickerStyle(.wheel))
264
+ if let picker = view as? UIPickerView {
265
+ if isTextInputSensitive(picker), !view.subviews.isEmpty {
266
+ maskableWidgets.append(picker.toAbsoluteRect(window))
267
+ return
268
+ }
269
+ }
270
+
271
+ // Detect any views that don't belong to the current process (likely system views)
272
+ if maskSandboxedViews,
273
+ let systemSandboxedView,
274
+ view.isKind(of: systemSandboxedView) {
275
+ maskableWidgets.append(view.toAbsoluteRect(window))
276
+ return
277
+ }
278
+
279
+ let hasSubViews = !view.subviews.isEmpty
280
+
281
+ // SwiftUI: Text based views like Text, Button, TextEditor
282
+ if swiftUITextBasedViewTypes.contains(where: view.isKind(of:)) {
283
+ if isTextInputSensitive(view), !hasSubViews {
284
+ maskableWidgets.append(view.toAbsoluteRect(window))
285
+ return
286
+ }
287
+ }
288
+
289
+ // SwiftUI: Image based views like Image, AsyncImage
290
+ if swiftUIImageLayerTypes.contains(where: view.layer.isKind(of:)) {
291
+ if isSwiftUIImageSensitive(view), !hasSubViews {
292
+ maskableWidgets.append(view.toAbsoluteRect(window))
293
+ return
294
+ }
295
+ }
296
+
297
+ // Generic SwiftUI types
298
+ if swiftUIGenericTypes.contains(where: { view.isKind(of: $0) }), !isSwiftUILayerSafe(view.layer) {
299
+ if isTextInputSensitive(view), !hasSubViews {
300
+ maskableWidgets.append(view.toAbsoluteRect(window))
301
+ return
302
+ }
303
+ }
304
+
305
+ // Recursively check subviews
306
+ if !view.subviews.isEmpty {
307
+ for child in view.subviews {
308
+ if !child.isVisible() {
309
+ continue
310
+ }
311
+ findMaskableWidgets(child, window, &maskableWidgets, &maskChildren)
312
+ }
313
+ }
314
+ maskChildren = false
315
+ }
316
+
317
+ // MARK: - Sensitive Content Detection Methods
318
+
319
+ private func isAnyInputSensitive(_ view: UIView) -> Bool {
320
+ return isTextInputSensitive(view) || maskImages
321
+ }
322
+
323
+ private func isTextInputSensitive(_ view: UIView) -> Bool {
324
+ return maskTextInputs || view.isNoCapture()
325
+ }
326
+
327
+ private func isLabelSensitive(_ view: UILabel) -> Bool {
328
+ return isTextInputSensitive(view) && hasText(view.text)
329
+ }
330
+
331
+ private func isButtonSensitive(_ view: UIButton) -> Bool {
332
+ return isTextInputSensitive(view) && hasText(view.titleLabel?.text)
333
+ }
334
+
335
+ private func isTextViewSensitive(_ view: UITextView) -> Bool {
336
+ return (isTextInputSensitive(view) || view.isSensitiveText()) && hasText(view.text)
337
+ }
338
+
339
+ private func isSwitchSensitive(_ view: UISwitch) -> Bool {
340
+ var containsText = true
341
+ if #available(iOS 14.0, *) {
342
+ containsText = hasText(view.title)
343
+ }
344
+ return isTextInputSensitive(view) && containsText
345
+ }
346
+
347
+ private func isTextFieldSensitive(_ view: UITextField) -> Bool {
348
+ return (isTextInputSensitive(view) || view.isSensitiveText()) && (hasText(view.text) || hasText(view.placeholder))
349
+ }
350
+
351
+ private func isSwiftUILayerSafe(_ layer: CALayer) -> Bool {
352
+ return swiftUISafeLayerTypes.contains(where: { layer.isKind(of: $0) })
118
353
  }
119
354
 
120
- private func shouldMaskView(_ view: UIView) -> Bool {
121
- // Check for UITextField - mask all text fields when inputMasking is enabled
122
- if view is UITextField {
355
+ private func hasText(_ text: String?) -> Bool {
356
+ if let text = text, !text.isEmpty {
123
357
  return true
358
+ } else {
359
+ // if there's no text, there's nothing to mask
360
+ return false
124
361
  }
362
+ }
363
+
364
+ private func isSwiftUIImageSensitive(_ view: UIView) -> Bool {
365
+ // No way of checking if this is an asset image or not
366
+ // No way of checking if there's actual content in the image or not
367
+ return maskImages || view.isNoCapture()
368
+ }
369
+
370
+ private func isImageViewSensitive(_ view: UIImageView) -> Bool {
371
+ // if there's no image, there's nothing to mask
372
+ guard let image = view.image else { return false }
125
373
 
126
- // Check for UITextView - mask all text views when inputMasking is enabled
127
- if view is UITextView {
374
+ // sensitive, regardless
375
+ if view.isNoCapture() {
128
376
  return true
129
377
  }
130
378
 
131
- return false
379
+ // asset images are probably not sensitive
380
+ if isAssetsImage(image) {
381
+ return false
382
+ }
383
+
384
+ // symbols are probably not sensitive
385
+ if image.isSymbolImage {
386
+ return false
387
+ }
388
+
389
+ return maskImages
132
390
  }
133
391
 
134
- private func getMaskingType(for view: UIView) -> MaskingType {
135
- // Default masking type for all text inputs
136
- return .rectangle
392
+ private func isAssetsImage(_ image: UIImage) -> Bool {
393
+ // https://github.com/daydreamboy/lldb_scripts#9-pimage
394
+ // do not mask if its an asset image, likely not PII anyway
395
+ return image.imageAsset?.value(forKey: "_containingBundle") != nil
137
396
  }
138
397
 
139
- private func applyBlurMask(in context: CGContext, frame: CGRect) {
140
- // Create a blur effect
141
- context.setFillColor(UIColor.black.withAlphaComponent(0.8).cgColor)
398
+ private func applyCleanMask(in context: CGContext, frame: CGRect) {
399
+ // Final validation before drawing to prevent CoreGraphics NaN errors
400
+ guard frame.size.width.isFinite && frame.size.height.isFinite &&
401
+ frame.origin.x.isFinite && frame.origin.y.isFinite &&
402
+ frame.size.width > 0 && frame.size.height > 0 else {
403
+ return
404
+ }
405
+
406
+ // Clean, consistent solid color masking approach
407
+ // Use system gray colors that adapt to light/dark mode
408
+ context.setFillColor(UIColor.systemGray5.cgColor)
142
409
  context.fill(frame)
143
410
 
144
- // Add some noise to make it look blurred
145
- context.setFillColor(UIColor.white.withAlphaComponent(0.3).cgColor)
146
- for _ in 0..<20 {
147
- let randomX = frame.origin.x + CGFloat.random(in: 0...frame.width)
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
- }
411
+ // Add subtle border for visual definition
412
+ context.setStrokeColor(UIColor.systemGray4.cgColor)
413
+ context.setLineWidth(0.5)
414
+ context.stroke(frame)
152
415
  }
153
416
 
154
- private func applyRectangleMask(in context: CGContext, frame: CGRect) {
155
- // Simple rectangle fill
156
- context.setFillColor(UIColor.gray.cgColor)
417
+ private func applyBlurMask(in context: CGContext, frame: CGRect) {
418
+ // Final validation before drawing to prevent CoreGraphics NaN errors
419
+ guard frame.size.width.isFinite && frame.size.height.isFinite &&
420
+ frame.origin.x.isFinite && frame.origin.y.isFinite &&
421
+ frame.size.width > 0 && frame.size.height > 0 else {
422
+ return
423
+ }
424
+
425
+ // Clean solid color masking
426
+ // Use a consistent gray color for consistent appearance
427
+ context.setFillColor(UIColor.systemGray5.cgColor)
157
428
  context.fill(frame)
158
429
 
159
- // Add some text-like pattern
160
- context.setFillColor(UIColor.darkGray.cgColor)
161
- let lineHeight: CGFloat = 4
162
- let spacing: CGFloat = 8
430
+ // Add subtle border for visual definition
431
+ context.setStrokeColor(UIColor.systemGray4.cgColor)
432
+ context.setLineWidth(1.0)
433
+ context.stroke(frame)
434
+ }
163
435
 
164
- for i in stride(from: frame.origin.y + spacing, to: frame.origin.y + frame.height - spacing, by: lineHeight + spacing) {
165
- let lineWidth = CGFloat.random(in: frame.width * 0.3...frame.width * 0.8)
166
- let lineX = frame.origin.x + CGFloat.random(in: 0...(frame.width - lineWidth))
167
- context.fill(CGRect(x: lineX, y: i, width: lineWidth, height: lineHeight))
436
+ private func applyRectangleMask(in context: CGContext, frame: CGRect) {
437
+ // Final validation before drawing to prevent CoreGraphics NaN errors
438
+ guard frame.size.width.isFinite && frame.size.height.isFinite &&
439
+ frame.origin.x.isFinite && frame.origin.y.isFinite &&
440
+ frame.size.width > 0 && frame.size.height > 0 else {
441
+ return
168
442
  }
443
+
444
+ // Clean solid rectangle masking
445
+ context.setFillColor(UIColor.systemGray5.cgColor)
446
+ context.fill(frame)
447
+
448
+ // Add subtle border
449
+ context.setStrokeColor(UIColor.systemGray4.cgColor)
450
+ context.setLineWidth(1.0)
451
+ context.stroke(frame)
169
452
  }
170
453
 
171
454
  private func applyPixelateMask(in context: CGContext, frame: CGRect, image: UIImage) {
172
- // Create a pixelated effect
173
- let pixelSize: CGFloat = 8
174
- let pixelCountX = Int(frame.width / pixelSize)
175
- let pixelCountY = Int(frame.height / pixelSize)
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
- }
455
+ // Final validation before drawing to prevent CoreGraphics NaN errors
456
+ guard frame.size.width.isFinite && frame.size.height.isFinite &&
457
+ frame.origin.x.isFinite && frame.origin.y.isFinite &&
458
+ frame.size.width > 0 && frame.size.height > 0 else {
459
+ return
196
460
  }
461
+
462
+ // Clean solid color masking (consistent with other methods)
463
+ context.setFillColor(UIColor.systemGray5.cgColor)
464
+ context.fill(frame)
465
+
466
+ // Add subtle border
467
+ context.setStrokeColor(UIColor.systemGray4.cgColor)
468
+ context.setLineWidth(1.0)
469
+ context.stroke(frame)
197
470
  }
198
471
  }
199
472
 
@@ -203,3 +476,83 @@ private enum MaskingType {
203
476
  case pixelate
204
477
  case none
205
478
  }
479
+
480
+
481
+ extension UIView {
482
+ func isVisible() -> Bool {
483
+ // Check for NaN values in frame dimensions
484
+ let frame = self.frame
485
+ let width = frame.size.width
486
+ let height = frame.size.height
487
+
488
+ // Validate that dimensions are finite numbers (not NaN or infinite)
489
+ guard width.isFinite && height.isFinite else {
490
+ return false
491
+ }
492
+
493
+ return !isHidden && alpha > 0.01 && width > 0 && height > 0
494
+ }
495
+
496
+ func toAbsoluteRect(_ window: UIWindow) -> CGRect {
497
+ let bounds = self.bounds
498
+
499
+ // Validate bounds before conversion to prevent NaN values
500
+ guard bounds.size.width.isFinite && bounds.size.height.isFinite &&
501
+ bounds.origin.x.isFinite && bounds.origin.y.isFinite else {
502
+ // Return a zero rect if bounds contain NaN values
503
+ return CGRect.zero
504
+ }
505
+
506
+ let convertedRect = convert(bounds, to: window)
507
+
508
+ // Validate the converted rect to ensure no NaN values were introduced
509
+ guard convertedRect.size.width.isFinite && convertedRect.size.height.isFinite &&
510
+ convertedRect.origin.x.isFinite && convertedRect.origin.y.isFinite else {
511
+ // Return a zero rect if conversion resulted in NaN values
512
+ return CGRect.zero
513
+ }
514
+
515
+ return convertedRect
516
+ }
517
+
518
+ func isNoCapture() -> Bool {
519
+ // Check for common patterns that indicate sensitive content
520
+
521
+
522
+ // Check accessibility label for sensitive keywords
523
+ if let accessibilityLabel = accessibilityLabel?.lowercased() {
524
+ let sensitiveKeywords = ["password", "secret", "private", "sensitive", "confidential"]
525
+ if sensitiveKeywords.contains(where: accessibilityLabel.contains) {
526
+ return true
527
+ }
528
+ }
529
+
530
+ // Check for secure text entry
531
+ if let textField = self as? UITextField {
532
+ return textField.isSecureTextEntry
533
+ }
534
+
535
+ // Check for password-related class names or accessibility identifiers
536
+ if let accessibilityIdentifier = accessibilityIdentifier?.lowercased() {
537
+ let sensitiveIdentifiers = ["password", "secret", "private", "sensitive", "confidential"]
538
+ if sensitiveIdentifiers.contains(where: accessibilityIdentifier.contains) {
539
+ return true
540
+ }
541
+ }
542
+
543
+ return false
544
+ }
545
+
546
+ func isSensitiveText() -> Bool {
547
+ // Check if this view contains sensitive text content
548
+ if let textField = self as? UITextField {
549
+ return textField.isSecureTextEntry || isNoCapture()
550
+ }
551
+
552
+ if let textView = self as? UITextView {
553
+ return isNoCapture()
554
+ }
555
+
556
+ return false
557
+ }
558
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplayer-app/session-recorder-react-native",
3
- "version": "0.0.1-beta.13",
3
+ "version": "0.0.1-beta.14",
4
4
  "description": "Multiplayer Fullstack Session Recorder for React Native",
5
5
  "author": {
6
6
  "name": "Multiplayer Software, Inc.",
@@ -40,8 +40,7 @@
40
40
  "scripts": {
41
41
  "clean": "rimraf dist",
42
42
  "prepublishOnly": "npm run build",
43
- "build": "tsc && babel ./dist --out-dir dist --extensions '.js'",
44
- "generate-metadata": "node scripts/generate-app-metadata.js"
43
+ "build": "tsc && babel ./dist --out-dir dist --extensions '.js' && ./copy-react-native-dist.sh"
45
44
  },
46
45
  "devDependencies": {
47
46
  "@babel/cli": "^7.19.3",
@@ -56,7 +55,8 @@
56
55
  "@types/react-native": "^0.72.0",
57
56
  "eslint": "8.48.0",
58
57
  "rimraf": "^5.0.5",
59
- "typescript": "5.7.3"
58
+ "typescript": "5.7.3",
59
+ "react-native-safe-area-context": "^5.6.0"
60
60
  },
61
61
  "dependencies": {
62
62
  "@multiplayer-app/session-recorder-common": "1.2.6",
@@ -84,7 +84,8 @@
84
84
  "@opentelemetry/api": "^1.9.0",
85
85
  "react": ">=18.0.0 <20.0.0",
86
86
  "react-native": ">=0.72.0 <0.82.0",
87
- "react-native-svg": "^15.13.0"
87
+ "react-native-svg": "^15.13.0",
88
+ "react-native-safe-area-context": ">=4.0.0 <6.0.0 || ^5.0.0"
88
89
  },
89
90
  "optionalDependencies": {
90
91
  "expo-constants": "^15.0.0"